Home mosaic, multi-select delete, offline banner and loading states

- Home puts the dashboards beside the flows: two equal-height columns,
  capped and scrollable, most recently worked on first. Each tile is a
  schematic footprint built from the stored widget placements.
- Flows and dashboards can be picked by long press or ctrl-click; the
  create button becomes a trash and one dialog covers the batch.
- The offline banner is drawn on the body so it centres on the viewport,
  and the live socket now releases the offline latch a stray 503 set.
- A boot spinner before React's first commit, a router pending screen for
  code-split pages, and skeletons where an empty list used to flash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
This commit is contained in:
2026-08-22 12:02:14 +02:00
co-authored by Claude Opus 5
parent 5369ccb68e
commit cec31ba853
13 changed files with 837 additions and 89 deletions
+25
View File
@@ -16,6 +16,16 @@ type Connection = {
}
let state: Connection = { offline: false, lastSeen: null }
/**
* Whether the live socket is up right now.
*
* The socket is the continuous signal; a failed request is only a lagging one.
* Latching `offline` on a single refused request is what used to leave the
* banner up for good: the socket had never dropped, so no reconnect was coming
* to clear it, and nothing else on a quiet page refetches. Reloading was the
* only way out.
*/
let socketOpen = false
const listeners = new Set<() => void>()
function emit(next: Connection) {
@@ -28,6 +38,10 @@ function emit(next: Connection) {
export const connectionStore = {
setOffline(lastSeen: string | number | null) {
// The socket is up, so the installation is reachable: this was one bad
// answer rather than a lost tunnel, and the banner would have nothing to
// take it back down again.
if (socketOpen) return
const at =
typeof lastSeen === "string"
? Date.parse(lastSeen)
@@ -39,6 +53,17 @@ export const connectionStore = {
setOnline() {
emit({ offline: false, lastSeen: null })
},
/**
* The live socket opened or closed.
*
* An open socket is proof the installation can be heard, so it also clears
* the banner — and since the socket reconnects on its own, that is the one
* release the offline state can always count on.
*/
setSocketOpen(open: boolean) {
socketOpen = open
if (open) emit({ offline: false, lastSeen: null })
},
subscribe(listener: () => void) {
listeners.add(listener)
return () => listeners.delete(listener)