Skip to content

Commit d6e8ef0

Browse files
paddymulclaude
andcommitted
feat(app): add read-only catalog customizations page
List the project-wide Buckaroo customizations — summary stats, styling classes, and post-processing functions — for a catalog, each expandable to inspect its source. Adds GET /{project}/api/customizations, which returns the three registries via the existing tallyman_core list_stats / list_display_klasses / list_post_processings helpers (the same ones the catalog_list_* MCP tools use), including _disabled/ items flagged accordingly. The React SPA gets a CustomizationsPage with a nav link; it refetches on SSE version bumps so edits made via the MCP tools show up live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b4e75ac commit d6e8ef0

7 files changed

Lines changed: 140 additions & 0 deletions

File tree

packages/app/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { LineagePage } from "./pages/LineagePage";
99
import { LineageEntryPage } from "./pages/LineageEntryPage";
1010
import { DiffPage } from "./pages/DiffPage";
1111
import { CachePage } from "./pages/CachePage";
12+
import { CustomizationsPage } from "./pages/CustomizationsPage";
1213
import { EmptyStatePage } from "./pages/EmptyStatePage";
1314
import { ProjectsPage } from "./pages/ProjectsPage";
1415
import { api } from "./api";
@@ -55,6 +56,7 @@ export default function App() {
5556
<Route path="lineage" element={<LineagePage />} />
5657
<Route path="lineage/:hash" element={<LineageEntryPage />} />
5758
<Route path="cache" element={<CachePage />} />
59+
<Route path="customizations" element={<CustomizationsPage />} />
5860
<Route path="diff/:alias" element={<DiffPage />} />
5961
<Route path="diff/:alias/:va/:vb" element={<DiffPage />} />
6062
<Route path="*" element={<Navigate to="catalog" replace />} />

packages/app/src/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
LineageLayout,
88
DiffData,
99
ResultCache,
10+
Customizations,
1011
Projects,
1112
} from "./types";
1213

