Skip to content

Dashboard: say how open an app is, and fix the settings controls - #435

Merged
onel merged 3 commits into
devfrom
ui/settings-nav-app-link-public-badge
Sep 3, 2026
Merged

Dashboard: say how open an app is, and fix the settings controls#435
onel merged 3 commits into
devfrom
ui/settings-nav-app-link-public-badge

Conversation

@onel

@onel onel commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

What & why

Ad-hoc dashboard polish, done from a direct request rather than a tracked issue, so there is no Closes #N line below. Say the word and I will file one to attach it to.

Four things:

The Settings nav is grouped by who owns the thing. "You" is Account + Installed apps, since that list is the apps this user has, not a box-wide setting. "System" is Users, Outgoing email, Notifications, Activity, About, with Notifications directly above Activity because both answer "what happened on this box".

The per-app page writes out the app's URL as a link. The Open button is a one-click path, but it hides the address and only renders while the app runs, so there was no way to read, copy, or send an app's address from that page. It renders in every state and truncates rather than wrapping. There is no copy button on purpose: navigator.clipboard is unavailable on the HTTP-only .local origin, so right-click-copy on a real anchor is the path that always works.

Home tiles say how open the app is. A globe in the bottom corner of the logo: whole for exposure: public, the same globe at 30% opacity when the app is Only me but its manifest declares access.public_paths (#415) and so is partly open, nothing at all when it is closed end to end. Hosted-only, since the appliance has no public app subdomains and stores "public" for every row. The globe is one PNG used as a mask filled with currentColor (the source is black line art, which would vanish on a dark tile). The one brain change is that GET /api/v1/apps now runs withPublicPaths as well: a list DTO carrying the exposure without the paths draws a fully closed app that is in fact partly open. One instance-manifest read per app, best-effort, no schema change.

The settings controls that were the wrong element. Button.vue gained an as prop (a tag name or a component), which fixed three defects: Open was a hand-built pill on an <a> at px-4 py-2, a size larger than the size="sm" buttons beside it; both Add account call-to-actions wrapped a <Button> in a <RouterLink>, nesting a button inside an anchor (invalid HTML, two focus stops for one control); and the add form's Cancel was a <Button @click="router.push(…)">, so it had no middle-click, no ctrl-click, and no target on hover. Every other link under /settings was checked and is correct.

Two smaller ones ride along: the Access card now lists the manifest's open paths as mono badges, and the app's email-account picker is a listbox showing each account's provider logo instead of a native <select>. Both are ported from the Tailwind Plus mirror, with the palette mapped to malmo's tokens and the Headless UI primitives mapped to the reka-ui this project already ships.

Spec(s) touched

  • docs/specs/DASHBOARD.md — # Tile gains the public marker and its two forms; # Settings records the nav grouping, the app URL line, and the narrower rule for the access label (it says that some paths are open, and the card below lists them).
  • docs/dev/web-ui.mdButton.vue's as prop.
  • Progress entry: docs/progress/settings-nav-app-url-public-marker.md, indexed in docs/progress/README.md.

No DECISIONS.md entry: nothing locked flipped.

What was tested

  • vue-tsc clean, and make check-web (typecheck + production build) green before the last two edits.
  • Go side: gofmt, go vet ./internal/... ./cmd/..., make test-nopam and go test ./internal/api/ all green, make openapi-check fresh. make check does not run end to end on this machine and did not here: vet fails inside dev/cloud/mkosi.tools/, a gitignored 1.6 GB leftover from an old local cloud-image build, and msteinert/pam does not build on this box. Neither is in the diff and CI sees neither.
  • inset-ring / inset-ring-border and the mask's inlined data: URI were checked in the built CSS rather than assumed.
  • Both marker forms were rendered at 16 px and 64 px on light and dark tiles. That is what killed the first version, an eclipsed globe with a circle cut out of its right side: it only read as an eclipse at 64 px, and as a smudge at the size it actually ships.
  • The maintainer ran the hosted path in the inner loop (MALMO_PROFILE_PATH pointed at a marker holding hosted, which is the only way any of this is visible on a dev box) and looked at the result.

Known gaps & deviations

  • No test runner exists for web-ui, so none of this is covered by an automated test. The evidence is a typecheck, a build, and looking at it.
  • The globe is an Icons8 asset. Their free tier asks for a visible link back and the dashboard gives it none. Attribution, a licence, or a different icon before this ships.
  • The marker overlays the logo square. Safe for today's centered glyph icons, not for a future edge-to-edge one.
  • The Installed apps list still marks nothing; only the home tiles and the per-app page say how open an app is.
  • HomeView.vue's "Browse the Store" call to action is the same hand-built-pill defect the as prop exists to remove, and it is not even the same pill (rounded-lg, no hover state). Left alone: this pass was scoped to /settings.
  • The new listbox was exercised against the provider list a dev box serves, not against a real bound account on a hosted box.
  • The access label no longer names the open paths in its sentence; the card below lists them instead. That narrows a rule DASHBOARD.md wrote down for Hosted: path-scoped app exposure — keep the UI owner-only while a token-authed API stays public #415, so the spec was updated rather than left contradicted.

Definition of done

  • Behavior works in the inner loop (make dev).
  • Tests added at the right layer; make check green. No tests added (web-ui has no runner; the brain change is a one-line reuse of an existing helper covered by the api suite). make check blocked on this machine as described above; make check-web and the Go suite are green.
  • No test deleted or newly skipped: the diff touches no *_test.go.
  • Progress entry written; both READMEs updated.
  • Catalog change: none.
  • Catalog change, per-app field: none.
  • Spec doc updated; no locked decision flipped.
  • No section-sign symbol, no hard-wrapped markdown, log/slog only.
  • Branch off dev, PR into dev.

Three surfaces on one branch.

Settings nav regrouped: "You" is Account + Installed apps (the apps
this user has, not a box-wide setting), "System" is Users, Outgoing
email, Notifications, Activity, About. Notifications sits above
Activity because both answer "what happened on this box".

The per-app page writes out the app's URL as a link. The Open button
hides the address and only exists while the app runs, so there was no
way to read or copy it. No copy button: navigator.clipboard is
unavailable on the HTTP-only .local origin, so a real anchor is the
path that always works.

Home tiles carry a globe when the app is open: whole for public, the
same globe at 30% opacity when the app is Only me but its manifest
declares public paths (#415), nothing when it is closed. Hosted-only.
The globe is one PNG used as a mask filled with currentColor, so it
survives a dark tile. GET /api/v1/apps now carries public_paths too,
because a list DTO with the exposure and not the paths draws a fully
closed app that is in fact partly open. No schema change.

The Access card lists those paths as badges, and the app's email
account is now a listbox showing each provider's logo instead of a
native select. Both ported from the Tailwind Plus mirror, with the
palette on malmo's tokens and Headless UI mapped to reka-ui.

Button.vue gained an `as` prop, which fixed three defects: Open was a
hand-built pill on an <a>, a size bigger than the buttons beside it;
both Add account links wrapped a <Button> in a <RouterLink>, nesting a
button inside an anchor; and the add form's Cancel was a button that
navigated. Every other link under /settings was checked.
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
internal/api/api.go Enriches app-list DTOs with manifest public paths so tiles can distinguish partly public apps.
web-ui/src/views/settings/InstalledAppDetailSection.vue Adds app-address and public-path displays and safely fixes both previously reported mail-picker loading and failure states.
web-ui/src/components/AppTile.vue Adds hosted-only full and partial public-access markers based on exposure and public paths.
web-ui/src/components/ui/Button.vue Adds an as prop so shared button styling can be applied to semantic links.
web-ui/src/views/settings/SettingsLayout.vue Regroups Settings navigation according to user and system ownership.

Reviews (3): Last reviewed commit: "Mail picker: open only once the account ..." | Re-trigger Greptile

Comment thread web-ui/src/views/settings/InstalledAppDetailSection.vue Outdated
Greptile caught this on #435. The app's binding comes from
GET /apps/{id}, the list that names the account from a separate
mail-provider-options query. In the window before the second lands,
the new trigger fell back to "None (email features off)" — a definite
claim, and the opposite of the truth for a bound app. Acting on it
costs a rebind and an app restart.

The label now says "Loading…" while the list is in flight and
"Unknown account" if it lands without the bound id. The trigger is
disabled while loading too: otherwise the menu opens on an empty list
where the only choice on offer is "unbind", which is the same trap one
click deeper.

Also make the Greptile check part of the review loop. code-review.md
already said to read prior AI-reviewer comments on an open PR, but not
that Greptile posts minutes after the PR opens — so an agent review
that runs immediately sees an empty comment list and reports clean,
which is what happened here.
Comment thread web-ui/src/views/settings/InstalledAppDetailSection.vue Outdated
Second Greptile pass on #435. The loading fix gated on "not loading",
which a failed request also satisfies: the query errors, the list stays
empty, and the picker is enabled over a menu whose only entry is None.
One click then unbinds a bound app and restarts it.

Gate on isSuccess instead, so the picker opens only when a real list is
behind it, and say "Account list unavailable" rather than a number of
accounts we do not have.
@onel
onel merged commit 67ddba0 into dev Sep 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant