The board carried two headers: a host identity strip (hostname, live dot, IP,
kernel, uptime, clock) and, under it, the board tabs and controls. Two rows of
chrome before any actual dashboard, and the strip was fixed — you could not
move it, size it, or point it at a different machine.
It is one row now: board tabs left, then the server picker, range, refresh,
TV and Edit. The server switcher moved into that row as a labelled `server`
pill, and still collapses to a plain readout when there is only one host.
Nothing was lost, it just became placeable. IP, kernel and uptime are the Host
details widget — which can be resized and pointed at any server, unlike the
strip. And the time is a new Clock widget: add it if you want it, put it on a
second timezone if that is what you need, leave it off if you do not. It ticks
locally rather than polling, because re-fetching every second to print a number
the browser already knows would be silly.
Also fixes a regression from the previous commit: making the spark row flexible
let it compete with the value for height, and centring then pushed the number up
out of the tile. The reading is the point of a stat widget, so it is pinned
first and the chart takes what is left.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The widget editor was a cramped fixed panel wedged against the right edge, with
inputs that rendered as bare white boxes because the field class sat on a
wrapper div while the stylesheet styled it as though it were the input. It is
now a wide drawer: settings on the right, a LIVE PREVIEW of the actual widget
on the left, sized from its real grid geometry so the size steppers visibly
move it. Stacks below lg. Built on the shared Drawer, which also makes the
floating AI bubble hide itself — it already hides for any open dialog, and the
old bare <aside> was exactly why it did not.
Every tile read "Stat". Seeded widgets carry no title, so the frame fell back
to the widget TYPE and a board of four different stats said "Stat" four times.
Titles are now derived at render time from what the widget points at — CPU
usage, Memory, Applications, "CPU usage & Memory" for a two-series chart —
falling back to the type name only when nothing better is known. Deliberately
derived rather than written into stored config: a title the user never set has
no business appearing in their saved board.
Boards already in the database are repaired on read. Fixing the shipped
defaults only ever helped users who had never opened the dashboard, because
seeding never runs again for anyone else — so `metrics` becomes `series`,
`actions` becomes `items`, dead shortcut keys are aliased or dropped, and an
empty shortcut list falls back to the shipped four instead of rendering "No
actions selected". The repair works on a deep copy: a JSON column mutated in
place looks unchanged to SQLAlchemy, so the commit would expire the instance
and silently reload the originals.
The log widget stopped guessing. With nothing configured it grabbed whatever
the log listing returned first — typically a system file the panel cannot read
— so a fresh dashboard greeted people with "sudo: a password is required". It
now asks you to pick a source, and phrases a permission failure as a setup
fact rather than quoting the shell.
Also deletes ~300 lines of stylesheet for the inspector that no longer exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thirty-five of the fifty-two class names the widget renderers emit had no rule
anywhere. The renderers kept the design mock's flat names (`skw-stat-v`,
`skw-feed-item`, `skw-bar-row`) while the stylesheet was written in BEM
(`skw-stat__value`, `skw-topn__row`). Both files compiled, linted and built
clean the whole time — the styles simply never reached the markup.
So the activity feed rendered as raw text, host details ran their labels into
their values ("HostnameDESKTOP-8ELA366"), stat values wrapped mid-tile, and the
time-series chart lost its height constraint and drew as stray vertical spikes
against an empty box. Renamed the JSX to match, and wrote the blocks that were
missing outright: the feed, quick-action rows, alert body, and distinct
loading-vs-error empty states.
The seeded boards were wrong in the same way, one layer down. `timeseries`
seeded `metrics` while the renderer reads `series`; `actions` seeded `actions`
while the renderer reads `items`, with shortcut keys (`new-app`, `deploy`) that
no route resolves. Both fail silently — the widget just renders empty, which is
what every new user saw on their first dashboard. `logs` and `status` were
seeding keys nothing reads either.
Two tests now pin that coupling: seeded configs must use keys the renderers
actually read, and every seeded shortcut must resolve to a real route. The
table in the test is an explicit mirror of renderers.jsx, and says so.
Also: seeded widgets carry real titles again — "CPU usage", "Applications",
"Hardware specs", following the old dashboard's panel names — instead of every
tile falling back to its widget type and reading "Stat". And "Local (this
server)" is now "This server", which fits a 3-column tile on one line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dashboard's layout lived in localStorage as `{id, visible}` — a visibility
toggle wearing a layout's clothes. It carried no geometry, so widget width was
baked into SCSS (`.dash-kpi { grid-column: span 3 }`) and "make the chart wider"
was a code change. And it stayed on one machine.
`DashboardBoard` gives a user real boards: named, ordered, each holding a JSON
array of placed widgets with x/y/w/h and per-instance config. Three defaults
ship — overview, infrastructure, apps & deploys — seeded on a user's first GET
and restorable per board.
Validation is deliberate about the boring cases: geometry must fit the 12-column
grid, ids must be unique within a board, bools are rejected as coordinates
(`bool` subclasses `int`), and a rejected PUT leaves the stored board untouched
rather than half-applied. A board belonging to another user is a 404, not a 403
— there is no reason to confirm it exists.
The shipped defaults are checked by rebuilding their occupancy map cell by cell,
so editing DEFAULT_BOARDS into an overlap fails the suite instead of shipping a
broken first-run layout.
31 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>