From af9995e053fca10d0d1d43b9561db2b7d347fe39 Mon Sep 17 00:00:00 2001 From: ChromaBeast Date: Fri, 13 Nov 2026 13:12:47 +0530 Subject: [PATCH 1/8] feat(studio): align shell and pages with the Figma design - Sidebar: 232px width, 10px item padding and gap, panel background, solid active item in dark mode. - Add StatCard/StatGrid and use them for summary tiles on Overview, Indexes and Backups, all fed by real engine values. - API Keys: four role cards on top, explanation note below the list. - Settings: left sub-menu (General, Storage, Networking, Security, Limits); card definitions moved to settings/SettingsCards.tsx. - Logs: Time/Level/Event header row and Export CSV (toLogCsv, tested). - docs: add studio-figma-gap-plan.md with the phased plan for the Figma features the engine does not back yet. --- docs/studio-figma-gap-plan.md | 85 +++++++ studio/src/app/globals.css | 2 +- studio/src/components/OverviewView.tsx | 12 +- studio/src/components/StatCard.tsx | 42 ++++ studio/src/components/StudioSidebar.tsx | 6 +- studio/src/components/keys/RolesReference.tsx | 56 ++--- .../src/components/settings/SettingsCards.tsx | 146 +++++++++++ studio/src/components/views/BackupsView.tsx | 13 + studio/src/components/views/IndexesView.tsx | 8 + studio/src/components/views/KeysView.tsx | 5 +- studio/src/components/views/LogsView.tsx | 26 +- studio/src/components/views/SettingsView.tsx | 228 +++++------------- studio/src/utils/logs.test.ts | 14 +- studio/src/utils/logs.ts | 7 + 14 files changed, 441 insertions(+), 209 deletions(-) create mode 100644 docs/studio-figma-gap-plan.md create mode 100644 studio/src/components/StatCard.tsx create mode 100644 studio/src/components/settings/SettingsCards.tsx diff --git a/docs/studio-figma-gap-plan.md b/docs/studio-figma-gap-plan.md new file mode 100644 index 0000000..7d27304 --- /dev/null +++ b/docs/studio-figma-gap-plan.md @@ -0,0 +1,85 @@ +# Studio: closing the gap to the Figma design + +Status: plan, nothing here is shipped. Written 2026-11-13 after comparing Figma file `rE4dmZFSRUzMElTXIKtnhG` (frames 1-11) with the running Studio. Complements [studio.md](studio.md) and [upgrade-spec.md](upgrade-spec.md). + +Principle: Studio only shows what the server reports. A Figma control ships only when the engine or API behind it exists. Until then the page keeps its current honest state (read-only, or a "Planned" label). No dead buttons. + +## What the engine exposes today (checked in code) + +| Area | Reality | Where | +|---|---|---| +| Keys | uint64 keys, 64-bit prefix partitions, no collections | `api/proto/beastdb.proto` | +| gRPC | Get, Put, Delete, Scan, BatchWrite; replication StreamWAL, Ack | proto | +| Indexes | One primary B+ tree plus one fixed in-memory text index | `internal/index/` | +| Tokens | Roles and `expires_at` exist on the server; no last-used | `internal/web/auth/token_store.go`, `handler/token_api.go` | +| Logs | Ring buffer of `{seq, time, level, message}`; no source field | `internal/logbuf/logbuf.go` | +| Replication | Leader lists replicas with connected and acked LSN; followers are started by flags; no promote | `internal/replication/leader_replicas.go` | +| Settings | Read-only startup config | `handler/settings_api.go` | +| Metrics | Prometheus `/metrics`; Studio keeps ~5 s polling history in the browser only | `internal/metrics/`, `useMetricsHistory.ts` | +| Backups | Offline `beastctl backup/verify/restore`; server only runs checkpoints | `cmd/beastctl`, `handler/ops.go` | + +## Phase 0: UI only, no engine work (done 2026-11-13) + +Shipped: Settings sub-menu, API Keys role cards, Logs table header and CSV export. Already existed before this plan: the key expiry field and Expires column, Table Editor Prev/Next, and the Split view with a record detail pane. Not done from the table below: page-top header buttons beyond those that already exist. + +Everything maps to data that already exists. + +| Item | Figma frame | Work | +|---|---|---| +| Settings sub-menu (General / Security / Networking / Node) | 10 | Split the existing read-only cards into sections with a left menu. No Team or Danger zone. | +| API Keys: four role cards on top | 7 | Re-skin `RolesReference` as a 4-up card row. | +| API Keys: expiry field and Expires column | 7 | Send `expires_in_seconds` on create (backend done, spec calls this out); show `expires_at` in the table. | +| Table Editor: record side panel, Prev/Next | 1 | Existing `RecordSheet` content as a persistent right panel; page-based paging on top of the current load-more. | +| Logs: table layout (Time / Level / Event) and client-side Export CSV | 9 | Reuse entries; drop the Source column. | +| Header actions that already work | all | Refresh, Checkpoint now, Download logs placed top-right like Figma. | +| Page titles | all | Keep honest titles ("Logs", not "Logs & Audit"). | + +Done when: each page matches its frame's layout using real data only, and `go build ./... && go vet ./... && go test ./... -race` plus `bun test` pass. + +## Phase 1: small server additions (2-3 weeks) + +| Item | Server work | Studio work | +|---|---|---| +| Token last-used | Update a `last_used` timestamp in the gRPC auth path (throttled, e.g. once a minute per token, so reads do not write). | "Last used" column. | +| Metrics history | Server-side ring of sampled gauges (WAL size, dirty pages, ops/s, replica lag) exposed at `/api/metrics/history?window=`. In-memory only, cleared on restart; say so in the UI. | Time-range picker; the Figma charts become real. Overview throughput chart reuses it. | +| Structured log source | Add `source` (engine, auth, replication, studio, grpc) to `logbuf.Entry`; tag existing call sites. | Source column and filter. | +| Log time-range filter | Query params on `/api/logs`. | Filter dropdown. | + +Risks: write amplification from last-used (mitigate with throttling and no WAL entry); history memory (bounded ring). + +## Phase 2: medium features, each needs a short design note (1-2 months) + +1. **Server-managed backups.** Move `beastctl` backup, verify and restore logic into an internal package. Add admin endpoints to create a bundle into a configured backup directory, list manifests with size, time and verify status, verify, and stream a download. Scheduling comes last (simple interval, retention count). Restore stays an offline `beastctl` step until restore-to-new-database is designed; the Figma "Restore" button is shown as copy-the-command, not a live action. Needs: concurrent-safe backup while serving (today beastctl assumes a stopped node), so the first design question is a consistent snapshot using checkpoint plus WAL position. +2. **Runtime settings.** Pick the subset that is safe to change live (checkpoint threshold, log level, rate limits), persist to a settings file, apply without restart, audit each change. Everything else stays startup-only. TLS and mTLS toggles are not live-changeable and stay read-only. +3. **Audit log.** Separate append-only log of admin actions (login, token create/revoke, delete user, checkpoint, settings change) with actor and timestamp. Tamper-evident chaining is already on the upgrade-spec "next wave"; start with plain append-only and a hash chain. Studio gets the Logs & Audit tab and the "Audit" level the Figma shows. + +## Phase 3: large, engine-level (quarters, each starts with an ADR) + +| Feature | Blocker | First step | +|---|---|---| +| Collections / namespaces | Keys are uint64; no string keys or namespace concept. Upgrade spec lists string keys plus namespaces as not started. | ADR in `docs/adr/`: key encoding, migration of existing data, partition interaction. | +| Visual query builder + generated beastctl/SDK command | Needs collections and field-level filters; engine only has Get, Scan, text search. | Define filter push-down in Scan after collections land. | +| Secondary indexes (list, create, slow-query suggestion) | Only one fixed text index. Needs index definitions, persistence, build-in-background, and query stats for suggestions. | ADR on index catalog; depends on collections. | +| Replication management (add replica, promote) | Followers start by flags; no membership, no leader election, no fencing. Promotion without fencing risks split-brain. | ADR on membership and epoch-based promotion; Studio shows the real topology read-only first (leader plus connected replicas diagram with real LSN and lag, which Phase 0 can already approximate). | +| Encryption at rest | Not built (data file, WAL, backups, spill file). | ADR on key management and on-disk format versioning. | +| Two-factor for admins | Studio auth is a signed cookie with local users. | Design TOTP storage and recovery; depends on audit log. | +| Restore to new database | Needs snapshot-consistent backups (Phase 2) and a swap-in procedure. | After backups design. | + +## Order and dependencies + +``` +Phase 0 ──► Phase 1 ──► Phase 2.1 backups ─────► restore-to-new-db + │ Phase 2.3 audit log ──► 2FA + └──► Phase 2.2 runtime settings +Phase 3 collections ──► query builder, secondary indexes +Phase 3 replication membership (independent ADR) +Phase 3 encryption at rest (independent ADR) +``` + +## Rules while this is in flight + +- Keep "Planned" labels and read-only states wherever a feature is not shipped; remove them only when the endpoint exists. +- Do not hard-code downstream-application schema in Studio (AGENTS.md); partition labels stay in `partitions.json`. +- No performance or feature-parity claims versus other databases without a benchmark. +- Each phase ends with a Figma-vs-running-Studio screenshot comparison at 1440x900 for the pages it touched. +- Each file stays one responsibility, around 400 lines (split `SettingsView.tsx` while doing the sub-menu). diff --git a/studio/src/app/globals.css b/studio/src/app/globals.css index 5ede487..d7f57b2 100644 --- a/studio/src/app/globals.css +++ b/studio/src/app/globals.css @@ -49,7 +49,7 @@ --border: 120 12% 18%; --input: 120 12% 18%; --ring: 80 89% 53%; - --sidebar: 120 11% 8%; + --sidebar: 120 11% 9%; --code: 120 14% 5%; } * { diff --git a/studio/src/components/OverviewView.tsx b/studio/src/components/OverviewView.tsx index d5b763d..f6a7cfc 100644 --- a/studio/src/components/OverviewView.tsx +++ b/studio/src/components/OverviewView.tsx @@ -7,6 +7,7 @@ import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { BufferPoolMap, WalHead } from "./EnginePanel"; import { PartitionChart } from "./PartitionChart"; +import { StatCard, StatGrid } from "./StatCard"; import { getRegisteredPartitions } from "../utils/key-decoder"; interface Props { @@ -31,6 +32,7 @@ export function OverviewView({ stats, statsError, recordsError, user, loaded, ha : [] ).filter((d) => d.count > 0); + const totalRecords = chartData.reduce((n, d) => n + d.count, 0); const isHealthy = Boolean(stats); const dirtyPages = stats?.storage?.dirtyPages ?? 0; const poolSize = stats?.storage?.poolSize ?? 64; @@ -66,13 +68,19 @@ export function OverviewView({ stats, statsError, recordsError, user, loaded, ha )} + + + + + + +
-
- +
diff --git a/studio/src/components/StatCard.tsx b/studio/src/components/StatCard.tsx new file mode 100644 index 0000000..74144b1 --- /dev/null +++ b/studio/src/components/StatCard.tsx @@ -0,0 +1,42 @@ +import type { ReactNode } from "react"; +import { Card } from "./ui/card"; +import { cn } from "@/lib/utils"; + +const HINT_TONES = { + muted: "text-muted-foreground", + primary: "text-primary", + success: "text-success", + warning: "text-warning", + info: "text-info", +} as const; + +export type StatTone = keyof typeof HINT_TONES; + +interface StatCardProps { + label: string; + value: string; + hint?: ReactNode; + tone?: StatTone; +} + +/** Figma "Stat card": muted caption, large number, optional coloured hint line. */ +export function StatCard({ label, value, hint, tone = "muted" }: StatCardProps) { + return ( + +

{label}

+

+ {value} +

+ {hint &&

{hint}

} +
+ ); +} + +/** Responsive row of stat cards (2 up on small screens, `cols` up on wide ones). */ +export function StatGrid({ cols = 4, children }: { cols?: 3 | 4; children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/studio/src/components/StudioSidebar.tsx b/studio/src/components/StudioSidebar.tsx index a77df7a..00eec8b 100644 --- a/studio/src/components/StudioSidebar.tsx +++ b/studio/src/components/StudioSidebar.tsx @@ -48,13 +48,13 @@ const PAGES: { id: StudioView; label: string; icon: LucideIcon }[] = [ ]; const itemBase = - "flex w-full shrink-0 items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors"; + "flex w-full shrink-0 items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm transition-colors"; export function StudioSidebar({ view, setView, user, busy, onRefresh }: Props) { const theme = useTheme(); return (