Skip to content

Overhaul first installation and admin editors - #170

Merged
windischb merged 34 commits into
developfrom
feat/appnote-compact-notes
Aug 1, 2026
Merged

Overhaul first installation and admin editors#170
windischb merged 34 commits into
developfrom
feat/appnote-compact-notes

Conversation

@windischb

Copy link
Copy Markdown
Contributor

Summary

  • redesign the first-installation flow so a fresh instance starts without an implicit system realm and creates the first realm plus real:admin explicitly
  • strengthen global-versus-realm data isolation, tenant context handling, recovery CLI behavior, and realm provisioning
  • add realm-admin invitations and API-driven installation support for automated environments
  • redesign and align the admin-area editors and modals, including realm settings, realms, users, groups, roles, applications, OAuth scopes/APIs/clients, and service accounts
  • update the related operator, API, realm, OAuth, and first-installation documentation

Verification

  • 1,452 unit tests passed
  • 575 API integration tests passed
  • frontend Vue type-check passed
  • frontend production build passed
  • documentation builds passed

windischb and others added 30 commits July 23, 2026 09:38
Introduce a local AppNote component: a single-line advisory strip (13px,
variant tint + left accent, semantic icon) with an optional Details
popover for the long text and an optional CTA slot. This replaces the
tall CoarNote block that dominated modals and settings panels.

- New src/components/AppNote.vue (uses existing --coar-* tokens, CoarPopover)
- Convert all 106 CoarNote usages across 29 views to AppNote
- Long explanations become short summary + #details popover; short
  status/error/success notes use :truncate="false" (full text, may wrap)
- Add German *Short i18n keys for the new summaries; fix pre-existing
  ${renamedCount} interpolation to the {count} form the lib expects

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate per-field helper paragraphs into CoarFormField's built-in hint
prop (info glyph on the label + hover popover), decluttering forms. Only
fields that have a label qualify — the hint affordance lives on the
label, so label-less checkbox rows keep their visible helper text.

Tab/section intros and dynamic status/error text stay visible. No copy
changes; text is relocated, not reworded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tabs

Two defects in Realm Settings:
- DCR reservedNames and both audit fields put their help text in
  <template #footer> / #help slots that CoarFormField never renders, so
  the guidance was invisible. Move it into the :hint prop.
- The 11-tab bar laid out in a single non-wrapping flex row, forcing the
  whole settings column past the viewport and clipping tabs plus the
  intro text. Let the tab row wrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app modal used MODAL_FULL (112rem / 90vh), sizing for the heaviest
user-app case but leaving read-only system apps (a few fields) stranded
in empty space and spanning nearly the whole viewport. A user-app
settings form plus the 3-column permission catalog fit MODAL_LG
(80rem / 82vh), which still gives the grid a definite height. The dense
client builder keeps the full frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
modalTitle returned an empty string for an existing user with no first/
last name and no acronym (e.g. the seeded admin), leaving the modal
header blank. Fall back to the username, then email.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The security tab rendered 2FA status with a bespoke .status-badge
(hardcoded colors), while the dashboard uses CoarTag. Switch to CoarTag
(success/warning/neutral) so status chips share one vocabulary, and drop
the now-unused custom styles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Boolean grid columns rendered three different ways: "Ja/Nein" text
(scopes, realms, clients, apis, login providers), a green check icon
(users), and a tag (scheduled jobs). Apply one rule by semantics:

- On/off status (Active/Enabled) → CoarTag (success/neutral) via a
  shared common.statusTag prefix, matching the dashboard and 2FA badge.
- Classification (IsSystem) → icon (lock), like the existing HasPassword
  icon; never a success-green check.

Adds common.statusTag.active/inactive to de.json and (finally) en.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the Modal & Form Contract (R2/R3), a relationship must behave the
same from both sides. UserDetails wrote direct-group add/remove
immediately on every dual-listbox change, while GroupDetails stages
members into Save — same relationship, two behaviors.

Stage the group selection into the form (stagedGroupIds) and commit the
diff as part of the single save(), guarded on groupsLoaded so a user
whose Groups tab was never opened is not stripped of all memberships.

Verified: moving a group fires no network call; closing without saving
discards the change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the Modal & Form Contract (R1), the app-settings "override" blocks
hid every field behind their checkbox, so a setting was unfindable
(Ctrl+F, scan, screen reader) and its effective value invisible until
you ticked override.

Load the realm defaults and keep the fields always visible: when a
section does not override, the fields render disabled and greyed showing
the inherited realm value (e.g. session idle/absolute days); ticking
override enables editing. A fieldBind() helper centralises the
override?staged:inherited + disabled wiring across all sections. Origin,
email-branding and posture have no realm equivalent, so they inherit-
display as empty/"(inherit from realm)".

