Commit Graph

157 Commits

Author SHA1 Message Date
aminovfariz
1b615a5e1c fix: update local docker and php config 2026-05-26 11:04:57 +02:00
fs
3742ad19c2 chore: enforce nginx-only setup and remove .htaccess 2026-04-26 09:50:40 +02:00
fs
3207d71244 harden(logout): enforce POST+CSRF and remove GET logout triggers 2026-04-25 23:35:03 +02:00
fs
e775ed2800 refactor(login): Stripe-style split layout + multi-step polish
Reworks the auth flow from a single centered card into a two-pane
layout — form on the left, tenant brand on the right — and tightens the
multi-step login UX along the way. Major changes:

LAYOUT
- templates/login.phtml splits into a flex-column body so the footer
  spans both panes at the bottom instead of getting clipped under main.
- New .login-form-pane and .login-brand-pane on a 1fr/1fr grid above
  768px; mobile stacks brand on top as a slim band, form below.
- Brand pane carries a soft halo + dot grid + tinted base, all derived
  from --app-primary so it tracks the tenant accent automatically.
- Login card gets a Stripe-style hairline border + soft shadow, no
  Pico article > header sectioning band, h1 in semibold + tracking-tight.
- The "body > main" global padding is overridden for login so the brand
  panel reaches the very top + bottom edges of its column.

OS THEME FALLBACK
- New appExplicitTheme() returns the user/tenant theme or empty string,
  used by login.phtml + error.phtml to OMIT data-theme entirely when no
  preference exists. CSS prefers-color-scheme media query then drives
  the theme — DB stays the source of truth, no browser-side persistence.

MULTI-STEP UX
- Heading is stage-aware: "Login" / "Select tenant" / "Login to {tenant}".
  Drops the redundant "Login credentials" subtitle.