@@ -132,6 +133,9 @@ export const api = {
132133
resultCache: (project: string): Promise<ResultCache> =>
133134
get(`/${project}/api/result_cache`),
134135

136+
customizations: (project: string): Promise<Customizations> =>
137+
get(`/${project}/api/customizations`),
138+
135139
deleteResultCache: (project: string, hash: string): Promise<{ ok: boolean; hash: string }> =>
136140
fetch(`/${project}/api/result_cache/${hash}`, { method: "DELETE" }).then((r) => {
137141
if (!r.ok) throw new Error(`delete failed: HTTP ${r.status}`);

packages/app/src/components/Header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function Header() {
8181
<Link to={`/${project}/notebook`}>notebook</Link>
8282
<Link to={`/${project}/lineage`}>lineage</Link>
8383
<Link to={`/${project}/cache`}>cache</Link>
84+
<Link to={`/${project}/customizations`}>customizations</Link>
8485
<Link to="/projects">projects</Link>
8586
</nav>
8687
)}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useEffect, useState } from "react";
2+
import { useParams } from "react-router-dom";
3+
import { api } from "../api";
4+
import { useSSE } from "../SSEContext";
5+
import type { CustomizationItem, Customizations } from "../types";
6+
7+
const SECTIONS: { key: keyof Omit<Customizations, "project">; title: string; blurb: string }[] = [
8+
{
9+
key: "summary_stats",
10+
title: "Summary stats",
11+
blurb: "Per-column statistics computed by Buckaroo. def compute(col) -> ibis.Expr",
12+
},
13+
{
14+
key: "display_klasses",
15+
title: "Styling classes",
16+
blurb: "ColAnalysis subclasses that control table rendering and pinned rows.",
17+
},
18+
{
19+
key: "post_processings",
20+
title: "Post-processing",
21+
blurb: "Expression transformers offered in Buckaroo's post-processing dropdown. def process(expr) -> ibis.Expr | DataFrame",
22+
},
23+
];
24+
25+
function Section({ title, blurb, items }: { title: string; blurb: string; items: CustomizationItem[] }) {
26+
const active = items.filter((i) => !i.disabled).length;
27+
return (
28+
<div className="cust-section">
29+
<div className="cust-section-head">
30+
<strong>{title}</strong>
31+
<span className="meta">
32+
{items.length === 0
33+
? "none"
34+
: `${active} active${items.length - active ? ` · ${items.length - active} disabled` : ""}`}
35+
</span>
36+
</div>
37+
<span className="meta cache-note">{blurb}</span>
38+
39+
{items.length === 0 ? (
40+
<div className="nb-empty">none defined</div>
41+
) : (
42+
items.map((item) => (
43+
<details key={item.path} className="cust-item">
44+
<summary>
45+
<span className={`cust-name${item.disabled ? " muted" : ""}`}>{item.name}</span>
46+
{item.disabled && <span className="vchip">disabled</span>}
47+
<span className="meta cust-path" title={item.path}>
48+
{item.path}
49+
</span>
50+
</summary>
51+
<pre className="code">{item.source}</pre>
52+
</details>
53+
))
54+
)}
55+
</div>
56+
);
57+
}
58+
59+
export function CustomizationsPage() {
60+
const { project } = useParams<{ project: string }>();
61+
const { version } = useSSE();
62+
const [data, setData] = useState<Customizations | null>(null);
63+
64+
useEffect(() => {
65+
if (!project) return;
66+
api.customizations(project).then(setData).catch(() => {});
67+
}, [project, version]);
68+
69+
return (
70+
<main style={{ overflow: "auto" }}>
71+
<div className="cache-pane">
72+
<div className="cache-summary">
73+
<strong>Catalog customizations</strong>
74+
<span className="meta cache-note">
75+
Project-wide Buckaroo customizations. They apply to every entry in the catalog and are loaded
76+
into each session. Read-only here — edit via the MCP tools.
77+
</span>
78+
</div>
79+
80+
{SECTIONS.map((s) => (
81+
<Section key={s.key} title={s.title} blurb={s.blurb} items={data ? data[s.key] : []} />
82+
))}
83+
</div>
84+
</main>
85+
);
86+
}

packages/app/src/styles.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,22 @@ table.cache-table .muted { color: var(--muted); }
514514
.del-btn:hover { background: #2a1b1b; color: #ffb4b4; border-color: #7a4a4a; }
515515
.del-btn:disabled { opacity: 0.5; cursor: default; }
516516

517+
/* catalog customizations page */
518+
.cust-section { margin-bottom: 22px; }
519+
.cust-section-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 2px; }
520+
.cust-section-head strong { font-size: 14px; }
521+
.cust-section .cache-note { display: block; margin-bottom: 8px; }
522+
.cust-item { border: 1px solid var(--border); border-radius: 5px; margin-bottom: 6px; background: var(--panel, transparent); }
523+
.cust-item > summary { display: flex; align-items: baseline; gap: 10px; padding: 6px 10px; cursor: pointer; list-style: none; }
524+
.cust-item > summary::-webkit-details-marker { display: none; }
525+
.cust-item > summary::before { content: "▸"; color: var(--muted); font-size: 11px; }
526+
.cust-item[open] > summary::before { content: "▾"; }
527+
.cust-item .cust-name { font-weight: 600; font-family: ui-monospace, monospace; }
528+
.cust-item .cust-name.muted { color: var(--muted); font-weight: 400; text-decoration: line-through; }
529+
.cust-item .cust-path { margin-left: auto; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 50%; }
530+
.cust-item .vchip { background: var(--accent-soft); color: var(--accent); padding: 1px 6px; border-radius: 10px; font-size: 11px; }
531+
.cust-item > pre.code { margin: 0; border-top: 1px solid var(--border); border-radius: 0 0 5px 5px; }
532+
517533
/* projects management page */
518534
.projects-page { max-width: 600px; margin: 32px auto; padding: 16px; }
519535
.projects-page h2 { margin-top: 0; margin-bottom: 16px; }

packages/app/src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ export interface ResultCache {
180180
total_formatted: string;
181181
}
182182

183+
export interface CustomizationItem {
184+
name: string;
185+
path: string;
186+
source: string;
187+
disabled: boolean;
188+
}
189+
190+
export interface Customizations {
191+
project: string;
192+
summary_stats: CustomizationItem[];
193+
display_klasses: CustomizationItem[];
194+
post_processings: CustomizationItem[];
195+
}
196+
183197
export interface Projects {
184198
active: string | null;
185199
available: string[];

src/tallyman_companion/app.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,23 @@ def api_result_cache(project: str):
10071007
"total_formatted": _fmt_bytes(total),
10081008
}
10091009

1010+
@app.get("/{project}/api/customizations")
1011+
def api_customizations(project: str):
1012+
"""Project-wide buckaroo customizations: summary stats, display
1013+
klasses and post-processing functions, each with its source. These
1014+
are global to the catalog (one set per project), not per-entry."""
1015+
project = _validate_project(project)
1016+
from tallyman_core.display_klasses import list_display_klasses # noqa: PLC0415
1017+
from tallyman_core.post_processing import list_post_processings # noqa: PLC0415
1018+
from tallyman_core.summary_stats import list_stats # noqa: PLC0415
1019+
1020+
return {
1021+
"project": project,
1022+
"summary_stats": list_stats(project),
1023+
"display_klasses": list_display_klasses(project),
1024+
"post_processings": list_post_processings(project),
1025+
}
1026+
10101027
@app.delete("/{project}/api/result_cache/{content_hash}")
10111028
async def api_delete_result_cache(project: str, content_hash: str):
10121029
project = _validate_project(project)

0 commit comments

Comments
 (0)