v0.0.79 — AI-modify polish, undo, per-tenant palette, dbt catalog
Released: 2026-05-18. Thirteen commits.
Big bug-fix-and-polish release. Closes the v0.0.78 Slack bug list (Ask-AI-Modify reliability + version history + donut readability + one-click Undo), lands the real dbt manifest adapter + Migration Cockpit page on the Catalog track, and ships the async runner for agent reviews.
Ask AI Modify, History, Search, Pie — Slack bug list
Five buckets of fixes around AI dashboard editing.
Bucket A — Ask-AI-Modify reliability
- Pre-mutation snapshot in every AI action (create / modify / clear_all / set_layout / set_period / filter ops) so the first-ever AI turn on a fresh dashboard is undoable. Root cause for "AI wiped my dashboard and there's nothing to revert to".
- SQL window 200 → 800 chars (4000 for selected card). The 200-char truncation dropped FROM/JOIN context, which is why MODIFY on a real card fell back to sample/mock data.
- Recent revisions block injected into the AI context — last 10 rows with
revision_number+ age +change_noteso the LLM can answer "what changed?" / "go back". - New chat action
restore_revision— natural-language "undo" / "revert" routes to the same atomic snapshot+replay primitive the History drawer uses, with a pre-restore snapshot so even AI picking the wrong revision is recoverable.
Bucket B — Version history revert
Frontend silent error swallowing fixed on restoreRevision / loadRevisions / previewRevision / takeSnapshot. The "version history not working when we try to claim it" was a 403/500 silently eaten by catch { /* ignore */ }. The AI chat result handler reloads the dashboard on every mutation action, not just create / modify.
Bucket C — Dataset AI Search
- Shared datasets now visible:
_DATASETS_QUERYLEFT JOINsdataset_sharesontarget_project_id = caller_pid. - Cross-project for org admins:
is_org_adminflag threaded through router →discover_datasets; admin scope lifts the project boundary within the org. Cobuild + chat-agent tool path stay strict by default. - Phrase-match boost (+4.0) when the full intent appears verbatim in
name/friendly_name/description. Catches long NL queries that pure token overlap was burying. - Result rows carry
project_id/project_name/is_sharedso the FE renders a "from Project X" pill for shared / foreign results.
Bucket D — Donut / Pie readability
Unified pie / donut renderer. Labels now show for every slice ≥ label_min_percent (default 4%) instead of being gated on data.length <= 6 (which left every realistic chart with no labels). New card_config.label_mode: percent | value | name_percent | none. White 2px stroke between slices for visible separation without hover.
Bucket E — One-click Undo
POST /dashboards/{id}/undo. Server resolves target: prefers the most recent "Before: …" snapshot, falls back to (latest - 1) for manual edits. 409 when there's nothing to undo. Toolbar Undo button (next to History) surfaces the undone change_note in the AI response panel.
Revisions — diff drawer + selected-card Undo + AI-search rerank
Three follow-ons to the bug-fix batch.
Revision diff endpoint + drawer
GET /dashboards/{id}/revisions/{a}/diff/{b} returns card-level added / removed / changed plus dashboard-level title / description / config diffs. Cards matched by card_id (fallback: lowercased title) so legacy snapshots still align. Field-level diffs limited to a curated whitelist to avoid noisy updated_at churn.
The History drawer's preview pane gets a Diff vs vN button that fetches the comparison against the revision immediately before the selected one. Inline +/−/~ summary with per-card field hints, capped to 5 fields per card for legibility.
Selected-card aware Undo (partial restore)
POST /dashboards/{id}/undo-card { card_id, revision_number? }. Resolves target revision the same way /undo does. Three branches: card added → DELETE, card removed → INSERT (preserving card_id), card changed → UPDATE every restorable field. Pre/post-snapshot bracket the operation so the partial undo is itself undoable.
The toolbar Undo button now reads Undo card when a card is selected and routes to /undo-card; otherwise it stays on /undo for full-dashboard revert.
AI-search LLM rerank
Optional stage-3 rerank using gpt-4o-mini. Token + phrase ranking stays as the deterministic base; when HONEYFRAME_AI_SEARCH_RERANK is enabled (default on) and intent has ≥3 tokens, the top _RERANK_TOP_K candidates go through one LLM call that re-orders by semantic relevance. Closes the "synonyms / NL intent gets buried" gap that phrase-match couldn't cover. Best-effort: any LLM / network failure leaves the token ranking intact. Model overridable via HONEYFRAME_AI_SEARCH_RERANK_MODEL (default openai:gpt-4o-mini).
Per-tenant branding palette
Pre-migration only primary_color was per-org — hover / accent_bg / page_bg / border_color / secondary_color all fell back to a global default hard-coded to honeyframe amber. That made full-tenant rebrands impossible without code edits.
Migration 2026-05-18_branding_palette.sql adds 5 palette columns to honeyframe.organizations (all nullable so existing tenants are unchanged) and can seed a tenant's row with a palette extracted from that tenant's visual system. Idempotent — only fills NULLs, so a future override doesn't conflict.
branding.GET ?slug=… overlays the new columns onto DEFAULTS with the same null-falls-through-to-default rule as the existing fields. UndefinedColumn from pre-migration tenants is caught and the SELECT retries against the legacy schema — zero-impact rollout.
Catalog — real dbt manifest adapter + Migration Cockpit page
DbtAdapter.list_assets() now parses manifest.json and emits one CatalogAsset per model / seed / snapshot / source. Tests are skipped (not migratable feeds). get_lineage() walks parent_map + child_map to surface upstream + downstream edges. health() reports asset_count + ISO last_sync_at. Manifest cache invalidates on mtime so long-running processes don't need a restart after dbt run. FQN convention: dbt.<project>.<schema>.<asset>.
bootstrap.py auto-configures a honeyframe-dbt instance at startup if a honeyframe manifest is found at HONEYFRAME_DBT_MANIFEST, paas/dbt/target/manifest.json, or /opt/honeyframe/paas/dbt/target/.
New MigrationCockpitPage at /catalog (project + standalone routes) + Data nav entry. Surfaces the 5 backend endpoints already scaffolded in v0.0.78: GET /sources, GET /adapter-types, GET /feeds, POST /score, POST /migrate (drawer with dbt path + SQL preview + AI explanation). Backend stubs still return empty so the page renders empty states that describe Phase 1 vs Phase 2 — honest about what's wired and what's coming.
Agent reviews — async runner + cancel
POST /agent-reviews/{rid}/runs now returns {run_id, status: "queued"} with HTTP 201 immediately instead of blocking until the loop finishes. run_all_tests splits into _prepare_run + _execute_run_loop; start_run() schedules execution via asyncio.create_task on a fresh AsyncSession. Frontend handleRunAll polls GET /runs/{id} every 2s until status is terminal (30-min ceiling).
POST /agent-reviews/{rid}/runs/{run_id}/cancel + a Cancel button next to Run All appears while the poll loop is active. Queued runs flip to status='cancelled' directly on the endpoint; running runs set a process-local cancel flag that _execute_run_loop checks between executions; the loop writes status='cancelled' and finishes the partial counts.
Webapp — header chips for freshness + alert
Optional header_meta / header_alert / header_alert_severity on AppPageSpec — render next to the page title (right side) as pills (e.g. a data-freshness stamp + a critical-branches alert).
Polish + small fixes
- KPI hero fills card vertically + bigger value. Used
flex-colwithoutjustify-, so content clustered at the top of the row leaving dead space when the grid row was sized for chart cards.justify-betweenso caption pins top, value sits middle, meta row anchors bottom. Hero value bumped totext-5xl extrabold tracking-tight. - Free-form toggle label inverted. Button showed the NEXT mode rather than the CURRENT one. New label reads Layout: Compact / Layout: Free-form so current state is unambiguous, with a tooltip describing what clicking will do and an icon to make the affordance obvious.
status_label = 'N/A'dominated'completed'in a unioned dbt intermediate model. One external source's rows hardcodedstatus_label = NULL, so the dashboard'sCOALESCE(status_label, 'N/A')ballooned every row from that source into the N/A bucket — outnumbering the realcompletedrows from the primary source. Replaced the NULL with a case-insensitiveCASE WHENagainst that source's raw status column covering completed / booked / pending / draft / no_show. Requires a downstreamdbt run --select <model>+to take effect.- Data Policies slice 6 audit now captures
old_status(the pre-flip status) alongsidenew_value, so the audit drawer can reconstruct the transition without a side query.