- Stage 3 gets an identity pill (icon + email + compact "use different
  email" button) replacing the old tenant-context block, so the user
  always sees which account they're signing in with regardless of
  multi-tenant status.
- Stage 2 tenant selection drops avatars + initials — just radio + name
  with text-overflow ellipsis for long tenant names.
- Tightens primary CTA: full-width on every stage incl. <p>-wrapped
  buttons. autofocus moves to the right input per stage (ldap_username
  / password).

NOTICE / HELP LINKS
- The placeholder "Problems logging in" link is gone (it used to point
  at the imprint route — misleading). show_support wired through 6
  auth pages and the partial removed; architecture tests adjusted.
- Help-links centered with bullet separators between items, hairline
  border-top so they read as secondary navigation under the main CTA.
- The "Encrypted / HTTPS/TLS 1.2+" trust badge at the card bottom is
  removed — modern users assume HTTPS, and the badge added noise.

DEAD CODE
- $authLogoHref, $selectedTenantAvatarUrl, $selectedTenantInitial,
  $hasSelectedTenantAvatar, $canSwitchTenant — unused after the
  identity-pill / brand-pane move, removed from all 6 auth pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:16:26 +02:00
fs
4890a280fb refactor(ui): unify notice + toast styling, polish inline notices
Notices used to be styled only inside the toast stack — every inline
.notice on the login page, auth pages and admin edit screens fell back
to browser defaults and looked unstyled. The Stripe-style toast redesign
(icon pill + neutral text + soft card surface) now lives on the base
.notice rule, and .app-toast-stack adds the slide-in animation, soft
shadow, dismiss button and progress bar on top.

Inline notices auto-render the variant icon via a ::before pseudo using
the Bootstrap Icons codepoints, so all 30+ existing call sites get the
new look without markup changes. Toasts opt out of ::before via
:has(> .notice-icon) and keep their explicit icon span.

The previous app-flash.phtml partial is folded into app-toast-stack.phtml
(both create the same .app-toast-stack container — having both mounted
made two stacks fight for the same fixed corner). default/login/page
templates now mount the unified partial; the architecture contract is
updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:32:09 +02:00
fs
2429588256 refactor(ui): move toast stack chrome to a server-rendered partial
Mirrors the detail-drawer move: the .app-toast-stack container is no
longer lazy-created in JS on first toast, instead it lives in
templates/partials/app-toast-stack.phtml and is mounted once in
default.phtml alongside the confirm dialog. The aria-live attribute now
sits in the SSR markup so screen readers register the live region from
page load, not from the first toast.

getToastStack() drops to a plain querySelector and warns via warnOnce
when the partial is missing instead of constructing a fallback container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:13:55 +02:00
fs
4d1bd13a3b refactor(ui): move detail drawer chrome to a server-rendered partial
The drawer chrome used to be built in JS via a 50-line innerHTML string
and needed every caller to plumb through a labels object sourced from
per-page PHP config. That diverged from the codebase convention — the
confirm dialog, search dialog and session warning are all PHP partials
mounted once in templates/default.phtml. The drawer now follows the
same pattern: templates/partials/app-detail-drawer.phtml renders the
markup with t() labels, default.phtml mounts it inside the logged-in
shell, and the JS only attaches behavior via the [data-detail-drawer]
selector.

Knock-on cleanup: ensureDrawerElement and resolveLabels disappear from
the JS, all three initDetailDrawer callers (admin/users, address book,
helpdesk debitor) drop their labels parameter, and the matching dead
'drawerClose'/'drawerPrev'/… entries leave the per-page PHP grid configs.
The drawer also gains a clean fallback when the partial is missing
(console warn + null return), so logged-out edges can't crash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:06:06 +02:00
fs
b205ba1312 refactor(sidebar): drop per-group rainbow, add subtle active background
Removes the nine hardcoded HSL overrides that gave each admin and
helpdesk nav group its own icon color and standardises on the tenant
accent (--app-primary) for every group, since the variable already had
that as its fallback. The active link state now also gets a subtle
background tint via a new --app-sidebar-active-bg token (12% accent
mixed with transparent) so the selected entry reads as more than just a
border-left and color shift; hover keeps a lighter 6% tint so it stays
distinct from active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:05:55 +02:00
fs
015d911ef8 fix(ui): give <small> a deterministic font-size so hints stay consistent
The base small rule set --app-font-size: 0.875em but never consumed it,
so <small> elements fell back to the browser default ("smaller", roughly
0.83em of the parent). That made hints under fields visually drift
depending on the parent context — most noticeably on the telemetry page
where the muted hint, the per-field hint inside a label, and the caption
under a fieldset all rendered slightly different sizes.

The codebase already standardises on var(--text-xs) wherever a small was
explicitly styled (gridjs, footer, docs); this commit just makes the
default match that convention so every bare <small> is 12px regardless
of where it sits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:22:34 +02:00
fs
9ee2589820 refactor(settings/telemetry): single card with full sub-config disclosure
Collapses the two telemetry cards (Frontend telemetry + Advanced) into
one card so the master toggle and its sub-config (sampling + allowed
events) read as one coherent block. The conditional disclosure now
hides the entire sub-config when telemetry is off — previously only the
sampling fieldset hid, leaving Advanced visible with no effect.

The sampling select sits in a 2-column grid with an empty filler so it
keeps a sensible width instead of stretching across the page. Both
redundant info blockquotes are gone (the master switch carries a muted
hint, and the events block has its own caption). The simplified
component drops the now-redundant samplingRowSelector and consumes a
single data-telemetry-when-enabled wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:22:23 +02:00
fs
5f3aff08cb refactor(settings/audit): conditional retention disclosure + flat layout
Drops the wrapping details-card so the audit toggle and retention input
read like the email page. The audit switch now uses role="switch" and a
new app-settings-audit component (built on the existing
createConditionalToggleInit primitive) hides the retention block when
audit is disabled — the input is meaningless without audit on, and the
control state syncs automatically on toggle. The redundant info
blockquote and the toggle's paraphrased DB description are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:50:04 +02:00
fs
fdbf30379c feat(ui): tone-based icon palette for app-tile, dark-mode aware
Adds an iconTone API (blue, violet, red, orange, amber, emerald, cyan,
pink, green, neutral) to appTile() that derives both icon background and
color from a single hue via color-mix(). Light mode keeps the pastel-pill
look; dark mode picks subtle dark-tinted backgrounds with bright accent
icons so tiles read clearly on the dark background.

The existing iconBg/iconColor escape hatch stays for callers that need a
custom hex (admin/stats); the nine settings tiles migrate to iconTone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:11:35 +02:00
fs
dd5c305f7e fix: not opening in target blank 2026-04-24 22:43:37 +02:00
fs
50851c0e9a manual commit 2026-04-24 21:53:14 +02:00
fs
874a90d8f8 refactor(admin-lists): slim tenant + user grids, Stripe-style pagination
Tenant list:
- Drop the avatar/logo column — the list now shows only Tenant name
  and user count. Also drop the logo-hasLogo server lookup and the
  initials/placeholder markup.

User list (13 → 4 columns, Stripe pattern):
- User (compound: avatar + Name + email subtitle, click opens drawer)
- Tenants (badges, primary tooltip)
- Last login (relative badge)
- State (active/inactive badge)
- UUID hidden at the last column for cells[uuidIndex].data
- Everything else (departments, roles, phone, mobile, short_dial,
  created, modified) lives in the detail drawer.
- Document the off-by-one gotcha: with row selection enabled gridjs
  prepends a checkbox cell, so runtime cells[] are shifted by +1;
  uuidIndex is 5 (column position 4 + selection offset).
- New .grid-user-profile css mirrors .grid-tenant-profile (avatar +
  stacked name/email) with ellipsis and primary-color hover affordance.

Pagination (all Grid.js lists):
- Non-current page buttons now use the neutral-chip secondary-outline
  tokens (--app-button-neutral-*) plus the raised box-shadow — matches
  the rest of the button system in both light and dark themes.
- Current page stays distinctly primary-filled with the filled-shadow;
  focus-visible combines the chip shadow with the primary focus ring.
- Disabled pager buttons remain neutral but lose the lift shadow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:52:44 +02:00
fs
fa86009e3f feat(ui): copyable input partial + dynamic copy-target
- New core partial templates/partials/app-input-copy.phtml renders a
  standard label + input with a compact copy button overlaid on the
  right edge of the input. Used for privacy/imprint URLs on the tenant
  form; reusable for any field where a one-click copy is helpful.
- Extend the shared copy-field component (web/js/components/app-copy-field.js)
  with data-copy-target support — the button now reads the target input's
  .value at click-time, so users can edit a field and still copy the
  current value. Static data-copy-value keeps working unchanged.
- New component CSS web/css/components/app-input-copy.css positions the
  button absolute/inset + margin-block:auto (robust vertical centering
  regardless of input height) and uses a descendant selector (0,2,0)
  so the button wins over the global [data-tooltip] position:relative.
- Also register app-input-copy.css + app-tenant-logo.css in core.css
  @import list — they were only in the shared asset group before, which
  default-template admin pages don't load, so tenant-logo styles were
  effectively missing on admin tenant edit (topbar size etc.).
- Document the Copy-to-Clipboard + Copyable Input standards in
  docs/reference-frontend-javascript.md so future consumers don't
  re-invent the markup/selectors.
- File-upload preview: pending-file block restructured to a compact
  list-item row (small square thumb + filename/size stack + clear X)
  and removed the transparency checker pattern on the current-image
  preview in favour of a flat --app-preview-bg surface.
- Tenant edit page title + breadcrumb now show the tenant description
  ("Acme GmbH") instead of the generic "Mandant bearbeiten" when one
  is present.
- i18n: add "Copy to clipboard" / "In Zwischenablage kopieren" across
  both locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:26:11 +02:00
fs
e814b5fd11 refactor(file-upload): SSR-first state, click-to-replace, cleaner preview
- Server renders initial file-upload state (src, alt, label text,
  has-current class) so there is no flash of broken-image icon while
  JS boots. JS no longer duplicates that work — it only reacts to
  user input (select, clear, drag-and-drop) and uses data-current-src
  solely to restore the server file after a blob preview.
- Clicking the current preview image now opens the file dialog, mirrors
  the Replace button, with hover affordance (cursor + primary border).
- Drop the transparency checker pattern in the preview — the admin UI
  does not need Figma-style transparency semantics. Preview is now a
  flat theme-aware surface (--app-preview-bg) that keeps white or
  black logos visible against a neutral gray.
- Move tenant favicon out of the aside into the Master-data tab as its
  own details block next to the Tenant-logos section. Uses the same
  barrier-form pattern (HTML5 form attribute + app-file-upload.phtml)
  for a consistent instant-upload UX.
- Tenant-logo slot label is a native <label> element — picks up the
  global form-label typography and spacing, no custom muted-color
  class needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:53:58 +02:00
fs
6e3fc63c1d feat(tenant): per-theme logos + file-upload + button UI polish
Replace the single tenant avatar with a pair of theme-scoped brand logos.
Render only the theme-matching <img> server-side and swap src on theme
toggle via a JS hook — no reload, no double request, no CSS tricks.

Tenant logos
- TenantLogoService (ImageUploadTrait) with theme whitelist and per-theme
  storage storage/tenants/{uuid}/logo/{light|dark}/, SIZES 128/256/512
- Public serving endpoint auth/tenant-logo-file so login can show the
  logo pre-auth; matching authenticated admin preview endpoint
- appTenantLogoUrl(?size, ?theme) with 4-step fallback cascade; PDF +
  mail always request 'light'
- Admin tenant edit: avatar block replaced by "Tenant logos" details
  block inside the Master-data tab, two side-by-side slots via Pico
  .grid with the core app-file-upload partial
- Policy rename ABILITY_ADMIN_TENANTS_AVATAR_VIEW -> LOGO_VIEW, action
  routes logo / logo-delete / logo-file with theme body/query param
- API endpoint path kept (backward compat), internals on new service
- CLI tenant:logo-migrate-avatars moves legacy avatar/ -> logo/light/
  idempotently (--dry-run, --yes, --cleanup)
- i18n "Tenant image" removed, 12 new keys synced across de/en

File upload component
- Full-width preview + filename/actions below (3D stack layout)
- Fixed 16:9 aspect ratio with 1rem inner padding for consistent
  preview size across any logo aspect
- Transparency checker pattern as background so black logos stay
  visible on dark mode and white logos on light mode
- form="" + deleteFormId support so the partial works with barrier
  forms inside another form

Buttons
- width:100% dropped from button[type="submit"]; scoped back via
  .login-main for the auth-flow primary CTA
- .outline base rule now tints background via color-mix of --app-color
  so secondary/primary/danger outlines all gain a subtle surface
- .outline.secondary restyled Stripe-style in both themes: solid white
  chip with soft shadow in light, solid elevated dark chip with white
  text in dark; neutral border replaces role-colored border
- .app-action-success/.app-action-danger outlines get color-mix bg +
  theme-aware outline-text tokens for stronger contrast
- Filled .primary/.app-action-success/.app-action-danger get raised
  box-shadow (inset highlight + drop) — opt-in via class so chrome
  buttons stay flat
- Dropped the legacy .secondary utility that was clobbering the
  custom-property cascade with a hardcoded muted color

Theme swap
- Logo img carries data-src-light + data-src-dark; theme-toggle JS
  swaps src when data-theme changes, keeping the topbar/login logo in
  sync without a page reload

Quality gates: PHPUnit (2045), PHPStan L5, CS-Fixer, docs link/drift,
codex skills sync — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:25:53 +02:00
fs
a05a628968 refactor(css): remove animated login gradient
Strip the heavy animated multi-layer radial gradient from the login
page. Removes:
- ani-gradient keyframes and 40 @property declarations
- Light and dark theme gradient rules
- Empty #gradient div from login template
- prefers-reduced-motion override that only served the gradient

The login page now uses the plain app background color consistently.
2026-04-24 19:00:38 +02:00
fs
5e705f780d refactor(css): overhaul dark mode palette to deep neutral tones
Align dark mode surfaces with a cooler, deeper aesthetic inspired by
Claude's dark theme:
- Background: #1f1f1e (was rgb(19,22.5,30.5))
- Sidebar: #171716 (separate from main background)
- Cards/Dropdowns: #242422 (was #181c25)
- Borders: #2f2f2d / #383835 (was blue-tinted grays)
- Text: neutralized grays (#c8c8c6, #6e6e6c etc.)
- Box-shadows: pure black rgba (was blue-tinted)

Also updates the self-contained error-debug page fallback tokens.

Both explicit [data-theme=dark] and prefers-color-scheme blocks are
kept in sync. High-contrast dark overrides remain untouched.
2026-04-24 18:54:39 +02:00
fs
87652e55da refactor(system-info): reduce to Stripe-style status page
Reduce admin/system-info to a single focused status view: overall
status banner, components list from existing health checks, and an
environment meta table. Drops the Modules and Permissions tabs —
those were dev-diagnostics already available via CLI.

- Remove SystemInfoService + its test; wire the action directly to
  SystemHealthService and build meta inline
- Extend SystemHealthService with per-check label + description so
  the UI speaks in customer terms while the CLI doctor path stays
  compatible
- Rebuild the view on existing app-stats-table + app-empty-state
  primitives; add a small app-status-banner component for the
  headline signal
- Align help-center nav label and i18n keys (de/en)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:32:46 +02:00
fs
0002c07755 refactor(theme): drop dark-green and collapse theme registry to light/dark
Remove the extensible theme catalog (config/themes.php + ThemeConfigGateway)
in favor of a two-entry const on SettingsAppGateway. appThemes() now returns
the list directly — no container lookup, no file include. Drop the
dark-green theme assets, narrow [data-theme^="dark"] selectors to
[data-theme="dark"], and tighten isDarkTheme() to an exact match. Ship an
idempotent migration that corrects any leftover 'dark-green' rows on users
and tenants to safe defaults (light / NULL).

Tenant scoping (default_theme, allow_user_theme) and per-user override stay
intact; only the catalog extensibility and the third theme are removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:15:10 +02:00
fs
b2982bdac7 feat(ui): token-select primitive for large multi-selects
Adds tokenSelectForm() in core/Support/helpers/ui.php: an inline
typeahead combobox + flat alphabetical removable list, designed for
selections that outgrow the chip-header of the vendor MultiSelect
(roles, permissions, and similar admin pickers).

Contract:
- Hidden <select multiple name="<name>[]"> is the form submission source —
  consumers read via $request->body() verbatim, no parsing
- Items are ['id' => int, <labelKey> => string, 'key' => string?]
  where labelKeys default to ['description', 'label', 'name']; 'key' is
  an invisible fuzzy-match hint
- $labelOverrides lets callers swap emptyState / removeTooltip /
  noMatches / clearAll / countSuffix / errorMessage with domain copy
- $disabled renders pure-presentation list (no combobox, no remove)

Runtime:
- initTokenSelect in web/js/components/app-token-select.js is registered
  as 'token-select' in app-init.js; destroy()/cleanupFns contract
- Syncs the hidden <select> on every mutation and dispatches 'change'
  so dependent UI can react

Wired up: pages/admin/permissions/_form.phtml (assigned roles),
pages/admin/roles/_form.phtml (permissions + assignable roles).
Helper contract lives in tests/Support/Helpers/TokenSelectFormHelperTest.php;
runtime contract + entrypoint registration + host usage are enforced by
FrontendRuntime*ContractTest.php.

Coexists with multiSelectForm() — pick tokenSelectForm() when the
selected set can grow beyond ~10 items or typeahead is expected.
Docs in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:52:32 +02:00
fs
cfba3f40ed feat(responsive): mobile polish — topbar, drawer, footer, tabs, notification dropdown
Unifies the breakpoint system and fixes cross-layer CSS bugs that kept
the old mobile hide rules from ever taking effect.

Breakpoints:
- Document canonical xs/sm/md/lg/xl/2xl scale in variables.base.css
- Replace 968px one-offs with 1024px (app-details, app-page-editor)
- Make details tab-panel max-width responsive (min(70ch, 100%))

Topbar (< md):
- Hide breadcrumb in components layer so cross-layer overrides actually win
- Dock search icon alongside right-column icons via flex layout
- Add xs (400px) tightening for hit-area preservation
- Hide brand divider alongside hidden brand name
- Switch hover states to theme-aware --app-dropdown-hover-background-color

Mobile drawer:
- Icon-bar now vertical like desktop (was horizontal top strip, effectively invisible)
- Sidebar shifted 52px right so icon-bar and sidebar slide in as one 280px unit
- Add breathing room above first nav item (20px top padding)

List tabs:
- Rewrite .app-list-tabs to horizontal scroll with hidden scrollbar, snap,
  and mask-image gradient fades; add initListTabs to scroll active into view
- Register as runtime component in app-init.js

List toolbar:
- Stack wide controls (multi-select, search/text inputs) full-width below sm,
  keep compact controls at natural width

Footer:
- New dedicated app-footer.css with mobile-first stacking, border-top separator,
  theme-aware hover; remove redundant rules from app-shell.css

Notification dropdown:
- Width with min(360px, 100vw - 2rem) and min-width: 0 override global
  details > ul rule; switch to position: fixed below md so it hugs the
  viewport right edge instead of the non-rightmost bell button

Content spacing:
- Add padding-block-start to .app-main-content on mobile (was 0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:50:59 +02:00
fs
68453fd6ed refactor(tenants): visibility form requires explicit appearance values
Simplifies the tenant Visibility tab to match the tenant-only appearance
model established by the previous commits:

- primary_color: now required. Removed the "No brand color / Use system
  default (no brand accent)" toggle — every tenant carries a concrete
  primary color. Legacy NULL rows render the neutral app default (#2fa4a4)
  in the color picker and are converted to that explicit hex on save.
- default_theme: the select no longer offers a "Use system default" empty
  option. Legacy NULL rows resolve to 'light' at render time; saves always
  persist a valid theme via SettingsAppGateway::normalizeTheme().
- allow_user_theme: replaced the tri-state "Use system default (allowed) /
  Force allow / Force disallow" select with a single boolean switch
  ("Users may choose their own theme"). Legacy NULL rows load as checked.
  Saves persist 0/1 explicitly.

TenantService: sanitize no longer reads primary_color_use_default or
allow_user_theme_mode; it validates primary_color as a required hex and
treats allow_user_theme as a plain boolean. Both create and update paths
write concrete values only — no more NULL writes for these three fields.

DirectorySettingsGateway gains a normalizeTheme() delegate so TenantService
can route through the same gateway it uses for isAllowedTheme().

Removed now-unused app-color-default-toggle JS component + its runtime
registration + its architecture-test entry. i18n cleanup: "No brand
color", "Use system default (no brand accent)", "When enabled the tenant
renders without a brand accent color.", "Use system default (light)",
"Use system default (allowed)", "Force allow user theme", "Force disallow
user theme", "User theme policy is invalid" all removed. New copy: "Users
may choose their own theme" + helper text, plus a tightened tab blockquote.

Tests: TenantServiceTest validInput() updated to send concrete values;
settingsGateway mock gets normalizeTheme() + isAllowedTheme() defaults.
All 1985 tests pass; PHPStan level 5 clean; QG-006 clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:10:44 +02:00
fs
4a8b9ab64d feat(drawer): open-full action as secondary outline button with text + icon
Converts the "Open full page" control in the detail drawer header from an
icon-only link into a text+icon button using the standard
`secondary outline small` classes. Because the base button rule in
app-shell.css only targets `button, [type=…], [role=button]`, the anchor
gets `role="button"` so the core cascade applies (border-radius, padding,
hover/focus states). Component-local CSS is trimmed to just the flex
layout (icon + label with gap) and text-decoration reset — border, radius
and colors come from the shared button base.

Tooltip is removed (redundant with visible text); aria-label remains for
screen readers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:41:12 +02:00
fs
bd10e07f37 feat(addressbook): Stripe-style detail profile — meta chips, always-visible contact stack, copy affordance
Refactors the address-book detail partial (templates/partials/app-user-profile.phtml)
toward a Stripe/Linear aesthetic: the banner + avatar still carry identity,
but the header now also carries meta chips (primary tenant, department,
hire date) and primary CTAs (Send email, Call). Email/phone/mobile move
out of a tab into an always-visible contact stack directly under the
identity block, with icon-led rows and a hover-revealed copy button.
Tabs reduce to Address / About / Organization, with Organization only
appearing for multi-tenant users — single-tenant assignments are fully
communicated through chips. The detail aside is gone; main content is
single-column and more focused.

Introduces web/js/components/app-copy-field.js — a generic, server-
markup-driven copy-to-clipboard button component wired from app-init.js.
The markup is rendered by the PHP partial (role-compatible, SSR-safe);
the component only attaches the click handler and drives icon-swap
feedback via an is-copied class.

Address-book index page now loads the 'address-book' CSS group alongside
'address-book-index' so the detail drawer (which mounts the same profile
partial inline) gets the correct styles.

Contact rows use inline-block + vertical-align centering inside the
clickable link; the field-label tooltip lives on a <span> wrapper rather
than the <i> itself (Bootstrap icons render their glyph via ::before, and
the tooltip rule would otherwise cap it). Hover reveals the copy button
on pointer devices; @media (hover: none) shows it permanently on touch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:40:55 +02:00
fs
3c6ce0cbdb fix(topbar): resolve theme + tenant-switch endpoints via lurl() to avoid relative URL breakage
Both endpoints were declared as bare relative paths ("admin/users/theme",
"admin/users/switch-tenant") and handed to postForm(), which normalizes
through `new URL(value, window.location.href)`. On a deep route like
/de/admin/permissions that resolves to /de/admin/admin/users/theme —
404 → 302 to error page → HTML response → JSON.parse throws → the theme
toggle reverts visually. The tenant switcher had the same latent bug.

- Topbar renders data-theme-url and data-switch-tenant-url via lurl(), so
  both carry the locale-aware root-anchored path.
- Tenant switcher reads data-switch-tenant-url from the switcher root
  (matching the theme-menu convention) and passes it through to
  switchTenant(); fail-fast warning if the attribute is missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:39:20 +02:00
fs
4cf4ccc595 fix(ui): robust grid table states — natural row heights, themed pagination, DOM skeleton
Rows expanded unboundedly because `fixedHeader: true` + a fixed `height`
forced a scroll container that filled itself with stretched rows on short
result sets. Pagination text rendered white-on-white because the shared
button base (app-shell.css) redefines `--app-color` as `--app-primary-inverse`
in the button scope, so `color: var(--app-color)` in an override resolves
to white. Sort-column headers looked misaligned because Grid.js uses floats
to place label and sort icon, which ignore the cell's vertical-align.

- Sticky header is now opt-in; default scroll behavior is page-level.
- Row heights come from padding + line-height only (no fixed `height`).
- Pagination buttons theme via `--app-background-color`/`--app-color`
  redefinition (the Pico convention) using `--app-form-element-color`,
  which is not shadowed in the button scope. `margin-inline-start: auto`
  on `.gridjs-pages` guarantees right-alignment even without a summary.
- Sort headers use `inline-block` + `vertical-align: middle` instead of
  vendor floats so label and icon share one cross-axis center.
- Loading UX replaced with a real `<table>` skeleton that mirrors the
  final column count + outer layout (card + footer with summary and
  5 pager placeholders). Grid.js mount point is hidden via
  `.app-grid-shell-loading > :not(.app-grid-skeleton)` — vendor wraps
  its own `.gridjs-container` one level deeper, so a direct-child
  selector on the mount element is required.
- Re-fetch state shows a thin top progress bar + faded tbody (GitHub
  pattern) so existing data stays as context.
- `prefers-reduced-motion` disables all skeleton/progress animations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:22:51 +02:00
fs
0c8ccae0fb feat(ui): refresh core grid table ux foundation 2026-04-22 19:56:23 +02:00
fs
0da785f4b7 refactor(admin-tenants): simplify list to 2 columns (avatar + name, users)
Reduce tenant grid from 8 columns to 2. Remove status, theme, sso,
active/inactive users, and created columns from the list view.
All details remain accessible on the edit page.

- filter-schema: allow order by description and users only
- data endpoint: return minimal row shape (id, uuid, description,
  status metadata for badge only, is_default, has_avatar, total_users)
- JS grid: 2 visible columns + hidden UUID for navigation
- CSS: add .grid-tenant-profile utility for flex avatar+name layout

Refs: TENANT-LIST-MINIMAL-001
2026-04-22 19:10:48 +02:00
fs
9a78a81af8 feat(admin-users): dedicated admin profile drawer with own tabs
The first pass had admin/users sharing the address book profile partial,
which surfaced Contact/Address/Organization/About tabs that ignored the
admin-specific context. Separates the admin quick-view so it mirrors the
edit-page tab structure while staying read-only.

- `UserProfileViewService::buildAdminProfile()` extends the base profile
  with `admin_extras` (active, email_verified_at, last_login_at,
  last_login_provider, created, modified).
- `templates/partials/app-admin-user-profile.phtml` is the admin-only
  render path: Master data · Organization · Security · About. Status
  badge under the name; timestamps locale-formatted.
- `pages/admin/users/view-fragment($id).php` / `(none).phtml` switched
  to the admin service method + partial.
- Drawer CSS aligns the aside with the main section's inline padding so
  the content strip does not visually step in/out.

Address book is unchanged — still uses `buildProfile()` + the base
partial. Two i18n keys added (`Login provider`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:30:03 +02:00
fs
c1cf9f94bb feat(core): UserProfileViewService + admin/users detail drawer
Extracts the user profile read-view into a core service + shared partial so
modules no longer duplicate the assignment/scope logic, and migrates the
admin/users list as the drawer's second consumer.

Consolidation:
- `core/Service/User/UserProfileViewService` owns the single read-path that
  resolves tenants/departments/roles for a user UUID, enforces scope-aware
  department visibility, and returns a consistent `{status, user}` shape.
- `templates/partials/app-user-profile.phtml` is the shared render template;
  takes `$profileKey` for storage namespace + lightbox grouping.
- `AddressBookService::buildViewContext()` shrinks to a one-line delegation
  and drops its `UserAssignmentService` dependency. The old
  `modules/addressbook/templates/address-book-profile.phtml` is removed.

admin/users migration:
- `pages/admin/users/view-fragment($id).php` + `(none).phtml` use the core
  service and enforce `ABILITY_ADMIN_USERS_VIEW`.
- `admin-users-index.js` replaces the grid-native `linkColumn` on the first
  name with a formatter that emits `data-drawer-trigger`; `linkColumn` is
  disabled so the drawer click handler wins. `fullUrl` points at the edit
  form — drawer → edit is one click.
- Drawer labels wired via page config.

The new `DetailDrawerFragmentContractTest` validated the users fragment
endpoint automatically — no manual test additions were needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:49:10 +02:00
fs
811588290c feat(core): detail drawer + address book list redesign
Introduces a reusable core detail-drawer primitive that slides in from the
right and loads any view via a `*-fragment(none).phtml` endpoint. Bundles the
address book list overhaul that is its first consumer.

Core additions:
- `app-detail-drawer.js` — generic drawer with stepper, focus trap, body
  scroll-lock, URL-hash deep-linking, session-expiry detection
- `app-fragment-init.js` — auto-wires tabs/lookups/confirm/file-upload/
  fslightbox inside injected HTML; consumers do not re-initialize components
- `app-focus-trap.js` — shared focus-trap + refcounted scroll-lock, used by
  both filter-drawer and detail-drawer
- `getHtml()` in `app-http.js` + `SessionExpiredError`; drawer reloads the
  page on auth redirect instead of rendering the login form in the panel
- `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer`
  consumer ships matching `*-fragment($id).php` + `*-fragment(none).phtml`

Address book list:
- Grid collapses from 9 columns to 4 (identity / context / phone / actions)
  with a two-line identity cell (avatar + name + email)
- Tenant register tabs above the grid using the `app-list-tabs` partial;
  tenant filter wired via hidden toolbar field so grid.js forwards it on
  every data fetch
- Profile body extracted to a shared partial so the full-page view and the
  new drawer fragment share the same markup
- New i18n keys for the drawer/list labels

Also refactors `app-filter-drawer` to reuse the shared focus-trap and
scroll-lock instead of maintaining its own copy, and documents the
detail-drawer convention in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:22:26 +02:00
fs
5ba086adee refactor(admin/users): migrate CSV export to core primitive
Replace the inline export implementation with the new generic primitive
(core/Service/Export + helpers/export + app-list-export.js). No more
hand-rolled fputcsv loop, header setup, or escape closure duplicated
here — one implementation, tested once, reused everywhere.

- export().php: uses exportRequireGetRequest, exportCapLimit,
  exportResolveFlavor, ExportColumn[], exportSendCsv. Phone/mobile/
  short_dial keep allowSignedNumeric=true so +49 numbers render
  untouched.
- export(none).phtml: deleted — headers + fputcsv now live in the
  core helper.
- index(default).phtml: inline <details class="dropdown"> "Export CSV"
  replaced by the reusable app-list-export-dropdown partial, exposing
  CSV and Excel flavors. exportUrl added to pageConfig.
- app-users-list.js: hand-rolled export-button binding removed in
  favor of initListExport({ gridConfig, exportUrl }).
- admin-users-index.js: forwards config.exportUrl into
  initUsersListPage.

Net effect: ~100 lines of duplicated export plumbing removed from the
users page; users gain the Excel flavor for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:57:31 +02:00
fs
12356e2266 feat(core): generic CSV export primitive + helpdesk domains consumer
Add a reusable CSV-export building block to the core so any Grid.js list
page can gain a filter/sort-aware download with two clicks of glue.

Core primitive:
- core/Service/Export/CsvExportService: flavor-aware writer (plain CSV
  or Excel-compatible UTF-8+BOM+semicolon), with OWASP-aligned formula-
  injection escape (=, @, +, -, TAB, CR) applied to both rows *and*
  headers. Value objects for columns (ExportColumn) carry an extractor
  closure and an allowSignedNumeric flag for phone-number-shaped cells.
- core/Support/helpers/export.php: thin HTTP layer (exportSendCsv,
  exportCapLimit, exportResolveFlavor, exportRequireGetRequest) reusing
  the requestInput() contract for GR-CORE-003 consistency. Filename is
  sanitized and length-clamped; callers must still enforce auth.
- templates/partials/app-list-export-dropdown.phtml: zero-config
  <details class="dropdown"> with CSV + Excel triggers.
- web/js/core/app-list-export.js: initListExport({ gridConfig, exportUrl })
  mirrors the current grid filters + sort onto the export URL and
  navigates, preserving session cookies for download.

First consumer — Helpdesk domains:
- DomainListService extracts the shared filter/enrich/sort logic from
  domains-data so the grid endpoint and the new domains/export endpoint
  cannot drift.
- domains/export endpoint delegates to DomainListService, declares
  ExportColumns (with translated level labels), and exits via
  exportSendCsv.
- domains-data now ~20 lines, delegating to DomainListService.

Tests:
- CsvExportServiceTest (10 cases): both flavors, BOM/no-BOM, formula
  escapes incl. TAB/CR, header escape, signed-numeric allowlist,
  quoting, multiline, empty columns, generator-compatible iterable.
- ExportHelpersTest (5 cases): exportCapLimit bounds.
- DomainListServiceTest (8 cases): filter, search, "all" sentinel,
  sort, paging, enrichment, BC failure, tenant-scoped security filter.

Gates: PHPUnit green, PHPStan clean on touched files, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:57:19 +02:00
fs
6ac685084d Feature: Security Level mit Notiz für Domains
Implementiert ein Dialog-basiertes UI zum Bearbeiten des Security Levels
mit optionaler Notiz-Funktion für Helpdesk-Domains.

Backend:
- DomainSecurityLevelRepository: CRUD für security_levels mit note-Spalte
- DomainSecurityLevelService: Business-Logik mit Batch-Enrichment
- security-level-data() Endpoint: AJAX + Full-Page POST mit CSRF
- Migration 010: note TEXT-Spalte hinzugefügt
- Bugfix: findByTenantAndDomainNo gibt null bei leerem Array zurück

Frontend (Domain-Liste):
- Badge in Grid klickbar → öffnet Modal-Dialog
- Dialog mit Level-Select + Notiz-Textarea
- AJAX-Save mit Live-Badge-Update ohne Reload
- Event-Delegation für dynamische Grid-Inhalte

Frontend (Domain-Detail):
- Notiz-Feld im Security-Level-Formular
- PRG-Pattern mit Flash-Messages

Core:
- app-http.js: DEFAULT_HEADERS auf XMLHttpRequest für CSRF-Kompatibilität

i18n:
- Keys: Priority, Note, Optional note, Security level updated

Tests:
- DomainSecurityLevelServiceTest: CRUD + Batch-Enrichment
2026-04-21 13:43:16 +02:00
fs
c3de7d4238 feat(js): enforce global lifecycle hard-cuts and helpdesk list adapter 2026-04-20 23:01:54 +02:00
fs
c163393cc4 refactor(js): migrate core list pages to list module factory 2026-04-20 22:54:19 +02:00
fs
7c47e818f2 feat(js): harden global HTTP and module import contracts 2026-04-20 22:41:07 +02:00
fs
f189ef9df6 feat(js): add app-http contracts and migrate helpdesk runtime layer 2026-04-20 22:31:13 +02:00
fs
5982e749d7 css: consolidate core stylesheet entrypoint and add contract check 2026-04-20 19:55:49 +02:00
fs
da0abc824a feat(helpdesk): add software updates tracking with BC ticket integration
New Updates page in the Helpdesk module that fetches UPDATE/UPDATE-HF tickets
from Business Central (PBI_LV_Tickets) and allows assigning a domain and Gitea
link via a dialog. Ticket status (from BC) and assignment status (local) are
shown as separate columns with filters for both plus type and free-text search.
Assigned updates also appear on the domain detail page. Includes session-cached
BC fetch with refresh button, admin permissions, migration, and 16 unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:21:40 +02:00
fs
a26c106083 feat(helpdesk): add handover protocol management
Implement handover protocols as a new entity in the helpdesk module,
allowing users to create fillable protocol records from admin-defined
software product schemas.

Key additions:
- DB migration (helpdesk_handovers table) with tenant scope
- HandoverService with status workflow (draft/in_progress/completed/archived)
- Three-tier permissions (view/create/manage)
- Two-step creation wizard (Stripe-style assistant)
- Grid.js list page with search and status filter
- Edit/detail page with aside metadata and status controls
- Reusable core autocomplete lookup component (app-lookup-field)
- Debitor lookup data endpoint for autocomplete
- Dynamic form rendering from schema snapshots
- 11 PHPUnit tests for HandoverService
- DE+EN i18n translations (48 keys each)

Also includes: PHPStan fixes (dead code removal, stale baseline cleanup),
software product edit title improvement, fieldset simplification,
and Stripe-style hover for schema preview.

Workflow: HD-HANDOVERS-001 (.agents/runs/HD-HANDOVERS-001/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:42:41 +02:00
fs
57b7920098 fix(flash): make flash messages one-shot per request 2026-04-15 18:45:45 +02:00
fs
63be2def76 docs: add UI/UX component guidelines and improve breadcrumb/tab-panel styling
Add "Komponenten in Formularen" section to boundaries-ui.md with rules
for core button classes, margin resets, checkbox sizing, sticky positioning,
icon-only buttons, responsive grids, and CSS variables. Add matching
quick-reference bullets to CLAUDE.md UI Patterns section.

Fix breadcrumb wrapping: nowrap + overflow hidden + flex-shrink for
single-line behavior. Add data-tab-panel-wide opt-out for full-width
tab panels (default 70ch).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 18:44:29 +02:00
fs
d472026df4 feat(helpdesk): add Domains list page with BC OData contract type enrichment
Add a new "Domains" page to the helpdesk module that fetches domain data
from the FS_Contract_Domains OData endpoint and enriches each domain with
its contract type (PI_Header_Type) by joining with FS_Contract_Lines_Test
where Type = 'Domain'.

New files:
- domains/index().php, index(default).phtml, filter-schema.php — list page
- domains-data().php — data endpoint with PHP-side filtering/sorting
- helpdesk-domains-index.js — Grid.js via initStandardListPage()

Gateway additions:
- listDomains() — fetch all contract domains
- listDomainContractLines() — fetch domain-type contract lines for type lookup

Sidebar restructured into three collapsible groups (Lookup, Monitoring,
Administration) with per-group icons and color coding, matching the core
admin panel pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:34:32 +02:00
fs
0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00
fs
99f8b55d49 refactor: consolidate JS toggle components, grid query parsers, and add tests
JS components: migrate app-custom-field-options-toggle, app-color-default-toggle,
and app-settings-telemetry to shared conditional-controls factory/utility,
removing duplicated syncControlState logic and init boilerplate.

grid.php: extract gridQueryCsvInput() to DRY up 3 CSV parsers, simplify
multi_csv handler via gridNormalizeLabelList, delegate order type to gridQueryEnum.

Tests: add 23 SearchQueryNormalizer tests (LIKE escaping, wildcards, Unicode)
and 7 additional Crypto edge-case tests (empty input, missing fields, special
chars, binary content, truncation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 22:22:15 +02:00
fs
31ecb40235 refactor: clean up web assets and consolidate PHP helpers
- Remove 2079 unused bootstrap-icons SVGs + min.css (~9.3 MB saved, app uses font approach only)
- Remove unused editorjs.umd.js (only .mjs is imported)
- Extract shared syncControlState into app-conditional-controls.js, deduplicate SSO/LDAP toggles
- Add settingToBool() helper to DRY up repeated bool-parsing pattern across 6 call sites
- Merge single-function auth.php into app.php, delete auth.php
- Extract 9 branding functions from app.php into dedicated branding.php (app.php 709→528 lines)
- Remove empty web/img/ directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:31:59 +02:00