The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
6.0 KiB
Permissions and sharing
Team scoping
DashboardWidget.teamrequired — all reads/writes filter byteam_id- Tile upsert validates existing widget IDs belong to the dashboard's team
Two access layers (both must pass)
| Layer | Check |
|---|---|
| Dashboard RBAC | dashboard:read / dashboard:write; access level on dashboard object |
| Product RBAC | See Product RBAC below — frontend lock + backend run_widgets must agree |
Product RBAC
Canonical rule (also SKILL.md rule 1):
- Backend:
required_product_accesson eachWidgetSpecinregistry.py(surfaced viaWIDGET_REGISTRY/get_widget_registry_entry) →get_widget_product_access_errorinwidget_access.pyforrun_widgetsand tile mutations - Frontend: matching
productAccesson catalog + registry;userHasDashboardWidgetProductAccessinDashboardWidgetItem.tsx; extendDashboardWidgetProductAccessandWIDGET_PRODUCT_ACCESS_CHECKSwhen adding a gated product required_scopeson registry entries is documentation only — never use it for enforcement- Never add per-type
if widget_type == …indashboard.py
Frontend locked alone is insufficient — backend must enforce the same gate.
Error tracking list tile — row status/assignee mutations: dashboard edit or Error tracking Editor (userCanMutateErrorTrackingIssuesOnDashboard in widgetProductAccess.ts). Tile filter PATCH still needs dashboard edit.
Copy / move / duplicate
| Action | Widget support |
|---|---|
| Duplicate within same dashboard | Deep-clones widget row; duplicateTileSuccess triggers refreshDashboardWidgets for the new tile so data loads without a full page refresh |
| Copy to another dashboard | Deep-clones widget row + tile on destination (name gets (Copy) when set) |
| Move to another dashboard | Moves tile row; same DashboardWidget FK (widgets are not shared across dashboards) |
| Duplicate entire dashboard | Always deep-clones widget rows (even when duplicate_tiles: false for insights) |
Button tiles still cannot be copied or moved between dashboards.
Cross-project copy
Transferring a dashboard between projects deep-clones widget rows. DashboardWidgetVisitor (insights/models/resource_transfer/visitors/dashboard_widget.py) is user_facing=True (default) so widgets appear in transfer preview dependencies with display_name from widget name when set. DashboardTile visitor stays user_facing=False. Tests: insights/api/test/test_resource_transfer.py (test_preview_returns_dashboard_with_widget_tiles).
Shared / public / subscriptions
| Placement | Widget tile behavior |
|---|---|
| Private dashboard | Full tile: run_widgets fetch, live Component, edit chrome when allowed |
Public / shared link (DashboardPlacement.Public) |
Tile renders with header metadata; no run_widgets fetch; body shows catalog sharedPlaceholder via WidgetCardSharedPlaceholderBody |
Export (DashboardPlacement.Export) |
Widget tiles hidden (isWidgetTileVisibleOnPlacement — export only) |
| Subscriptions / snapshots | Read-only — no edit modal, no mutations from tile chrome |
Public shared dashboards (implementation)
isWidgetTileVisibleOnPlacement(dashboardUtils.ts) — hides widgets on export only; public/shared still render tilesdashboardLogic.dashboardWidgetsEnabled—trueon public when the dashboard has widget tiles (enables tile chrome + layout; still skipsrefreshDashboardWidgetswhenplacement === Public)SharedDashboardWidgetMetadataSerializer(dashboard.py) — when tile serializer context hasis_shared, widget payload is metadata-only (no audit/user fields beyond what the shared view needs)- Sharing path —
insights/api/sharing.pysetsis_shared: Trueon dashboard tile serializer context - Frontend placeholder —
DashboardWidgetItembranches onplacement === Public→WidgetCardSharedPlaceholderBodywithheaderCatalogEntry.sharedPlaceholder ?? DEFAULT_SHARED_DASHBOARD_WIDGET_PLACEHOLDERfromcatalog.ts titleHrefsuppressed on public; ⋯ menu / edit controls hidden via existing placement helpers- Tests —
insights/api/test/test_sharing.py(widget tiles in shared payload);DashboardWidgetItem.test.tsx(public placeholder)
When adding a type, set sharedPlaceholder { title, message } on the catalog entry (product-specific copy). Omit only if generic fallback copy is acceptable.
Activity logging
DashboardWidgetusesModelActivityMixin; scope"DashboardWidget"handle_dashboard_widget_changeindashboard.pylogs create/update/delete- Do not store secrets in
config— changes are logged