Add docker-compose.yml: a disposable Stalwart instance with a fixed dev
admin account (STALWART_RECOVERY_ADMIN), matching the credentials
scripts/dev-token.ps1 already expected. Wired up via new npm scripts
(dev:server, dev:server:down, dev:server:logs).
scripts/dev-token.ps1 was previously untracked (the whole scripts/
directory was gitignored) even though it's part of the documented dev
workflow — un-ignored it, and added scripts/dev-token.sh, a POSIX
equivalent for non-Windows shells and AI agents without PowerShell.
New DEVELOPMENT.md documents the full loop end-to-end (start server,
get a token, run the dev server, verify), written so it's actionable by
both humans and AI coding agents without needing a browser. Linked from
AGENTS.md (Commands) and README.md (Getting started).
Verified manually: docker compose up brings the server to a healthy
state, /api/auth + /auth/token issue a working bearer token, and
/jmap/session returns 200 with it end-to-end.
Rewrite everything below the project header to actually describe this
fork instead of a generic copy of upstream's README:
- "About this fork" + list of official Stalwart repositories.
- Corrected the "Nothing is hardcoded" overclaim; Features now lists
what's shared with upstream vs. this fork's own additions, and links
to SCHEMA_DEVIATIONS.md for the tracked exceptions.
- New "Switching your server to this fork's UI" section: how to point
a Stalwart server's WEBAPP application at this fork's release build
via stalwart-cli, and how to switch back.
- Support section now distinguishes fork-specific issues (this repo)
from Stalwart Mail Server support (upstream channels).
- License/Copyright text left untouched; added one line noting fork
changes stay under the same dual license per each file's SPDX header.
The project header block (logo, title, badges) is unchanged.
src/types/schema.ts had drifted from the official schema contract: a
`clientOnly` field had been added to `FilterEnum` to support client-side
log filtering. Restored it to match upstream exactly and moved the
deviation-only type into a new intersection type in
src/lib/schemaDeviationTypes.ts instead.
Audited the codebase for every place the UI does something the official
schema doesn't support and tagged each with `// SCHEMA-DEVIATION: <id>`,
documented in the new SCHEMA_DEVIATIONS.md registry (what, why, and the
ideal server-side fix):
- log-client-filters: Level/Event filters on Log Entries, applied
client-side because x:Log/query rejects them as JMAP filters.
- account-quota-usage-column: synthetic quotaUsage column on the
Accounts list.
- mailbox-client-hierarchy-sort: full-fetch + client-side sort to
reconstruct mailbox parent/child hierarchy.
- webapp-enabled-column-fallback: synthetic "Enabled" column label for
x:Application when the schema list doesn't define one.
Other viewName/objectName special cases (x:OtpAuth, x:Expression,
x:Rate, x:Action, x:Trace, CustomComponent/*) were checked against
upstream and are pre-existing architecture, not fork deviations.
Any AI agent working in this repo (Claude Code, Codex, Kimi, or any
other) must stay schema-driven and never hardcode object types, field
names, filters, or columns as a shortcut. AGENTS.md is the index (tech
stack, commands, universal rules); CLAUDE.md just points Claude Code at
it so the same rules apply without duplicating content. The detailed
schema-fidelity rule lives in .agents/rules/.
.gitignore previously excluded *.md except README/CHANGELOG, which
would have hidden these files (and SCHEMA_DEVIATIONS.md) from git.
main.tsx no longer needs an eager logo fetch call: Logo.tsx already
triggers ensureLogoLoaded() from logoCache.ts (the fork's own
implementation, kept over upstream's during the sync merge).
Documents everything since the v1.0.8 entry: icon batches, the Web
Applications active-app info card, PWA support, Log Entries filters
and refresh button, Accounts list columns, the account-switch fixes
(reviewed against and aligned with upstream's own fix for the same
issue), the WebUI Fork rename, mailbox hierarchy, new color themes,
and the confirmed-backend-only limitations (Log filter properties,
exact-match-only text search across the admin API).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Aligns with upstream stalwartlabs/webui@189e270 (v1.0.7, "fixes #17"),
reviewed after they independently landed a fix for the same issue:
- authStore now persists activeAccountId (sessionStorage) and
setSession preserves it across a session refresh instead of always
resetting to primaryAccountId, so a hard reload keeps you on the
group account you had selected instead of bouncing back to your own.
- switchAccount now clears cacheStore (displayNames/objectLists) when
actually changing account, since those were resolved against the
previous account and would otherwise show stale labels.
- The ErrorBoundary wrapping MainContent is now keyed on
activeAccountId, forcing a full remount of every view on switch.
This is more robust than gating individual components' fetch
effects on activeAccountId (our earlier fix in DynamicList.tsx,
kept as-is — harmless now, but no longer load-bearing on its own)
since it covers every current and future view type, not just lists.
Verified against a live instance: switching to a group account updates
the JMAP accountId immediately (no tab switch needed), and a full page
reload keeps the group account active instead of resetting to admin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds three new color themes alongside the existing Stalwart/Ocean/
Forest/Violet set so more people can find one they like, and switches
the out-of-the-box defaults to Ocean + square corners instead of the
neutral Stalwart theme + rounded corners (existing users' saved
preferences are untouched, this only changes what a fresh install
starts with).
Verified in the browser: all three new themes render with legible
primary-foreground contrast in both light and dark mode, and the
pre-hydration flash-prevention script in main.tsx (which reads
localStorage before React mounts) now recognizes the new theme names.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixesstalwartlabs/webui#16. Mailboxes had no visual indication of
parent/child relationships. Since a mailbox's parent can land on a
different server page than the mailbox itself, the Mailbox list now
always fetches the full set (like the existing client-filter path)
and orders it depth-first by parentId, then indents each row's name
with a corner connector proportional to its depth.
Verified against a live instance with a 3-level nested hierarchy
(Projects > Alpha > Docs) alongside the default flat mailboxes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switching accounts via the profile dropdown (TopBar) only updates
authStore.activeAccountId with no navigation, but DynamicList's fetch
effect depended on [viewName, sort, resolved?.list, appliedFilters] —
missing activeAccountId, and account resolution only happened through
a non-reactive getState() snapshot inside getAccountId(). So views
like Mailboxes, Calendars, or Sieve Scripts kept showing the previous
account's data until an unrelated viewName change (switching tabs)
incidentally re-ran the effect.
Reproduced and verified against a live Stalwart instance: a mailbox
list stayed on accountId "b" after switching to a group account "d"
in the dropdown, and only picked up "d" after navigating away and
back. Subscribing to activeAccountId reactively and adding it to the
effect's dependencies fixes the switch to apply immediately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Created At is replaced with two columns that previously required
opening each account individually: Role (badge, resolved against
x:UserRoles specifically since the list's merged User/Group field
definitions otherwise resolve `roles` against the wrong object) and
Usage / Quota (usedDiskQuota vs quotas.maxDiskQuota, "Unlimited" when
no limit is set).
Also makes renderCellValue's generic 'object' case resolve variant
labels via schema.schemas, instead of only ever printing the raw
@type string.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a Refresh button next to Filters, right-aligned, specific to the
Logs list. Guarded to one click per 5 seconds (button disabled and
re-enabled via a timer) to avoid hammering the server if left clicked
repeatedly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The backend rejects `level`/`event` as x:Log/query filter conditions
(unsupportedFilter), even though both are already returned per row.
Since a real fix requires backend changes outside this repo, the two
filters are injected into the schema client-side (clientOnly flag) and
applied entirely in the browser: excluded from the JMAP filter sent to
the server, and used to narrow an eagerly-fetched, locally-paginated
result set instead.
Also makes large enum filters (Event has 634 values) render as a
searchable Combobox instead of a plain Select, generically for any
list with more than 15 enum options.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds vite-plugin-pwa with a generated manifest, icon set derived from
the Stalwart mark (192/512/maskable/apple-touch), and theme-color meta
tags. /api and /jmap are excluded from the service worker's navigate
fallback and are never precached or runtime-cached, so authenticated
mail data can't end up in Cache Storage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wrap the main area in a centered max-w-7xl container and center the max-w-4xl form column inside it, so pages no longer hug the left edge on wide screens. Lists and views center up to 7xl, forms stay at a readable 4xl.
Introduce a --field design token (page background tone) used by inputs, textareas and select triggers, and raise the dark-mode card lightness so cards read as elevated surfaces with inset fields, following the shadcn convention.
Collapsible groups now auto-open when the active page lands inside them (while remaining manually toggleable), and the sidebar scrolls the active item into view. Previously, navigating via the command palette left the sidebar collapsed on the wrong spot.
Replace the TopBar dropdown search with a cmdk-based command palette opened via a visible trigger button (with platform-aware shortcut badge) or the Ctrl+K/Cmd+K global shortcut. Shared search logic moves to a useGlobalSearch hook; the palette reuses the existing ui/command components.
Same-origin proxying to localhost:8080 avoids CORS without weakening the server's CORS policy; pair with VITE_API_BASE_URL= (empty) in .env.development.local.
The dashboard (recharts, ~120 kB gzip), tracing, troubleshoot, actions and bootstrap wizard now load on demand, and the admin panel is split out of the entry chunk so anonymous visitors only download the login page (~134 kB gzip instead of ~557 kB).