build()/populate() semantics are unchanged — an untouched section still
emits null (inherit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contract rule 4 ("one modal plumbing") for the two profile modals.
ChangePasswordModal and MfaSetupModal were opened from ProfileView
through a hand-rolled Teleport plus a fixed-inset backdrop div, with
local visibility flags; MfaSetupModal committed through an in-body
<form> submit and a full-width button instead of the modal footer.

Both are now routed fragments on the /profile route (#change-password,
#mfa-setup) like every other modal in the app, so they are
URL-addressable and deep-linkable. MfaSetupModal commits through
ModalLayout :footer-button; a complete OTP still auto-submits via
@complete, which replaces the lost form-submit-on-Enter. Since a routed
modal reports back through the API rather than a return value,
ProfileView re-reads the MFA status when the fragment disappears.

Also adds useModalOverlay() for the remaining case a routed fragment
cannot cover: a modal opened from inside another modal. It opens a bare
overlay with the same preset the fragment parser uses. useDialog() is
wrong there — the CoarDialog shell draws its own header, close button
and card background, so a ModalLayout inside it renders as a visible
modal-in-modal.

The named modal sizes move to router/modal-sizes.ts so both consumers
(the route table and useModalOverlay call sites) share one definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Acute defects found while smoke-testing the modal plumbing. The full
rework of this modal is a separate piece of work.

Two of these were real i18n bugs, not cosmetics: the two options of the
access-token-format select and the dual-listbox group headings were
hardcoded string literals that never went through t(), and the keys
accessTokenType / accessTokenTypeHint were missing from de.json while
their inline fallbacks were written in German. Since en.json is empty by
design and English comes from those fallbacks, an English user was shown
German. All fallbacks are English now and the missing keys exist.

Layout: the modal ignored the form-layout contract entirely — a flat
flex column with ad-hoc <h4> headings, long help paragraphs flexed next
to them, and a dead deprecated width prop. It now uses the same
modal-form / form-section / modal-form-grid primitives as its siblings,
with field help in CoarFormField :hint and the two listbox explanations
as one-line AppNotes with a details popover.

The long scroll becomes three tabs (Basics / Scopes / Apps, same in
create and edit per rule 5) on a fixed panel frame, so switching tabs
never resizes the modal. Both dual-listboxes take their height from a
flex-section wrapper instead of from their content, so the lists are the
same size on either tab and whether they are empty or full.

In the owning modal, the credentials section had a "Issue credential"
button that silently cut off its own label: the row let it shrink and
the label is nowrap+overflow:hidden. The section also carried its own
heading style and stated the same thing twice, in the paragraph and
again in the empty-state box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The create modal offered six fields and hid everything else behind a
save: the whole tab bar was suppressed in create mode, so Direct Groups
and Security were unreachable, and the active flag was edit-only. An
admin had to create a bare user and then edit it — which is exactly what
contract rule 5 forbids ("same layout in create and edit; do not switch
navigation between modes"). The migration table wrongly recorded
UserDetails as compliant.

Create now carries the same tabs as edit. Only "Effektiv" drops out:
effective membership is derived and there is nothing to derive it from
yet. The single Save writes the profile, an optional initial password,
the active flag and the email-verified flag in the POST, then applies
group memberships and the per-user 2FA policy against the id from the
response — the two things that genuinely cannot exist before the user
does. The pinned body height now applies in create too, since a tabbed
create would otherwise resize between the short General tab and the
group picker.

The initial password needed no API work: CreateUserCommand has always
accepted one, the form simply never asked for it.

IsActive on create did need API work, and the interesting part is that
setting it on the ApplicationUser document is not enough. UserView takes
IsActive exclusively from UserActivatedEvent / UserDeactivatedEvent and
defaults it to true, so a document-only write left every projection-fed
surface — the list query, the admin grid — insisting the user was active
while the document disagreed. The create path now appends the same
UserDeactivatedEvent the update path appends. Covered by a test that
asserts the read-back, not just the create response, because that is the
half that was broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Modelled on the storage banner in Cocoar.Atlas. The distinction to AppNote
is SCOPE, not styling: a banner states something about everything below
it, so it is pinned directly under a header (main or modal), is
full-bleed, and there is at most one per scope. A note belongs to a
single field or section, is indented with a left accent bar, and several
may appear in one view. A banner a third of the way down a form would be
lying about its own reach; a note under the header is too quiet for a
statement about the whole surface.

ModalLayout gets a #banner slot BETWEEN the header and .modal-content —
inside the content region the banner would inherit the 20px padding (so
it would no longer be full-bleed) and would scroll out of view.

Copy shape follows the original: a two-word bold label, then the
consequence, then the remedy, with an optional trailing action leading to
where the state is fixed. Unlike AppNote the text wraps instead of
truncating — a statement about the whole surface must stay readable, so
there is no single-line + details-popover contract here.

Migrates the two places that already made modal-wide statements and made
them differently: the system-app warning in AppDetails (which sat BELOW
the tab bar, so it read as belonging to the active tab, and duplicated
the header's read-only tag) and the built-in login provider notice, whose
comment already called itself a banner. In AppDetails the roles are now
split: the header tag names the state, the banner gives the reason and
says what the surface is still good for.

Deliberately not touched: the 106 existing AppNotes. They are field- and
section-scoped and correct where they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nner

Measured against the Cocoar.Atlas banner the strip was reading far heavier
than its model: the -bold border tokens are near-black (#3f2503 for
warning) and were drawn top AND bottom, so a hairline box was framing the
text instead of a tinted wash sitting under the header, and the text was
neutral grey (#3f3f46) on a coloured patch instead of being tinted in the
banner's own hue.

Now: bottom rule only, in the -subtle border token, and both text and icon
in the hue. Note the foreground deliberately uses the -bold BORDER token,
not --coar-text-semantic-*-bold — the latter resolves to #ffffff for the
info variant and would be invisible on a light background.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntence

Migrating these to AppBanner dropped the short/long split they used to
have (systemHintShort plus a details popover) and put the LONG text on the
strip, so the system-app banner ran to three clauses over two lines.

A banner states one thing. The bold label already names WHAT this is, so
the text only has to carry the consequence — and the consequence is the
same for both, so both now say it in the same words. The dropped sentence
("changing the strings would break the RequiresPermission calls") is
rationale for developers, not something an admin can act on; it survives
in the code comment where it belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The system-app banner was `warning` and the built-in-login-provider banner
was `info` for the same statement, with different labels — inherited from
the two AppNotes they replaced, and carried over without being questioned.

Unified on `info`: read-only is a fact, not a hazard. The fields are
disabled and the backend rejects the write anyway, so nothing can go
wrong, and warning colours only keep their force while they stay reserved
for things that can actually bite.

Both now read through ONE pair of strings (common.systemManagedLabel /
common.systemManaged) instead of two parallel per-entity keys, so they
cannot drift apart again; the four now-unused keys are removed. "System"
as the label matches the vocabulary the lists already use — the login
provider grid badges it "System" and the app grid has a System column.

Also gives LoginProviderDetails :readonly, so it carries the same
read-only tag as every other read-only modal. Its block was hand-rolled
(Save hidden plus :disabled on fifteen fields), which worked but left the
title bar silent about it. Tag names the state, banner gives the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two components for one thing had already drifted once (the same statement
rendered `warning` in one modal and `info` in the next), so inline and
banner become one component with a `placement` prop. What differs is
REACH, and reach is carried by POSITION — pinned under a header means the
whole surface, in the flow means this section. Nothing else has to say it,
so the old 3px near-black left bar is gone; fill, tinted ink, icon, 13px
and the bold label are identical in both placements.

Dropped the "App" prefix: it read as if a notice were always app-wide,
which is exactly the distinction the component is trying to make.

`truncate` now defaults to OFF. Of the 105 call-sites, 82 were switching
truncation off — a default that three quarters of the callers opt out of
is the wrong default. The 23 that relied on clamping now say so.

Colour, reworked against the reference banner in Cocoar.Atlas. The
`-subtle` ground (L 0.92 / C 0.04) plus the near-black `-bold` border ink
(L 0.29 / C 0.06) read as grey text on a coloured patch; the reference is
a pale ground carrying mid-dark SATURATED ink, and that saturation is what
makes it "dark yellow" instead of "dark brown". The ground is now
lightened and desaturated RELATIVELY off the token — never a literal
lightness, because the tokens invert for dark mode — and the ink is a
straight swap to `--coar-background-semantic-*-bold` (#8f5300, a whisker
from the reference's #92400e). What remains is hue: the design system
seeds warning at #cc821f (H 67, amber-orange) where the reference sits at
H 95 (yellow). That is the system's warning identity, not this
component's, so it stays.

Dark mode lives in an unscoped block on purpose: it has to reach an
ancestor, and Vue's scoped compiler silently rewrites
`:global(.dark-mode) .notice--warning` down to bare `.dark-mode`, which
put one variant's ink on the root element for every notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iants

The ground derivation subtracted a fixed 0.018 from chroma. That reads
fine on warning (C 0.04) but info's seed is a slate at C 0.012, so the
channel went negative, clamped to zero, and every info notice rendered as
flat #f8f8f8 — no colour at all.

Absolute arithmetic on chroma always breaks whichever variant happens to
be the quietest. Lightening alone already produces the pale ground, so
chroma is now left exactly as the design system set it and each variant
keeps its own identity: info #f4f9ff, warning #fff4dd, error #fff0ed,
success #e8ffed.

Neutral's border came straight off the neutral ramp at #5a5a5a and drew a
hard box where every other variant draws a hairline; lightened to match.
That one does use an absolute lightness — its source is a grey with no
hue to derive from — so dark mode hands it back to the raw token, which
is already the light end of the ramp there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README duplicated the dev-loop commands under "Build it yourself"
and pointed only at first-time-setup for the admin bootstrap, so
developing-locally.md — the page that is actually kept in sync with the
code — was reachable from CONTRIBUTING and the docs nav but not from the
front door.

Adds it to Quick links directly under the Docker quickstart, since those
two are the same question answered differently (run the image vs run from
source), and marks the inline snippet as the short version so the two
copies cannot silently drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@windischb
windischb merged commit bc1c3ed into develop Aug 1, 2026
8 checks passed
@windischb
windischb deleted the feat/appnote-compact-notes branch August 1, 2026 00:17
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