feat(greenfield): rebuild SlabUploader M1-M3 on clean foundation, all gates green
Clean re-implementation of the full product (not a port of the old M3 tree): - P0 foundation: 13-table schema + per-call sqlite factory (thread-safe), FastAPI app (health, JSON logging + secret redaction, CORS, startup guard). - P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure, png crop/inventory/overlay, EXIF decodeOrientedBitmap. - P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop to batch) + photos-ready lineup. - P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing, inference Call1/Call2, review, publish, settings, slabs, photos) + real FE review/publish/settings screens consuming the API (no hardcoded Woo ids). - P4 gate exit: check_requirements + check_coverage both exit 0 (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green. Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
This commit is contained in:
+33
-50
@@ -1,7 +1,10 @@
|
||||
"""S1 Settings service (DATA-MODEL §6, API.md §10, SETTINGS-5/15/20/25/30/75/85/90).
|
||||
"""Settings service (SETTINGS-5/15/20/25/30/75/85/90).
|
||||
|
||||
Secrets (value_enc) are AES-GCM write-only; GET returns only {configured: bool}.
|
||||
woo_base_url is env read-only. Flags default false. Partial PUT. Decrypt fail -> configured:false (no 500, no leak).
|
||||
Secrets (value_enc) are AES-GCM write-only; GET returns only {configured: bool}
|
||||
by default (SETTINGS-75 masking). SLAB_UAT_UNMASK_SECRETS=1 is a dev-test-only
|
||||
unmask (never set in prod). woo_base_url is env read-only (SETTINGS-85).
|
||||
Partial PUT; unknown keys ignored. Decrypt failure -> configured:false (no 500,
|
||||
no leak).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,7 +13,7 @@ from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .crypto import decrypt, encrypt, DecryptError
|
||||
from .crypto import decrypt, encrypt
|
||||
from .db import get_connection
|
||||
|
||||
SECRET_KEYS: set[str] = {
|
||||
@@ -44,29 +47,26 @@ DEFAULTS: dict[str, Any] = {
|
||||
|
||||
|
||||
def _row_to_value(row: dict) -> Any:
|
||||
if row is None:
|
||||
return None
|
||||
v = row.get("value")
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, str):
|
||||
if v.lower() in ("true", "false"):
|
||||
return v.lower() == "true"
|
||||
if isinstance(v, str) and v.lower() in ("true", "false"):
|
||||
return v.lower() == "true"
|
||||
return v
|
||||
|
||||
|
||||
def get_settings() -> dict[str, Any]:
|
||||
conn = get_connection()
|
||||
rows = {}
|
||||
for r in conn.execute("SELECT key, value, value_enc FROM settings").fetchall():
|
||||
rows[r["key"]] = {"value": r["value"], "value_enc": r["value_enc"]}
|
||||
rows = {
|
||||
r["key"]: {"value": r["value"], "value_enc": r["value_enc"]}
|
||||
for r in conn.execute("SELECT key, value, value_enc FROM settings").fetchall()
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
result["woo_base_url"] = os.environ.get("WOO_BASE_URL", "")
|
||||
result: dict[str, Any] = {"woo_base_url": os.environ.get("WOO_BASE_URL", "")}
|
||||
|
||||
# TEMPORARY UAT override (owner-directed, LAN/HTTP only): when set, secrets are
|
||||
# returned as their decrypted plaintext so they can be inspected on the dev-test
|
||||
# box. Default OFF -> SETTINGS-75 masking intact. Remove before prod.
|
||||
# TEMPORARY UAT override (owner-directed, LAN/HTTP only): when set, secrets
|
||||
# are returned as decrypted plaintext for dev-test inspection. Default OFF ->
|
||||
# SETTINGS-75 masking intact. Never set in prod.
|
||||
unmask = os.environ.get("SLAB_UAT_UNMASK_SECRETS") == "1"
|
||||
|
||||
for k in sorted(ALL_KEYS):
|
||||
@@ -81,28 +81,23 @@ def get_settings() -> dict[str, Any]:
|
||||
result[k] = ""
|
||||
else:
|
||||
result[k] = ""
|
||||
else:
|
||||
if enc:
|
||||
try:
|
||||
decrypt(enc) # verify roundtrip possible
|
||||
result[k] = {"configured": True}
|
||||
except Exception:
|
||||
result[k] = {"configured": False}
|
||||
else:
|
||||
elif enc:
|
||||
try:
|
||||
decrypt(enc) # verify roundtrip possible
|
||||
result[k] = {"configured": True}
|
||||
except Exception:
|
||||
result[k] = {"configured": False}
|
||||
else:
|
||||
if k in rows:
|
||||
val = _row_to_value(row)
|
||||
result[k] = val if val is not None else DEFAULTS.get(k)
|
||||
else:
|
||||
result[k] = DEFAULTS.get(k)
|
||||
result[k] = {"configured": False}
|
||||
else:
|
||||
val = _row_to_value(row) if k in rows else None
|
||||
result[k] = val if val is not None else DEFAULTS.get(k)
|
||||
if k in NUMERIC_KEYS:
|
||||
try:
|
||||
f = float(result[k])
|
||||
result[k] = int(f) if f.is_integer() else f
|
||||
except (TypeError, ValueError):
|
||||
result[k] = DEFAULTS.get(k)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -141,34 +136,22 @@ def update_settings(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
conn = get_connection()
|
||||
for k, v in payload.items():
|
||||
if k not in ALL_KEYS:
|
||||
continue # ignore unknown for partial
|
||||
continue # partial update: unknown keys ignored
|
||||
if k in SECRET_KEYS:
|
||||
if v in (None, "", False):
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES (?,?,NULL)",
|
||||
(k, None),
|
||||
)
|
||||
conn.execute("INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES (?,?,NULL)", (k, None))
|
||||
else:
|
||||
try:
|
||||
enc = encrypt(str(v))
|
||||
except Exception:
|
||||
enc = None
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES (?,?,?)",
|
||||
(k, None, enc),
|
||||
(k, None, encrypt(str(v))),
|
||||
)
|
||||
else:
|
||||
# plain
|
||||
if v in (None, ""):
|
||||
conn.execute("DELETE FROM settings WHERE key=?", (k,))
|
||||
elif k in ("inference_enabled", "content_llm_enabled"):
|
||||
conn.execute("INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES (?,?,NULL)",
|
||||
(k, "true" if bool(v) else "false"))
|
||||
else:
|
||||
if k in ("inference_enabled", "content_llm_enabled"):
|
||||
val_str = "true" if bool(v) else "false"
|
||||
else:
|
||||
val_str = str(v)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES (?,?,NULL)",
|
||||
(k, val_str),
|
||||
)
|
||||
conn.execute("INSERT OR REPLACE INTO settings(key, value, value_enc) VALUES (?,?,NULL)", (k, str(v)))
|
||||
conn.commit()
|
||||
return get_settings()
|
||||
|
||||
Reference in New Issue
Block a user