Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/progress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,4 @@ Oldest first; append new entries to the bottom.
| [hosted-grow-root-disk.md](hosted-grow-root-disk.md) — Grow the hosted image's root filesystem to fill the whole provider disk on boot. The image bakes a fixed **8 GiB** root (sparse raw stays small) but nothing grew it onto the far larger provider disk, so a box ran on ~8 GiB — docker image storage + the brain's SQLite store share that one volume, so a single app install can fill it and the brain's first store write 500s login (the box looks offline though it is up). **Fix:** a runtime `repart.d` definition (`Type=root`, `GrowFileSystem=yes`, no size cap) + a `malmo-grow-root.service` oneshot running `systemd-repart --dry-run=no`, ordered `Before=docker.service host-agent.service` and **fail-closed** (`Requires=` from both, via a docker drop-in and host-agent's unit) so the write-heavy services refuse to start on an un-grown root. Build-time root stays pinned at 8 GiB; the two repart definitions are deliberately separate. Lean set gains `systemd-repart` + `libfdisk1` only (no `libcryptsetup`, cryptsetup cut preserved). Boot-proof asserts the tool is present and the unit reached `active`; real full-disk growth is a provider-box acceptance step. **Known:** the stock `systemd-repart.service` also runs the same config (harmless double execution — idempotent; consolidation deferred pending a live re-test), and a pre-existing unrelated `/setup` 503 boot-proof failure reproduces on clean `main`, tracked separately | done |
| [hosted-setup-boot-proof-race.md](hosted-setup-boot-proof-race.md) — Close the `/setup` 503 boot-proof failure [hosted-grow-root-disk.md](hosted-grow-root-disk.md) tracked separately, and confirm the wildcard-TLS/`:443` path is green. The `unseeded` boot polled `POST /api/v1/setup` for **403** but broke on any of `403\|503\|409\|200`, so a transient **503** — Caddy answering "no ready `/api` upstream" in the first second after the control-plane stack comes up, before the brain's listener + dashboard route land — ended the loop and failed the proof, though the box is correct (the brain returns 403 unconditionally on hosted, no 503 path; the diag showed all four containers "Up <1–2 seconds"). **Fix:** break only on a definitive `403\|409\|200` and ride through `502\|503`, exactly as the `/api/v1/me` poll above already does; a genuinely stuck `/setup` still fails after the 30s window. The louder "`:443` never binds" symptom was a **broken-build artifact, not a live regression**: the build-and-boot CI job (`CI / Cloud image`, `publish=false`) passed both the `unseeded` and `seeded` boots (`seeded` hard-asserts `:443` bound + `caddy: wildcard TLS configured`), and every known real-box root cause (`certificates.automate` #301, seed-fetch keep-alive, static resolver) is already fixed in-tree — so no product change. Adds `docs/dev/hosted-boot-proof.md` (runbook: happy-path flow, brain-log milestones, symptom→where-to-look, how to run) and de-stales `TESTING.md`'s `/setup` gate description (the as-built SSO/403, superseding the secret 401/200). Test-lane + docs only | done |
| [hosted-grow-root-fs.md](hosted-grow-root-fs.md) — Corrects [hosted-grow-root-disk.md](hosted-grow-root-disk.md)'s "verified on a real provider box" claim. A live acceptance run found the root **partition** grew to the full disk (`systemd-repart` extended it, GPT `GROWFS` bit set) but the **ext4 filesystem inside it stayed at the original 8 GiB** — the disk-full/500 failure mode was not actually closed. Found via the same **Hetzner rescue mode** technique as `hosted-wildcard-cert-automate.md`: booting the (still-running) probe box into rescue and reading its disk + persistent journal directly. **Root cause:** `GrowFileSystem=yes` only sets the GPT `GROWFS` attribute bit; actually growing the mounted root filesystem is normally `systemd-growfs-root.service`'s job, which `systemd-gpt-auto-generator` wires up only when booted with `root=gpt-auto` — Hetzner (and cloud providers generally) boot with an explicit `root=PARTUUID=...` instead, so that generator, and the growfs service, never ran; zero mention of `growfs` anywhere in the boot journal. The existing boot-proof assertion couldn't have caught this: on the QEMU disk (fixed-size, no spare space) growing the filesystem is a no-op regardless of whether the growfs step runs. **Fix:** `malmo-grow-root.service`'s `ExecStart` now calls `systemd-growfs /` (the exact call the generator-driven service would have made) after `systemd-repart`, in the same command rather than a separate `ExecStartPost` — a review catch found `ExecStartPost` runs even on `systemd-repart`'s accepted 76/77 "nothing to grow" exits and has no visibility into which code fired, so it would fail the fail-closed unit on those harmless cases; the merged step only calls `growfs` when `systemd-repart` exits 0 and passes 76/77 through as success. `cloud-assertions.sh` also checks `systemd-growfs` is present in the lean image, and its pass message no longer claims real growth is verified by this lane. **Live acceptance:** re-provisioned onto a real box with the fix; root volume reported 79.99 GB total / 78.21 GB free (was 8.35 GB pre-fix) — filesystem grow confirmed on real hardware. `ENVIRONMENT.md` # Storage corrected to describe the grow as the two explicit steps it actually is | done |
| [oatmeal-home-settings-composition.md](oatmeal-home-settings-composition.md) — Restructure the signed-in **Home + Settings** surfaces onto the Oatmeal composition (**closes #290**, finishes the rollout #261 deferred). Two signature moves applied uniformly: every section title moves from the tiny uppercase-muted `<h2>` label to the shared `components/ui/Heading.vue` **Instrument Serif** display heading (top-level titles `:level=2`; app-detail sub-sections `:level=3`; the app-detail page title drops its bespoke `<h1>` onto `<Heading :level=2>`), and every olive-50 (`bg-card`) panel/row/table container moves from the compact `rounded-xl … px-4 py-3` to the calmer `rounded-2xl … p-5` (the auth cards' radius + cloud spacing). Covers `HomeView` (System health/Household/Yours) + all Settings sections (Account, Notifications, About, Users, Outgoing email, Installed apps, installed-app **detail**) + the desktop nav panel; **Activity** — the only section with no in-page heading — gains a serif "Activity" title. Pure composition: no behavior/IA/routing change, no literal `bg-olive-*`, no raw hex. **Two documented judgement calls**: (1) the Home launcher grids are left **open** (tiles are already olive-50 cards — wrapping them in an olive-50 panel washes their contrast and fights `DASHBOARD.md`'s calm-launcher north star), and (2) the pill `<Button>`/input **element-idiom is deferred** (it's coupled to a pill-input idiom out of #290's card-panel/heading scope; a buttons-only pass would leave pill buttons beside square inputs) → follow-up. No spec edit (composition realizes `WEB_UI.md` # Styling; IA unchanged, `DASHBOARD.md`/`SETTINGS.md` pin no heading style — matches #261's "no visual-language rule changed" precedent). `make check-web` green; live full-stack visual pass not run in-env (authenticated SPA, no headless-browser lib) — reviewer eyeball recommended on the two judgement calls | done |
41 changes: 41 additions & 0 deletions docs/progress/oatmeal-home-settings-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Oatmeal composition — Home + Settings onto serif headings + olive-50 card panels

- **Status:** done
- **Date:** 2026-07-09
- **Specs touched:** none (composition realizes the Oatmeal theme already owned by `WEB_UI.md` # Styling; no IA or visual-language rule changed — see # How it maps to the specs)
- **Closes:** #290
- **Builds on:** #260 (`oatmeal-theme.md`, palette + fonts + token remap), #261 (`oatmeal-element-idioms.md`, the shared `components/ui/Heading.vue` display idiom this slice rolls out), #262 (`oatmeal-token-sweep.md`).

## What was done

#260 recolored the whole dashboard onto the olive tokens and restructured the **auth** surfaces to the theme's composition; #261 added the shared `<Heading>` / `<Button>` idioms and applied them to chrome + auth, explicitly deferring the Home/Settings section-heading rollout to #290. This slice finishes the job for the two remaining signed-in surfaces so "our UI structure follows the theme" holds across the whole dashboard, not just its colors.

Two signature Oatmeal moves, applied uniformly to Home and every Settings section:

- **Serif `font-display` section headings.** Every section title switched from the old tiny uppercase-muted label (`text-xs font-medium uppercase tracking-wide text-muted-foreground`) to the shared `components/ui/Heading.vue` (Instrument Serif, `tracking-tight`). Top-level section/group titles use `:level="2"` (`text-2xl`); the app-detail page's per-app sub-sections (Outgoing email / Setup secrets / Settings) use `:level="3"` (`text-xl`) so they read as sub-headings under the app name. The app-detail page title (`app.name`) moved from a bespoke `<h1 class="text-xl font-semibold">` to `<Heading :level="2">` — aligning it with the rest of the signed-in shell (which is uniformly `h2`-based; there is no `h1` in the signed-in app, only the auth wordmark uses `:level="1"`).
- **Olive-50 card panels with cloud spacing.** Panels were already `bg-card` (olive-50); they moved from the compact `rounded-xl … px-4 py-3` to the calmer `rounded-2xl … p-5` (the auth cards' `1rem` radius + generous padding). Applied to every card/row/table container across the sections and the Settings desktop nav panel.

Per-surface notes:

- **Home** (`HomeView.vue`) — "System health", "Household", "Yours" are now serif headings; the health-issue rows are `rounded-2xl p-5`. The **launcher grids are deliberately left open** (tiles on the `background`, not wrapped in a panel) — see # Known gaps.
- **Settings sections** — Account, Notifications, About, Users (Add user + People), Outgoing email (Add + Email accounts), Installed apps, and the installed-app **detail** page all take the serif heading + `rounded-2xl p-5` panel treatment. **Activity** was the one section with no in-page heading (its title came only from the nav); it gains a serif "Activity" heading on the header row (Export CSV/JSON stay to its right), with the admin/member intro line moved just below — so all sections now open with a consistent serif title. The audit table card is `rounded-2xl`.
- **Settings nav** (`SettingsLayout.vue`) — the desktop nav panel bumped to `rounded-2xl` to match the content panels; the grouped-nav labels stay small (they're navigation chrome, not page headings).

No behavior, data flow, routing, or IA changed — this is a pure composition pass. No literal `bg-olive-*` and no raw hex introduced (colors keep flowing from the semantic tokens).

## How it maps to the specs

- **`WEB_UI.md` # Styling** already owns the Oatmeal theme (olive `@theme` + Inter/Instrument Serif); this slice *realizes* the serif-heading + card-panel composition it describes, so no edit was needed there.
- **`DASHBOARD.md`** (calm-launcher north star, the tile model) and **`SETTINGS.md`** (the My-account / Box-settings IA, panel inventory) describe *information architecture and behavior*, not heading pixels or panel radii. The IA is unchanged and neither doc pins the old heading style, so — matching the #261 precedent ("No visual-language rule changed, so DASHBOARD.md needed no edit") — no spec edit was warranted. The `docs/README.md` spec map is unaffected (no spec added/removed).

## Known gaps & deviations

- **Home launcher grids are left open, not wrapped in a panel — a deliberate reconciliation of #290 with `DASHBOARD.md`.** An earlier pass wrapped each Household/Yours grid in an `bg-card` (olive-50) panel to read literally as "card panels". But the app tiles' own logo squares are *also* `bg-card` (olive-50), so nesting them inside an olive-50 panel washed out their fill contrast, and boxing the apps leans toward the "control panel" look `DASHBOARD.md`'s north star argues against ("a calm launcher, not a control panel … breathing room"). On the launcher **the tiles are the cards**; re-wrapping them isn't the theme's intent. So the grids stay open (tiles as cards) with serif group headings — the composition change for Home is the headings + tiles-as-cards, and Settings carries the explicit card-panel composition. Flagged because it's a judgement call a reviewer should eyeball.
- **Form element-idiom (pill `<Button>` + pill inputs) deferred to a focused follow-up.** #261's ADR anticipated #290 also consuming `<Button>` for action buttons, but #290's own "Do" list scopes to *card/panel composition + font-display headings + structural utilities*, and the button question is coupled to inputs: the theme's pill button (`rounded-full`) is paired with a pill input idiom (the auth surfaces make **both** `rounded-full`). There is no shared `<Input>` component yet, and pillifying inputs across the dense admin forms (Users, Outgoing email) is out of #290's stated scope. Converting **only** the buttons would leave pill buttons beside square (`rounded-lg`) inputs in every form — an internally inconsistent half-conversion that reads worse than today's uniform rounded controls. So all action buttons/inputs stay on the current token-based `rounded-lg` shape for this slice; unifying them onto the pill idiom (buttons + an `<Input>` component together) is the natural next step and wants a visual pass.
- **Pre-existing non-olive accents left untouched (surgical scope).** The Activity "OK" result badge still uses `bg-emerald-500/10 text-emerald-600` and the failed-tile tint uses `amber-*` — both predate this change and are unrelated to the composition. They weren't introduced here and weren't swept (out of scope for a composition pass); the `success`/`warning` tokens exist if a future sweep wants them.
- **Verification is `make check-web` (typecheck + production build) — green.** A live full-stack visual pass wasn't run in this environment (the signed-in Home/Settings sit behind the authenticated SPA, and no browser-automation lib is available to drive it headlessly). The change is presentational and built on the already-proven `<Heading>` idiom and semantic tokens, but the two judgement calls above (open launcher grids; the calmer heading/panel scale) are worth a reviewer's eyeball.

## What's next

1. **Form element-idiom rollout** — a shared `<Input>` (pill) idiom, then move Home/Settings action buttons to `<Button>` and inputs to the pill input together, so the control layer matches the auth surfaces without a transitional button/input shape mismatch.
2. **Dark mode** — still the deferred downstream decision from #260.
15 changes: 8 additions & 7 deletions web-ui/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useAuth } from "../auth";
import { useHealth } from "../useHealth";
import { relativeTime } from "../utils";
import AppTile from "../components/AppTile.vue";
import Heading from "@/components/ui/Heading.vue";

const { currentUser, singleUserMode } = useAuth();

Expand All @@ -42,13 +43,13 @@ const empty = computed(() => household.value.length === 0 && yours.value.length
<!-- Active health issues (HEALTH.md # Display, the inline issues list). The
degraded-mode banner links here; shows the full active set, warnings
included. Admin-only (the query is gated), so members never see it. -->
<section v-if="activeIssues.length" id="health-issues" class="space-y-3">
<h2 class="text-xs font-medium uppercase tracking-wide text-muted-foreground">System health</h2>
<section v-if="activeIssues.length" id="health-issues" class="space-y-4">
<Heading :level="2">System health</Heading>
<ul class="space-y-2">
<li
v-for="i in activeIssues"
:key="`${i.id} ${i.instance_key ?? ''}`"
class="flex items-start gap-3 rounded-xl border border-border bg-card px-4 py-3"
class="flex items-start gap-3 rounded-2xl border border-border bg-card p-5"
>
<span class="health-dot" :data-sev="i.severity" aria-hidden="true"></span>
<div class="min-w-0 flex-1">
Expand Down Expand Up @@ -82,15 +83,15 @@ const empty = computed(() => household.value.length === 0 && yours.value.length
</div>

<template v-else>
<section v-if="household.length" class="space-y-3">
<h2 v-if="!singleUserMode" class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Household</h2>
<section v-if="household.length" class="space-y-4">
<Heading v-if="!singleUserMode" :level="2">Household</Heading>
<div class="grid grid-cols-2 gap-x-6 gap-y-8 sm:grid-cols-4 lg:grid-cols-6">
<AppTile v-for="a in household" :key="a.id" :instance="a" />
</div>
</section>

<section v-if="yours.length" class="space-y-3">
<h2 v-if="!singleUserMode" class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Yours</h2>
<section v-if="yours.length" class="space-y-4">
<Heading v-if="!singleUserMode" :level="2">Yours</Heading>
<div class="grid grid-cols-2 gap-x-6 gap-y-8 sm:grid-cols-4 lg:grid-cols-6">
<AppTile v-for="a in yours" :key="a.id" :instance="a" />
</div>
Expand Down
7 changes: 4 additions & 3 deletions web-ui/src/views/settings/AboutSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
// genuinely known (product identity + project link) rather than fabricating
// system facts. When the brain exposes a build/version + box-name surface, this
// section grows to show it.
import Heading from "@/components/ui/Heading.vue";
</script>

<template>
<section class="space-y-3">
<h2 class="text-xs font-medium uppercase tracking-wide text-muted-foreground">About</h2>
<div class="space-y-2 rounded-xl border border-border bg-card px-4 py-4">
<section class="space-y-4">
<Heading :level="2">About</Heading>
<div class="space-y-2 rounded-2xl border border-border bg-card p-5">
<div class="text-base font-medium">malmo</div>
<p class="text-sm text-muted-foreground">
A home-server OS for people who want to own their data — not become sysadmins.
Expand Down
7 changes: 4 additions & 3 deletions web-ui/src/views/settings/AccountSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { ref } from "vue";
import { api, type ApiError } from "@/api";
import { changeMyPassword, useAuth } from "@/auth";
import Heading from "@/components/ui/Heading.vue";

const { currentUser } = useAuth();

Expand Down Expand Up @@ -41,9 +42,9 @@ function cancelPwChange() {
</script>

<template>
<section class="space-y-3">
<h2 class="text-xs font-medium uppercase tracking-wide text-muted-foreground">Account</h2>
<div class="space-y-3 rounded-xl border border-border bg-card px-4 py-3">
<section class="space-y-4">
<Heading :level="2">Account</Heading>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Long Account Row Overflows

The wider p-5 card leaves less space for the non-wrapping account row below it. On a narrow phone, a long username plus the fixed Change password button can overflow the card because that row uses flex items-center justify-between without wrapping or truncation, leaving the password action outside the visible panel.

<div class="space-y-3 rounded-2xl border border-border bg-card p-5">
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
<div class="text-sm font-medium">{{ currentUser?.username }}</div>
Expand Down
Loading