diff --git a/src/quant_engine/snapshot_admin_server_v1.py b/src/quant_engine/snapshot_admin_server_v1.py index 995b187..1bea589 100644 --- a/src/quant_engine/snapshot_admin_server_v1.py +++ b/src/quant_engine/snapshot_admin_server_v1.py @@ -42,11 +42,6 @@ QUALITATIVE_SELL_BROWSABLE_TABLES = ( EDITABLE_TABLES = { "settings", "account_snapshot", - "collection_runs", - "collection_snapshots", - "collection_source_errors", - "sell_strategy_results", - "satellite_recommendations", } @@ -83,10 +78,23 @@ def list_browsable_tables(workspace_db_path: Path) -> list[dict[str, Any]]: "row_count": row_count, "editable": table in EDITABLE_TABLES, }) + tables.sort(key=lambda item: ( + 0 if item["table"] == "account_snapshot" else 1 if item["table"] == "settings" else 2, + 0 if item["row_count"] else 1, + item["table"], + )) return tables -def fetch_table_rows(table: str, workspace_db_path: Path, *, limit: int = 50, offset: int = 0) -> dict[str, Any]: +def fetch_table_rows( + table: str, + workspace_db_path: Path, + *, + limit: int = 50, + offset: int = 0, + filter_text: str = "", + column_filters: dict[str, str] | None = None, +) -> dict[str, Any]: db_path = _resolve_table_db(table, workspace_db_path) if db_path is None: raise ValueError(f"unknown or non-browsable table: {table}") @@ -94,14 +102,27 @@ def fetch_table_rows(table: str, workspace_db_path: Path, *, limit: int = 50, of return {"table": table, "db": str(db_path), "columns": [], "rows": [], "total": 0, "limit": limit, "offset": offset, "editable": table in EDITABLE_TABLES} with sqlite3.connect(db_path) as conn: conn.row_factory = sqlite3.Row - total = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] # noqa: S608 - whitelisted table name - cursor = conn.execute( - f"SELECT rowid as _rowid, * FROM {table} ORDER BY rowid DESC LIMIT ? OFFSET ?", # noqa: S608 - whitelisted table name - (limit, offset), - ) - rows = [dict(row) for row in cursor.fetchall()] + cursor = conn.execute(f"SELECT rowid as _rowid, * FROM {table} ORDER BY rowid DESC",) # noqa: S608 - whitelisted table name + all_rows = [dict(row) for row in cursor.fetchall()] columns = [description[0] for description in cursor.description] if cursor.description else [] - return {"table": table, "db": str(db_path), "columns": columns, "rows": rows, "total": total, "limit": limit, "offset": offset, "editable": table in EDITABLE_TABLES} + cleaned_filter_text = str(filter_text or "").strip().lower() + normalized_column_filters = {str(key): str(value).strip().lower() for key, value in (column_filters or {}).items() if str(value).strip()} + + def _match_row(row: dict[str, Any]) -> bool: + display_row = {k: v for k, v in row.items() if not str(k).startswith("_")} + haystack = json.dumps(display_row, ensure_ascii=False, default=str).lower() + if cleaned_filter_text and cleaned_filter_text not in haystack: + return False + for key, needle in normalized_column_filters.items(): + cell = str(display_row.get(key, "") or "").lower() + if needle not in cell: + return False + return True + + filtered_rows = [row for row in all_rows if _match_row(row)] + total = len(filtered_rows) + rows = filtered_rows[offset: offset + limit] + return {"table": table, "db": str(db_path), "columns": columns, "rows": rows, "total": total, "limit": limit, "offset": offset, "editable": table in EDITABLE_TABLES, "filter_text": cleaned_filter_text, "column_filters": normalized_column_filters} def fetch_domain_rows(domain: str, workspace_db_path: Path) -> dict[str, Any]: @@ -503,6 +524,9 @@ def render_index_html() -> str: padding: 0; vertical-align: top; font-size: 12px; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; } .sheet th { position: sticky; @@ -537,6 +561,10 @@ def render_index_html() -> str: z-index: 1; } .sheet th.rownum { z-index: 3; } + .sheet th.col-wide, .sheet td.col-wide { min-width: 180px; } + .sheet th.col-xwide, .sheet td.col-xwide { min-width: 260px; } + .sheet th.col-narrow, .sheet td.col-narrow { min-width: 88px; } + .sheet th.col-micro, .sheet td.col-micro { min-width: 68px; } .note-box { margin-top: 12px; display: grid; @@ -578,6 +606,42 @@ def render_index_html() -> str: flex-wrap: wrap; align-items: center; } + .top-banner { + display: grid; + gap: 8px; + margin-top: 12px; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid rgba(56, 189, 248, .28); + background: linear-gradient(135deg, rgba(14, 165, 233, .18), rgba(15, 23, 42, .92)); + box-shadow: 0 16px 50px rgba(15, 23, 42, .35); + } + .top-banner-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + } + .top-banner .stat { + padding: 10px 12px; + border-radius: 12px; + background: rgba(2, 6, 23, .45); + border: 1px solid rgba(255,255,255,.08); + } + .top-banner .stat .label { + display: block; + font-size: 11px; + color: var(--muted); + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: .04em; + } + .top-banner .stat strong { + display: block; + color: var(--text); + font-size: 14px; + line-height: 1.35; + word-break: break-word; + } .two-col { display: grid; grid-template-columns: 1fr 1fr; @@ -589,12 +653,111 @@ def render_index_html() -> str: flex-wrap: wrap; } .sheet tr.selected td { - background: rgba(56, 189, 248, .10); + background: rgba(14, 165, 233, .20); + box-shadow: inset 0 1px 0 rgba(186, 230, 253, .24), inset 0 -1px 0 rgba(186, 230, 253, .14); } .sheet tr.selected td.rownum { - background: rgba(56, 189, 248, .18); + background: rgba(14, 165, 233, .36); color: #e0f2fe; } + .sheet tr.selected td[contenteditable="true"] { + background: rgba(34, 211, 238, .14); + outline: 1px solid rgba(103, 232, 249, .30); + outline-offset: -1px; + font-weight: 600; + } + .sheet tr.selected td.selected-field { + background: rgba(251, 191, 36, .12); + outline-color: rgba(251, 191, 36, .42); + } + .sheet tr.selected td.selected-field:focus { + background: rgba(251, 191, 36, .16); + outline-color: rgba(251, 191, 36, .78); + box-shadow: 0 0 0 2px rgba(251, 191, 36, .18); + } + .sheet tr.selected td[contenteditable="true"]:focus { + background: rgba(103, 232, 249, .18); + outline: 2px solid rgba(125, 211, 252, .78); + outline-offset: -2px; + box-shadow: 0 0 0 2px rgba(14, 165, 233, .16); + } + .table-banner { + display: grid; + gap: 8px; + margin-bottom: 12px; + padding: 12px 14px; + border-radius: 14px; + border: 1px solid rgba(148, 163, 184, .18); + background: rgba(15, 23, 42, .55); + } + .table-banner .meta-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + } + .table-banner .muted { + color: #cbd5e1; + } + .panel.snapshot-panel { + border-color: rgba(34, 197, 94, .22); + box-shadow: 0 0 0 1px rgba(34, 197, 94, .06) inset, 0 18px 50px rgba(15, 23, 42, .18); + } + .panel.snapshot-panel .panel-head { + background: linear-gradient(90deg, rgba(34, 197, 94, .12), rgba(15, 23, 42, 0)); + border-bottom: 1px solid rgba(34, 197, 94, .16); + } + .panel.snapshot-panel .status { + border-left: 3px solid rgba(34, 197, 94, .55); + background: rgba(34, 197, 94, .08); + } + .snapshot-callout { + display: grid; + gap: 8px; + margin-top: 10px; + padding: 12px; + border-radius: 14px; + border: 1px solid rgba(34, 197, 94, .18); + background: rgba(3, 7, 18, .42); + } + .snapshot-callout strong { color: #dcfce7; } + .snapshot-callout .muted { color: #bbf7d0; } + .snapshot-detail-card { + display: grid; + gap: 8px; + padding: 12px; + border-radius: 14px; + border: 1px solid rgba(59, 130, 246, .16); + background: linear-gradient(180deg, rgba(15, 23, 42, .72), rgba(3, 7, 18, .48)); + } + .snapshot-detail-card .meta-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + } + .snapshot-detail-card .field-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + } + .snapshot-detail-card .field { + padding: 8px 10px; + border-radius: 12px; + background: rgba(255,255,255,.04); + border: 1px solid rgba(255,255,255,.08); + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; + } + .snapshot-detail-card .field strong { + display:block; + color: #dbeafe; + margin-bottom: 4px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: .04em; + } .diff-list { display: grid; gap: 8px; @@ -788,10 +951,31 @@ def render_index_html() -> str: Open table browser
+
+
+
+ Approval + Loading... +
+
+ Lock + Loading... +
+
+ Selection + No row selected. +
+
+ Diff + Pending diff loading... +
+
+
Snapshot approval state and lock state are pinned here for immediate review.
+
-
+

Workspace

@@ -935,7 +1119,7 @@ def render_index_html() -> str:
-

Settings

+

Settings 0 rows

@@ -968,7 +1152,7 @@ def render_index_html() -> str:
-

Account Snapshot

+

Account Snapshot 0 rows

@@ -979,6 +1163,10 @@ def render_index_html() -> str: Canonical column order follows spec/15_account_snapshot_contract.yaml
+
+ Account snapshot editing surface +
This panel is intentionally separated from settings so row selection, field edits, and save approval stay visually dominant.
+
- -
+
+
+ + + +
+ + @@ -2681,9 +2954,28 @@ def render_tables_html() -> str:
+
+
+
+ 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 ready + 0 rows + filter=none + page=0 +
+
If a table shows "no rows", the current filter or selected table has no visible records.
+
- - +
+ + + +
@@ -2693,7 +2985,7 @@ def render_tables_html() -> str: