Feature Cards¶
Component-gated dashboard cards — the reusable pattern for widgets whose existence (and backend work) is justified by an opted-in component.
Details¶
work-buddy's dashboard hosts widgets whose existence is justified by an opted-in component — the Obsidian bridge sparkline only makes sense if the user wants Obsidian. A feature card is the reusable pattern that ties such a widget to its component: when the component is opted out, the card is not rendered (no placeholder) and the backend work that feeds it stops.
Gate AST — work_buddy/control/gates.py¶
A Gate is a typed boolean expression over component-active state: Component leaves combined with And, Or, Not. It is JSON-serializable, evaluable in Python and JS, and introspectable.
evaluate(gate, active_ids)—Truewhen the expression holds for the given active-component set. ANonegate is always active.referenced_components(gate)— everyComponentid in the tree; used for validation and "which cards depend on X?" diagnostics.validate(gate, known_components)— raisesValueErrorif a gate names a component not inCOMPONENT_CATALOG.to_json/from_json— wire format{"op": "and"|"or"|"not"|"component", ...}.parse_gate(expr)— string-DSL convenience:parse_gate("obsidian & (thunderbird | outlook)"). Operators low-to-high precedence:|,&,!; parentheses group.
The same Gate type is the intended home for future scheduler-side job gating.
Card registry — work_buddy/dashboard/cards.py¶
A DashboardCard descriptor carries id (namespaced, e.g. obsidian.bridge_sparkline), mount_point, gate, mount_slot (render order), needs_state_keys, and background_jobs. register_card() validates the gate at registration. cards_for_tab(mount_point) returns the active card descriptors in slot order.
Active = not explicitly opted out. A component counts as active unless its feature preference is unwanted (is_wanted(id) is not False — undecided, wanted, required, and core all count as active). The gate evaluates against feature preferences, NOT the control graph's effective_state.
Endpoint¶
GET /api/dashboard/cards/<mount_point> → {"cards": [{"id", "mount_slot"}, ...]} — the active cards for a mount point, gates evaluated server-side. Read-only.
Frontend — core/card_registry.py + tabs/cards/¶
window.wbCardRenderers maps card id → renderer function (sync, returning an HTML string, or async, returning a Promise of one). window.wbMountCards(mountPoint, container, state) fetches the active list, runs each renderer, and morphdom-merges the result. Each card's renderer lives in its own frontend/scripts/tabs/cards/<id>.py module.
First consumer — Settings → Activity¶
The Settings tab's Activity sub-view is registry-driven. loadActivity() calls wbMountCards('activity', ...). Three cards mount there: obsidian.bridge_sparkline (gated on Component("obsidian")), core.event_log, and core.notification_log (both ungated).
Backend gating¶
The frontend gate hides the card; the backend must independently stop the supporting work, because /api/state is fetched by every tab regardless of which card renders. get_system_state() skips get_bridge_status() when is_wanted("obsidian") is False, so an Obsidian opt-out also halts the bridge probe and its rolling latency history.
Live re-render¶
Toggling a preference fires component.preference_changed, which the event bus routes to settingsSurface.refresh(); that re-runs loadActivity(), which re-fetches the gated card list. A card whose component was just opted out disappears within ~250 ms with no page reload; opting back in restores it just as fast.
Adding a card¶
register_card(DashboardCard(id=..., mount_point=..., gate=...))incards.py(or, for a plugin, from the plugin's own module).- Add a
frontend/scripts/tabs/cards/<id>.pymodule whosescript()registerswindow.wbCardRenderers['<id>']. - Add that module to the
SCRIPTSregistry infrontend/scripts/__init__.py.
No edit to the mount point's loader is required — the registry plus endpoint do the rest.
Deferred¶
DashboardCard.background_jobs reserves a declaration surface for scheduler-side gating — a scheduled job that should only fire when its supporting components are opted in. The scheduler rule that consumes it is not yet implemented; when built it must evaluate the same Gate AST.