Default dashboards
There's one global default per app mode — a row with company_id IS NULL.
Any company without its own config renders the default for its app mode.
Three defaults ship today.
Payroll
Two tabs, no inheritance.
| Tab | Endpoint | Filter | Action | Widgets |
|---|---|---|---|---|
overview (Home) | GET /dashboards/payroll, param payroll_period_id | groupedSelect period_id, grouped by year | link → run payroll | a group (left column) + three charts: cost overview, YTD costs, cost by period |
wallet (AccountBalanceWallet) | GET /dashboards/payroll_wallet, param year | select year | link → run payroll | one chart: wallet transactions breakdown |
The period filter defaults from the payload itself
(current_period.payroll_period_id), so the dashboard opens on the current period
without the URL carrying a value.
EDI
One tab, no inheritance.
| Tab | Endpoint | Widgets |
|---|---|---|
transfer_volume (SsidChart) | GET /dashboards/edi, no params | statCards KPI row · a group with the volume chart + a progressList · a tabbedGroup of three calendarHeatmaps (total / incoming / outgoing) · four pie charts (204 / 990 / 214 / 210 by partner) |
Read this one as the worked example of dynamicCards. The KPI row has one
hard-coded card (partner count) and then expands an array into the rest:
A company trading two sets gets two cards; one trading eight gets eight. Nobody edits the config when a new set appears — the SP returns it and a card shows up, titled "EDI 940" until the translation key exists.
AI Platform
One tab, no inheritance.
| Tab | Endpoint | Widgets |
|---|---|---|
chat_usage (TrendingUp) | GET /ai/chats/usage_per_day, no params | three groups — tokens, chats, messages — each pairing KPIs with a daily trend |
How inheritance works
A company config can pull tabs from its app-mode default. Two ways:
inheritDefaultTabs—"prepend"puts the default's active tabs before yours,"append"after. Deduped bykey.$inheritplaceholders —{"$inherit": {"from": "default", "key": "…"}}drops one specific default tab exactly where you put it. Use this when the order matters.
The pull happens at render time, not at save time. Change a default and every inheriting config picks it up on the next page load. That's the feature, and also the risk — check who inherits before restructuring a default.
key breaks inheriting configsA placeholder pointing at a key that no longer exists is silently dropped. The tab just stops appearing, with no error anywhere. Update the placeholders in the same change.
To find who inherits: look for configs on that app mode whose inheritDefaultTabs
isn't "none", or whose tabs contain a $inherit.
Changing a default
All of it happens in the Builder.
- Settings → Dashboard Builder.
- Open the card under Global defaults for that app mode.
- Edit, watch the validation chip, Save & activate.
The save follows the revision pattern: the old default becomes an inactive row you can restore any time from the revision-history drawer. That history is the backup — there's no file to keep in sync.
Every company without its own config renders this, and every inheriting config
pulls from it. Check who inherits (below) before restructuring one, and prefer
isActive: false over deleting a tab.
Copying a default to another environment
Since configs live only in the database, moving one between dev, stage and prod is a copy-paste through the Builder:
Validation still runs before the save, so a config that came from somewhere else gets checked exactly like one you built by hand.
Icon names and permission codes are validated by the Builder's dropdowns, not by the Validator. A config pasted from another environment can reference a permission code that doesn't exist there — which silently hides the element from everyone. Spot-check permissions after a cross-environment copy.
Two rules specific to defaults
A global default can't be deactivated. POST /:id/deactivate returns 422
when company_id IS NULL. It's the fallback every company without its own config
renders — turning it off would leave them with a 404.
Only one default per app mode can be active. Filtered-unique indexes enforce it, and the controller deactivates siblings before inserting or activating, so those indexes are never tripped.