Workspace tables are editable only when the table is in the canonical workspace DB.
Collection and strategy tables are read-only by design unless the backing store explicitly supports editing.
Table browser ready0 rowsfilter=nonepage=0
If a table shows "no rows", the current filter or selected table has no visible records.
"""
class SnapshotAdminHandler(BaseHTTPRequestHandler):
db_path: Path = DEFAULT_DB
seed_json_path: Path = DEFAULT_SEED_JSON
def log_message(self, format: str, *args: Any) -> None: # noqa: A003
return
def _handle_exception(self, exc: Exception) -> None:
_json_response(self, HTTPStatus.INTERNAL_SERVER_ERROR, {"detail": str(exc)})
def do_GET(self) -> None: # noqa: N802
parsed = urlparse(self.path)
if parsed.path == "/":
_text_response(self, HTTPStatus.OK, render_index_html(), "text/html; charset=utf-8")
return
if parsed.path == "/collection":
_text_response(self, HTTPStatus.OK, render_collection_html(), "text/html; charset=utf-8")
return
if parsed.path == "/tables":
_text_response(self, HTTPStatus.OK, render_tables_html(), "text/html; charset=utf-8")
return
if parsed.path == "/api/tables":
_json_response(self, HTTPStatus.OK, {"tables": list_browsable_tables(self.db_path)})
return
if parsed.path == "/api/table_rows":
query = parse_qs(parsed.query)
table = (query.get("table") or [""])[0]
try:
limit = int((query.get("limit") or ["50"])[0])
offset = int((query.get("offset") or ["0"])[0])
except ValueError:
_json_response(self, HTTPStatus.BAD_REQUEST, {"detail": "limit/offset must be integers"})
return
limit = min(max(limit, 1), 500)
offset = max(offset, 0)
filter_text = (query.get("filter") or [""])[0]
column_filters: dict[str, str] = {}
for key, values in query.items():
if key.startswith("filter_") and values:
column_filters[key.removeprefix("filter_")] = values[0]
try:
payload = fetch_table_rows(
table,
self.db_path,
limit=limit,
offset=offset,
filter_text=filter_text,
column_filters=column_filters,
)
except ValueError as exc:
_json_response(self, HTTPStatus.BAD_REQUEST, {"detail": str(exc)})
return
_json_response(self, HTTPStatus.OK, payload)
return
if parsed.path == "/api/state":
_json_response(self, HTTPStatus.OK, build_ui_state(self.db_path))
return
if parsed.path == "/api/history":
_json_response(
self,
HTTPStatus.OK,
{
"settings": load_change_log_rows(self.db_path, limit=25),
"approvals": load_approval_rows(self.db_path),
"locks": load_locks(self.db_path),
},
)
return
if parsed.path == "/api/export":
_text_response(
self,
HTTPStatus.OK,
json.dumps(export_payload(self.db_path), ensure_ascii=False, indent=2),
"application/json; charset=utf-8",
)
return
if parsed.path == "/favicon.ico":
_text_response(self, HTTPStatus.NO_CONTENT, "")
return
_json_response(self, HTTPStatus.NOT_FOUND, {"detail": "not found"})
def do_POST(self) -> None: # noqa: N802
parsed = urlparse(self.path)
try:
if parsed.path == "/api/bootstrap":
summary = import_seed_json(self.db_path, self.seed_json_path)
_json_response(self, HTTPStatus.OK, summary)
return
payload = _read_json_body(self)
if parsed.path == "/api/settings/save":
if is_locked(self.db_path, "settings"):
raise ValueError("settings are locked")
rows = payload.get("rows")
if not isinstance(rows, list):
raise ValueError("rows must be a list")
normalized_rows = []
for idx, row in enumerate(rows, start=1):
if not isinstance(row, dict):
continue
key = str(row.get("key") or "").strip()
if not key:
continue
normalized_rows.append(
{
"ordinal": idx,
"key": key,
"value": row.get("value", ""),
"note": str(row.get("note") or ""),
}
)
conflicts = lock_conflicts_for_rows(self.db_path, "settings", normalized_rows)
if conflicts:
refs = ", ".join(sorted({str(item.get("target_ref") or "") for item in conflicts if item.get("target_ref")}))
raise ValueError(f"settings lock conflict: {refs}")
with open_connection(self.db_path) as conn:
replace_settings(conn, normalized_rows)
_json_response(self, HTTPStatus.OK, summarize_workspace(self.db_path))
return
if parsed.path == "/api/account_snapshot/save":
if is_locked(self.db_path, "account_snapshot"):
raise ValueError("account_snapshot is locked")
rows = payload.get("rows")
if not isinstance(rows, list):
raise ValueError("rows must be a list")
normalized_rows: list[dict[str, Any]] = []
for idx, row in enumerate(rows, start=1):
if not isinstance(row, dict):
continue
candidate = {key: value for key, value in row.items() if not key.startswith("_")}
candidate["ordinal"] = idx
normalized_rows.append(candidate)
conflicts = lock_conflicts_for_rows(self.db_path, "account_snapshot", normalized_rows)
if conflicts:
refs = ", ".join(sorted({str(item.get("target_ref") or "") for item in conflicts if item.get("target_ref")}))
raise ValueError(f"account_snapshot lock conflict: {refs}")
with open_connection(self.db_path) as conn:
replace_account_snapshot(conn, normalized_rows)
_json_response(self, HTTPStatus.OK, summarize_workspace(self.db_path))
return
if parsed.path == "/api/account_snapshot/import_tsv":
if is_locked(self.db_path, "account_snapshot"):
raise ValueError("account_snapshot is locked")
tsv_text = str(payload.get("tsv") or "")
rows = parse_account_snapshot_tsv(tsv_text)
with open_connection(self.db_path) as conn:
replace_account_snapshot(conn, rows)
_json_response(self, HTTPStatus.OK, summarize_workspace(self.db_path))
return
if parsed.path == "/api/table/save":
table = str(payload.get("table") or "").strip()
rows = payload.get("rows")
if table not in EDITABLE_TABLES:
raise ValueError(f"table not editable: {table}")
if not isinstance(rows, list):
raise ValueError("rows must be a list")
db_path = _resolve_table_db(table, self.db_path)
if not db_path:
raise ValueError(f"database not found for table: {table}")
with open_connection(db_path) as conn:
conn.execute("BEGIN TRANSACTION")
try:
conn.execute(f"DELETE FROM {table}") # noqa: S608 - Whitelisted table name
if rows:
first_row = rows[0]
columns = [k for k in first_row.keys() if not k.startswith("_")]
if "rowid" in columns:
columns.remove("rowid")
if "_rowid" in columns:
columns.remove("_rowid")
placeholders = ", ".join(["?"] * len(columns))
col_list = ", ".join(columns)
insert_sql = f"INSERT INTO {table} ({col_list}) VALUES ({placeholders})" # noqa: S608 - Whitelisted table name
for row in rows:
values = [row.get(col) for col in columns]
conn.execute(insert_sql, values)
conn.commit()
except Exception as e:
conn.rollback()
raise e
_json_response(self, HTTPStatus.OK, {"status": "SUCCESS", "table": table, "row_count": len(rows)})
return
if parsed.path == "/api/approval_packet":
packet = payload.get("packet")
if not isinstance(packet, dict):
raise ValueError("packet must be an object")
artifacts = write_approval_packet_artifacts(packet)
response = {
"gate": "PASS",
"packet_path": artifacts["json_path"],
"md_path": artifacts["md_path"],
"formula_id": packet.get("formula_id", "SNAPSHOT_ADMIN_APPROVAL_PACKET_V1"),
}
_json_response(self, HTTPStatus.OK, response)
return
if parsed.path == "/api/approve":
domain = str(payload.get("domain") or "")
if domain not in {"settings", "account_snapshot"}:
raise ValueError("domain must be settings or account_snapshot")
target_ref = str(payload.get("target_ref") or "*")
with open_connection(self.db_path) as conn:
before = _approval_entry_from_conn(conn, domain, target_ref)
set_approval(conn, domain, "APPROVED", target_ref=target_ref, approved_by="ui", note="manual approval")
after = _approval_entry_from_conn(conn, domain, target_ref)
record_change_log(
conn,
domain=domain,
action="approve",
target_ref=target_ref,
before_json=before,
after_json=after,
actor="ui",
note="manual approval",
)
conn.commit()
_json_response(self, HTTPStatus.OK, {"domain": domain, "target_ref": target_ref, "status": "APPROVED"})
return
if parsed.path == "/api/lock":
domain = str(payload.get("domain") or "")
target_ref = str(payload.get("target_ref") or "*")
if domain not in {"settings", "account_snapshot"}:
raise ValueError("domain must be settings or account_snapshot")
with open_connection(self.db_path) as conn:
before = _lock_entry_from_conn(conn, domain, target_ref)
set_lock(conn, domain, target_ref, locked_by="ui", reason="manual lock")
after = _lock_entry_from_conn(conn, domain, target_ref)
record_change_log(
conn,
domain=domain,
action="lock",
target_ref=target_ref,
before_json=before,
after_json=after,
actor="ui",
note="manual lock",
)
conn.commit()
_json_response(self, HTTPStatus.OK, {"domain": domain, "target_ref": target_ref, "status": "LOCKED"})
return
if parsed.path == "/api/unlock":
domain = str(payload.get("domain") or "")
target_ref = str(payload.get("target_ref") or "*")
if domain not in {"settings", "account_snapshot"}:
raise ValueError("domain must be settings or account_snapshot")
with open_connection(self.db_path) as conn:
before = _lock_entry_from_conn(conn, domain, target_ref)
clear_lock(conn, domain, target_ref)
after = _lock_entry_from_conn(conn, domain, target_ref)
record_change_log(
conn,
domain=domain,
action="unlock",
target_ref=target_ref,
before_json=before,
after_json=after,
actor="ui",
note="manual unlock",
)
conn.commit()
_json_response(self, HTTPStatus.OK, {"domain": domain, "target_ref": target_ref, "status": "UNLOCKED"})
return
if parsed.path == "/api/undo":
domain = str(payload.get("domain") or "")
if domain not in {"settings", "account_snapshot"}:
raise ValueError("domain must be settings or account_snapshot")
if is_locked(self.db_path, domain):
raise ValueError(f"{domain} is locked")
with open_connection(self.db_path) as conn:
result = undo_last_change(conn, domain, actor="ui")
_json_response(self, HTTPStatus.OK, result if result else {"domain": domain, "status": "UNDONE"})
return
if parsed.path == "/api/autofix":
action_id = str(payload.get("action_id") or "")
if not action_id:
raise ValueError("action_id required")
with open_connection(self.db_path) as conn:
result = apply_safe_autofix_action(conn, action_id, actor="ui")
_json_response(self, HTTPStatus.OK, result)
return
_json_response(self, HTTPStatus.NOT_FOUND, {"detail": "not found"})
except Exception as exc: # noqa: BLE001
self._handle_exception(exc)
def serve(host: str, port: int, db_path: Path | str | None = None, seed_json_path: Path | str | None = None, bootstrap: bool = True) -> None:
db = normalize_db_path(db_path)
seed = Path(seed_json_path) if seed_json_path else DEFAULT_SEED_JSON
if bootstrap and seed.exists():
with open_connection(db) as conn:
from .snapshot_admin_store_v1 import ensure_schema
ensure_schema(conn)
if summarize_workspace(db)["settings_rows"] == 0 and summarize_workspace(db)["account_snapshot_rows"] == 0:
import_seed_json(db, seed)
SnapshotAdminHandler.db_path = db
SnapshotAdminHandler.seed_json_path = seed
server = ThreadingHTTPServer((host, port), SnapshotAdminHandler)
print(f"Snapshot Admin listening on http://{host}:{port}")
print(f"SQLite DB: {db}")
print(f"Seed JSON: {seed}")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
def main() -> int:
parser = argparse.ArgumentParser(description="Run the snapshot admin web server.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8787)
parser.add_argument("--db", type=Path, default=DEFAULT_DB)
parser.add_argument("--seed", type=Path, default=DEFAULT_SEED_JSON)
parser.add_argument("--no-bootstrap", action="store_true")
args = parser.parse_args()
serve(args.host, args.port, args.db, args.seed, bootstrap=not args.no_bootstrap)
return 0
if __name__ == "__main__":
raise SystemExit(main())