Repository navigation
feat: deployments, and one dashboard design for every role - #119
Conversation
…ashboards The admin, watcher and super admin dashboards each had their own copy of the layout, sidebar and nav. They now share DashboardShell and DashboardSidebar (components/dashboard/), after the Gigz app: a grey frame holding a see-through sidebar, the content in a white card with rounded corners, and a sidebar that collapses to an icon rail with tooltips (header toggle or Ctrl/Cmd+B). Each dashboard keeps only its nav list in app-sidebar.tsx, so a style change reaches all three. The layouts are now server components, so they can read the sidebar_state cookie and keep a collapsed sidebar collapsed on reload. Nav highlighting matches sub-pages too, with the home link matched exactly. The unused nav-actions.tsx template leftovers are removed.
The card grew with the page, so its bottom ran off the screen and its rounded corners were never seen. As in Blaze, the shell is now locked to the screen height and the card is a viewport: it always ends 8px above the bottom edge, and the page scrolls inside it, under the header, clipped by the card's corners.
…ions" The super admin header showed a fixed "Super Admin". It is now a breadcrumb: "Super Admin", linking back to the dashboard's home, then the section you're in. The section's name comes from the same nav items the sidebar shows (moved to superadmin-dashboard/nav.tsx so both use one list), so the two can't drift apart. DashboardBreadcrumb is in the shared shell, so the admin and watcher dashboards can use it too.
The rebrand set --muted to #6b6b6b, the same as --muted-foreground, but nothing uses muted for text: it is the surface behind skeletons, avatar fallbacks and muted panels (bg-muted, fill-muted), which all turned dark grey. Back to oklch(0.97 0 0), the subtle grey the sidebar's accent uses. This also lightens the /reports and /datasets-page card sections, which hold white cards written for the light grey.
Dashboard pages sat in the public site's Container: capped at max-w-360 and padded 64px a side from a 1280px *viewport*, though the card beside the sidebar is far narrower. That left empty bands at the sides, and collapsing the sidebar only widened them. The card's scroll area is now a size container, and the content is zoomed by --dashboard-scale, clamp(1, 100cqw / 64rem, 1.5): the site's --viewport-scale, measured against the card instead of the viewport, so the content grows when the sidebar collapses as well as when the window widens. Inside the card, Container (now marked data-slot="container") drops its max width and uses 16/24px padding.
Zooming the content to the card's width made everything look too large. The empty bands at the sides came from Container's max width and viewport-based padding, which the shell still removes, so pages now fill the card at their normal size and grow and shrink with it as the sidebar opens and closes. --dashboard-scale and the size container are gone.
The stat cards at the top of the dashboards took their height from their content and their width from the grid, so they stretched and squashed as the sidebar opened and closed. The grid also switched to four columns at a viewport width, which with the sidebar open made the cards too narrow for their content. StatsCard is now a 3:2 tile, the usual KPI card shape, with the title at the top and the figure at the bottom. The cards sit in a new StatsGrid whose columns follow its own width (container queries): one column, two from 32rem, four from 56rem. Those steps, with the padding trimmed to 20px, keep every card wide enough for its content at 3:2. On one column the card keeps its natural height. The super admin's loading skeleton matches, so nothing jumps when the figures arrive.
56 of the dashboards' 80 cards overrode Card's rounded-xl (14px) with rounded-md (8px), which looked pinched inside the 16px-rounded content card. The override is dropped, so they take Card's default; the other 24 already did.
Dashboard cards drop their border for a background of --muted, the subtle grey of the sidebar accent and the skeletons. Rules at the end of globals.css apply it to every card in the shell's content area (now marked data-slot="dashboard-content"), so the public site's cards are unchanged. They are unlayered so they beat Card's own bg-card and border utilities, and the border is made transparent rather than removed, so nothing shifts by a pixel. Inside a filled card, surfaces of the same grey (skeleton bars, muted panels, accent highlights) would vanish, so they turn white. The dashed "new form" card is an add placeholder and stays outlined.
The stat cards' titles were text-sm and medium weight, smaller than
every other dashboard card's text-lg semibold title. They now match,
with the icon up from 16px to 20px to balance. The longer titles
("Published Reports") would wrap on the narrowest four-column cards
and break the 3:2 shape, so StatsGrid goes to four columns from a
64rem container (cards ~244px) instead of 56rem.
The admin, watcher and super admin home pages opened with "Welcome
back, {name}", a line of description and, on the super admin's,
"Last updated" (the time the page rendered). They now start with the
stat cards. The admin's organisation name went with it; the header
breadcrumb shows it, so the page's own query for it is gone too. The
watcher keeps its Quick Report button and organisation name, which are
not a greeting, right-aligned above the cards. The super admin's
loading skeleton loses the welcome placeholder to match.
The stat cards' figures were text-2xl (24px), small for tiles that are 244px wide at the least and up to ~500px. They are now text-4xl (36px), and text-5xl (48px) once the card itself is 20rem wide: a container query on the card, so the figure follows the card's width as the sidebar opens and closes, not the viewport's. leading-none keeps it inside the 3:2 shape on the narrowest cards (140px of content in 163px), tabular-nums keeps digits from shifting, and the change note beside it keeps a gap and right-aligns if it wraps.
The stat cards' icons were 16-20px in the top right corner, next to the title. They are now watermarks: 96px, and 128px once the card is 20rem wide (the same step as the figure), at 7% of the text colour, in the card's empty right-hand middle and bleeding off its edge, under the title and figure. They tint with the title on hover. The stroke is thinned to 1.5, since Lucide's grows with the icon, and the icon is hidden from screen readers and the pointer. The super admin's loading skeleton drops its icon placeholder to match.
StatsCard takes a Hugeicons icon (IconSvgElement) and renders it with HugeiconsIcon, the icon set the home page already uses. The twelve cards map one to one (Users to UserGroupIcon, AlertTriangle to Alert02Icon, and so on; FormInput, which Hugeicons lacks, to Task01Icon), except two super admin cards whose Lucide icons didn't fit: Admins gets UserShield01Icon instead of a bare shield, and Users gets UserMultipleIcon instead of an eye. Lucide imports the swap left unused are dropped, along with a few that already were.
Solid Hugeicons need Hugeicons Pro, so the watermarks stay outline icons, made to read more as shapes: Hugeicons' standard stroke of 2 instead of 1.5, and 9% of the text colour instead of 7%.
The admin, watcher and super admin home pages wrapped their Container in padding of their own (py-4 md:p-6, or py-6): 24px of empty space between the header and the stat cards, and 48px at the sides with the Container's 24px. The wrappers now keep only a 24px bottom margin, so the cards start right under the header and the sides are the Container's 24px, the same on every dashboard page that uses it.
The table in QuickOverview (Pending Applications, Critical Incidents and the other dashboard lists) sat in a rounded-sm (~4px) border inside a rounded-xl (14px) card. It is now rounded-lg (10px), a step smaller than the card, and clips its contents so the rows' hover background follows the corners.
Two things let "Recent Platform Activity" run past the screen edge on phones. The dashboards' content grids set columns only from lg up, so below that the single column was sized auto and grew to its widest unwrappable content; they now say grid-cols-1, which Tailwind sizes minmax(0, 1fr), so the column can't outgrow the screen. And inside RecentActivity, the title had truncate but, as a flex item without min-w-0, never shrank below its full one-line width; it now truncates, the timestamp beside it no longer shrinks, and long descriptions (IDs, emails) wrap. The grids on all three home pages get the fix.
The rule that turns muted surfaces white inside a filled dashboard card matched [class*="bg-muted/"], which also catches variant classes such as hover:bg-muted/50. Every TableRow has that class, so every table row inside a card was painted white permanently, with no hover feedback. It now matches only plain bg-muted/NN classes (at the start of the class list or after a space), and table rows in a filled card get a hover that shows on the grey: a 60% white wash.
The applications table sat in a rounded-md bordered box. It is now a Card (flush: no padding, gap or shadow), so it takes the style the dashboard home pages use: filled with the muted grey, no outline, Card's rounding, with the table's rows clipped to it.
Its wrapper had pt-6, 24px of empty space between the header and the table. Like the dashboard home pages, it now has no top padding and a 24px bottom margin.
As on the applications page: the organizations table is now a flush Card instead of a rounded-md bordered box, so it takes the dashboards' card style (filled with the muted grey, no outline), and the page's wrapper drops its top padding (pt-2) for a 24px bottom margin, so the table starts right under the header.
As on the applications and organizations pages: the admins table is now a flush Card instead of a rounded-md bordered box, so it takes the dashboards' card style (filled with the muted grey, no outline), and the page drops its top padding (pt-6) for a 24px bottom margin, so the table starts right under the header.
As on the applications, organizations and admins pages: the watchers table is now a flush Card instead of a rounded-md bordered box, so it takes the dashboards' card style (filled with the muted grey, no outline), and the page drops its top padding (pt-2) for a 24px bottom margin, so the table starts right under the header.
The super admin reports page opened with an "All Reports (Superadmin)" heading, a "N total reports across all organizations" line and a Refresh button, then a "Filters" card whose controls sat in a cramped group beside the title, ending in "Showing N of N reports". All of that is now one row straight on the page: the search (with an icon) fills the space the rest leave, then status, organization and Refresh. With no card behind them, the controls are filled with the muted grey instead of outlined. On phones the row stacks. The table's card was padded 24px above and below (Card's py-6); it is now flush, in the dashboards' card style, like the other tables. The page gets no top padding and a 24px bottom margin.
POST /admin/incident-types creates a platform-wide incident type, but the /admin group requires an organization, which a super admin has none of, so the super admin's incident types page couldn't create one (403). The same CreateIncidentType handler is now also registered at POST /superadmin/incident-types, behind the super admin role only. No new logic: the handler never tied a type to an organization. Swagger regenerated (swag v1.16.4, as pinned in go.mod).
The super admin's incident types page rendered the admin's component:
"Your Organization's enabled incident types" and types "you can enable
for your organization", which mean nothing to a super admin, who has no
organization. It now has its own component listing every incident type
on the platform, active or not (colour, name, description, status,
created), with a search and a "New incident type" button in one row,
the search filled with the muted grey, and the table in a flush card in
the dashboards' card style, starting right under the header.
Creating goes through the new super admin route: a
createIncidentTypeAsSuperAdmin procedure and incidentsApi wrapper, and
a scope prop on CreateIncidentTypeDialog ("organization" by default,
"platform" here). The list comes from getAllIncidentTypesForSuperAdmin
(GET /incident-types?activeOnly=false). The admin's page is unchanged.
Datasets are no longer kept, so the feature goes: the super admin's /superadmin/datasets page and its sidebar link, the public /datasets-page (unlinked from the site's navigation and footer, but still reachable by URL), features/datasets, the datasets tRPC router and lib/api/datasets.ts, and the Datasets messages (with Navigation.datasets, used only for that page's title) in all 13 languages. The sitemap comment, TRPC.md, SEO.md and the README stop naming them. The Go API's dataset routes go in the next commit.
Datasets are no longer kept. With the frontend no longer calling them (previous commit), the API's side goes too: the Dataset entity, repository interface, Postgres repository and its cache decorator, the usecase, the handler, the four public /datasets routes and the three /superadmin/datasets routes, and their wiring in server.go. Swagger is regenerated without them (swag v1.16.4). Only code is removed. The datasets table and the files its rows point to in R2 are left as they are; AGENTS.md says so. The 50 MB upload cap was sized for datasets; it stays, as uploads such as reports may rely on it, and its comment now says why it exists.
The schema had no migrations since Drizzle's were deleted, so it lived only in each database. Schema changes are now goose migrations in backend/migrations, embedded into a cmd/migrate command (`make migrate`, `make migrate-status`) that uses the API's DATABASE_URL. goose is pinned to v3.27.0: later releases require Go 1.26, and CI builds with 1.25. The first migration adds deployments, the platform's new centre: an organization's own map, with a name and description, a unique public slug, an area (country, as report locations store it, and region), a draft/live/closed status with optional dates, and its theme, the incident types it tracks, in deployment_incident_types. Applied to the development database; its Down section was checked with a round trip.
The admin's home showed the older model: Active Watchers, Total Incidents, Active Forms and Published Reports, recent incidents, pending reports and incident charts. Until reports are tagged with the deployment they arrive through, the numbers an admin can trust are their deployments', so the home now shows those: Deployments, Live, Drafts and Closed (counted by Go, GET /admin/deployments/counts, via getOrganizationDeploymentCounts), and their five newest deployments in the shared table, with a way to create the first one.
Both homes now read the deployments model; AGENTS.md says where each number comes from, and that per-deployment report numbers wait for reports to carry their deployment.
anonymous_incident_reports gets deployment_id: set when a report arrives through a deployment's reporting link, null for the general form and every report from before deployments, and cleared if the deployment is deleted. A partial index serves a deployment's map. Applied to the development database; Down checked with a round trip.
- GET /deployments/:slug: a deployment as anyone may see it (its organization's name, description, area, dates and incident types), for its public page and reporting link. Live and closed deployments are found; a draft is its organization's business, so it is a 404. - PATCH /admin/deployments/:id/status: an admin takes one of their organization's deployments live, back to draft, or closes it. Another organization's deployment is a 404. - Deployment.AcceptsReports: live, and inside its dates if it has them; an end date is the last day, inclusive. The next commit uses it for reports arriving through the reporting link. The repository's queries share one finder, so a lookup by slug or ID reads the theme the same way as the lists. Tests cover the draft rule, the organization check and the date edges. Swagger regenerated.
- POST /incidents/anonymous takes an optional deploymentId, set by a deployment's reporting link. The incident usecase accepts it only while the deployment takes reports (live, inside its dates) and only for an incident type it tracks; otherwise a 400 that says which. The report is saved with its deployment_id. - The map and analytics filters take deploymentId: only the reports sent through that deployment's link. Reports through a deployment stay public, so the global maps show them too. - The map cache key includes the deployment, so two deployments' maps (or one and the global map) never share cached points; a test pins it. Tested in the usecase (each refusal) and end to end against the running API and the development database: a report of a tracked type is saved and tagged, another type refused, and the deployment's map shows exactly its report. Swagger regenerated.
- deploymentsApi.getPublic (a live or closed deployment by slug, for its public page's server component) and updateStatus, with tests. - tRPC updateDeploymentStatus (admin): draft, live or closed; Go's 404 and 400 pass through. - The public report submission carries an optional deploymentId (the procedure names each field it forwards, so it is added there too), and the map filter a deploymentId, through to /map/points and /map/summary. The pages that use them follow.
Deployment responses (the public one, the admin's and super admin's lists, and a status change) carry acceptingReports: AcceptsReports at the time of the request (live, inside its dates). Pages can then offer or hide the reporting link without repeating the rule.
- /d/<slug>: the deployment's live map. The full-screen live map, given the deployment: only the reports sent through its reporting link and only its incident types, under a bar naming it, its organization and area, with "Report an incident" while it takes reports and a note when it doesn't. The site header steps aside, as on the other full-screen maps. - /d/<slug>/report: the public report form as the deployment's reporting link: the deployment named under the title, the incident type list limited to its theme, and the report sent with its deploymentId. When it isn't taking reports, a note and a link to its map replace the form. Both pages read the deployment on the server (getPublicDeployment, cached per request) for their title and description, and are a 404 for an unknown slug or a draft. Three new strings (DeploymentPage) in all 13 languages. Checked on the development server with a temporary live deployment: both pages at desktop and phone widths, the form offering exactly the deployment's three incident types (the general form still offers all six), and a 404 for an unknown slug.
Each row of the deployments table gets a menu. Once a deployment isn't a draft (a draft's pages aren't public), it opens the deployment's map in a new tab; for the admin it also copies the reporting link (/d/<slug>/report) and changes the status: a draft goes live, a live one closes or moves back to draft, a closed one reopens. The super admin's table has the map only. The single "Reporting Link" page, which copied the general /anonymous-reports form's address for every organization, is retired: each deployment has its own link now. Its sidebar item goes, and its address redirects to Deployments.
How /d/<slug> and /d/<slug>/report work, what Go checks before accepting a report through a deployment, and that a new MapFilter field must also go into the map cache key.
Reports now carry their deployment, so the dashboards can count them: - Every deployment in a response has reportCount: how many reports came through its reporting link. - GET /admin/deployments/stats replaces /admin/deployments/counts (unreleased): the organization's deployments by status, plus the reports through them, ever, this month against last, by incident type, and per week over the last seven weeks. - The super admin's stats gain mostActiveDeployments: the five deployments with the most reports this month (only those with any), with their totals. growthMetric moves to entity.NewGrowthMetric, shared by both dashboards. Checked against the development database with a temporary deployment and three reports (two this month, one last): the totals, the month split, the most-active entry and reportCount. Swagger regenerated.
- Admin home: Deployments (draft and closed beneath), Live, Reports This Month (against last month) and All Reports, all through the organization's reporting links; their reports by incident type and per week (shown once there are any); then their deployments. It reads getOrganizationDeploymentStats, which replaces the counts procedure; the line chart's headline uses Go's month-on-month numbers rather than working out a change from the points. - Super admin home: Most Active Deployments (this month, with their totals, each linking to its public map) replaces Recent Deployments. - Both deployments tables gain a Reports column. monthOnMonth moves to components/dashboard, shared by both homes.
The admin's home reads /admin/deployments/stats (which replaced /counts), the super admin's adds the most active deployments, and deployments carry reportCount.
The invite form loses its heading and bordered box: name, email and "Invite watcher" are one row, filled with the muted grey, with each field's message beneath it. The watchers table drops its heading and count, and sits in a flush card (no white table background over the fill). The page uses the standard container, with no top padding and a 24px bottom margin.
The "Reports" heading and count, the "Filters" card and its "Showing N of N" go. Search (new: by title, over the loaded list), status, Refresh and Upload Report are one row, filled with the muted grey, the search taking the room the rest leave. The table sits in a flush card. No top padding, a 24px bottom margin.
The "Incident Types" heading and description go; search and "New
incident type" are one row, the search filled with the muted grey. The
two tables sit in flush cards under short headings ("Enabled for your
organization", "Available to enable") in place of card titles and
descriptions. No top padding, a 24px bottom margin.
The "Incidents" heading and count, the "Search & Filters" card and its "Showing N of N" go. Search, status, form, sort (with its order toggle), Refresh and Clear filters are one row from lg up, filled with the muted grey, the search taking the room the rest leave. The table sits in a flush card. No top padding, a 24px bottom margin. The sort select's value is typed instead of any.
The "Incident Details" heading and full-ID line give way to one row: back, "Incident <short id>" (the full ID on hover) with its status badge, and Actions, filled with the muted grey. The two cards lose their shadow and the description line; their titles take the dashboards' card style. A 24px bottom margin.
The "Forms" and "Existing Forms" headings, each over its own grid, become one grid: the dashed Create New Form card first (it keeps its outline), then the forms. The page drops its own p-6 wrapper (the content brings its container) and uses the standard width, with no top padding and a 24px bottom margin.
The questions and settings panels become filled, rounded cards beside the form, with a gap, instead of edge-to-edge panels with borders and grey title bars; their titles take the dashboards' card style. Each question is a white card on the grey (no shadow or border). The preview loses its title bar, and the form in it is a filled card. On narrow screens the panels stack instead of squeezing two fixed 320px columns beside the form.
The page brought its own sticky, blurred header bar and min-h-screen from before the dashboard shell. Now: Back to Forms and Edit Form in one row, then the form in a shadowless card whose title takes the dashboards' card style, inside the shell's padding. The loading state centres in 400px rather than a full screen.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo many files! This PR contains 181 files, which is 81 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configuration
⛔ Files ignored due to path filters: 1
📒 Files selected for processing: 181
You can disable this status message by setting the
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Moves Watchtower from "organisations whose watchers file reports" to deployments, and gives the dashboards one shared design. An approved organisation's admin creates deployments: each is their own map, with a theme (the incident types it tracks), an area, a draft/live/closed status with dates, and its own public reporting link.
Before merging or deploying
Run the migrations against each environment's database. They're new tables and a nullable column, so existing data is untouched:
Schema changes now go through goose (
backend/migrations,make migrate,make migrate-status). The repo had no migrations after Drizzle's were deleted. goose is pinned to v3.27.0, because later releases need Go 1.26 and CI builds with 1.25. The development database is already at version 2.Deployments
/d/<slug>, showing only the reports sent through its link. Its reporting page is/d/<slug>/report: the public form, limited to the deployment's incident types. Drafts aren't public and return a 404.GETandPOST /admin/deploymentsPATCH /admin/deployments/:id/statusGET /admin/deployments/statsGET /deployments/:slugGET /superadmin/deploymentsGET /superadmin/organizationsdeploymentIdfilter on the map and analytics endpointsdeploymentIdonPOST /incidents/anonymousDashboards
components/dashboard/): a collapsible sidebar and the content in a rounded card that stays within the screen. Cards are filled with the muted grey instead of outlined.Removed (marked
!in the commits)/datasets-page, the API routes and their code. Thedatasetstable and its stored files are left in place; drop them on purpose if they aren't wanted.GET /superadmin/dashboard/statsreturns a new shape. The only caller, the super admin home, is updated in this PR.Worth checking
DeploymentPage). Luganda, Luo, Kikuyu, Oromo, Dinka and Sukuma are best effort and need a native speaker's review.Verification
go vet.--no-verify. The hook runspnpm build, which rewrites the shared.nextand breaks a running dev server, and also fails locally on missing AWS credentials.