Add BMAD planning artifacts: brief, PRD, architecture, UX, spec, tickets
Includes the BMAD install config, the initiative ticket tree, the portal context-resources scripts, and the .gitignore change that tracks BMAD files (generated _bmad/render/ is ignored). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sk72AxLwQnYttF6VFSxQLt
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
97b2a95062
commit
24de5878ed
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
scope: ping-mee self-service portal, whole system (greenfield)
|
||||
purpose: build substrate for epics/stories; architecture spine
|
||||
altitude: initiative
|
||||
updated: 2026-10-15T11:22
|
||||
---
|
||||
|
||||
|
||||
- (constraint) Input: PRD _bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15 (prd.md FR-1..FR-29, addendum.md) is binding; its non-goals and decisions are inherited silently.
|
||||
- (constraint) Deployment: ONE Docker container on Linux behind existing reverse proxy (TLS terminated there); all config + secrets via env vars; nothing ping-mee-specific baked in; internet-exposed.
|
||||
- (constraint) Integrations: OIDC (authentik, generic OIDC); LDAPS to AD with a least-privilege service account (OU + attrs givenName,sn,displayName,mail,telephoneNumber,mobile,password); SMTP (IONOS, configurable); SMB share (write source/<sam>.jpg, read processed/<sam>.md5); existing PowerShell sync scripts unchanged.
|
||||
- (constraint) Scale: ~10 users; hard security bar (no injection, no session takeover, no cross-user access, throttling in front of every AD bind).
|
||||
- (constraint) Open from PRD for architecture: Q-1 share unreachable, Q-2 mail retry/IONOS limits, Q-3 languages, Q-4 absolute session max, Q-5 data store/backup, Q-6 removed-AD-user cleanup, Q-7 removed-picture pending, Q-8 time zone, Q-9 md5 timing.
|
||||
- (decision) Mode: COACHING. Audience: only Ebbe + AI agents (spine only; no extra human-facing artifact requested yet). User has NO stack preference - delegates choice to architect.
|
||||
- (version) Django: 5.2 LTS (security support to Apr 2028), 6.0.x current as of Oct 2026 per web search (source: endoflife.date / djangoproject.com); next LTS 6.2 Apr 2027. Verify exact pin when scaffolding.
|
||||
- (version) ldap3 (pure Python): last PyPI release 2.9.1 ~4 years ago, single maintainer -> stagnant. python-ldap: healthy, releases within last 3 months, needs libldap/libsasl build deps. Both can set AD unicodePwd over ldaps.
|
||||
- (version) pillow-heif: 1.3.0 (Feb 2026) on PyPI per search, newer 1.6.0 in Arch testing Aug 2026; HEIC/AVIF decode 8/10/12-bit. Verify pin.
|
||||
- (version) Fluent UI Blazor (Microsoft.FluentUI.AspNetCore.Components) 4.13.x documents .NET 8/9; .NET 10 support not confirmed -> weakens the .NET option.
|
||||
- (decision) STACK [user-chosen, option A]: Python + Django (5.2 LTS or current; pin at scaffold), server-rendered pages, python-ldap (not ldap3: stagnant), Pillow + pillow-heif. Rejected: ASP.NET/Blazor (HEIC, .NET10 Fluent unconfirmed, Blazor circuits vs session kill), Node/React (SPA attack surface, HEIC), Go (cgo HEIC, hand-built). Binds: all units use Django ORM/sessions/CSRF/templates; Prevents: parallel hand-rolled web/auth layers.
|
||||
- (direction) Office 365 look via Microsoft Fluent web components as static assets in server-rendered templates (UNVERIFIED - check at UX/scaffold).
|
||||
- (decision) PARADIGM [ADOPTED by user]: modular monolith, Django project, one app per feature (accounts, profile, picture, announcements, adminui, audit) + external-world adapters with ONE OWNER each: directory (only LDAP/service account), mailer (only SMTP), share (only picture folders), idp (OIDC). No other module imports ldap/smtplib or touches share paths. Prevents: scattered AD/file access, injection surface.
|
||||
- (decision) INVARIANT [agreed]: self-service actions always operate on the session user; no username taken from URL/form. Every state change writes its Audit Entry in the SAME transaction in the service layer (not view). Roles from AD group membership cached briefly (default 60s, setting).
|
||||
- (decision) DATA STORE (Q-5) [user-confirmed constraints]: SQLite on a mounted volume, ONE gunicorn worker with threads + one in-process scheduler thread; scale-out not supported without moving to PostgreSQL. Backups: Veeam copy-job copies directories -> portal must write consistent SQLite backups (online backup API) into a backups/ dir on the volume on a schedule; Veeam must not rely on copying the live DB file.
|
||||
- (decision) Q-4: absolute session max 4h (plus 1h idle). Q-3: German AND English, all text translatable from day one. Q-8: store UTC, display Europe/Berlin default (configurable), zone named in e-mails. Share: host mounts CIFS, container gets bind mounts source (rw) + processed (ro).
|
||||
- (decision) CLIENT IP: reverse proxy is Zoraxy (per issue tobychui/zoraxy#93): appends real client IP to X-Forwarded-For and forwards an existing X-Real-IP UNCHANGED (spoofable). Rule: if TCP peer is in TRUSTED_PROXIES, client IP = rightmost X-Forwarded-For entry (trusted-proxy count configurable); NEVER use X-Real-IP; else use peer address. Verify with a test request at deploy.
|
||||
- (decision) ENVIRONMENTS: no staging and no test AD. Only prod AD/authentik. Test OU can be created: a non-prod instance of the SAME image points at a test OU with a test service account scoped to it (OU, groups, share paths all config). Adapters have fakes; automated tests need no real AD/IdP/SMTP; manual smoke checklist against the test OU.
|
||||
- (decision) DEPLOY: docker compose by hand on Linux host; image location not stated (assume built on host or private registry - deferred). Veeam copy-job notifies on copy failure only; portal MUST show last-successful-backup age + failed jobs (backup, mail outbox) in Admin UI because a portal-side backup failure would be invisible to Veeam. No alert e-mail; Admin UI status is enough (user).
|
||||
- (decision) SECURITY [proposed, no objection]: Authlib for OIDC (code flow + PKCE, verify state/nonce/signature); throttle counters in DB keyed endpoint+user and endpoint+IP, checked BEFORE any directory call; sensitive actions re-verify password via directory adapter after throttle; passwords memory-only, never logged; secure HttpOnly cookies, CSRF everywhere, strict CSP (no inline script), security headers; DB sessions with username column; one-query session kill.
|
||||
- (decision) OPS [proposed, no objection]: multi-stage image, non-root, only data/backups/share writable; entrypoint migrates then starts, fail-fast on missing env; /healthz checks DB only (not AD); JSON logs to stdout with request id, no secrets; 30s idempotent state-driven scheduler tick (no scheduler lib) running publish, outbox send, token expiry, audit retention, DB backup, nightly AD reconciliation; upgrade = backup, migrate; rollback = restore + previous tag.
|
||||
- (decision) MAIL [proposed]: outbox table, per-recipient rows, retries/backoff + rate limit for IONOS, no direct sending. Q-6: nightly reconciliation deletes opt-outs/pending changes/picture state of users gone from OU; audit kept to retention. Q-1: share adapter atomic write (tmp+rename) with timeouts, error to user, no state change. Q-7/Q-9: Pending clears when processed md5 == stored md5; for removal when processed md5 != previous md5; sync-writes-md5-before-AD limitation accepted + timeout message.
|
||||
- (version) nh3 (Rust ammonia binding) is the maintained HTML sanitizer; bleach deprecated Jan 2023. nh3 0.2.x on PyPI. Markdown parser: markdown-it-py (not verified; pin at scaffold).
|
||||
- (version) @fluentui/web-components v3.0.0 stable 29 Jun 2026, v3.0.1 7 Jul 2026; framework-agnostic. DECISION: vendor built assets into the image at build time (no runtime CDN) - strict CSP and supply-chain; Node only in build stage.
|
||||
- (version) Authlib 1.7.x current (June 2026 release) - verified by search; mozilla-django-oidc 4.0.1 (2024) older.
|
||||
- (event) Gate: lint clean; adversarial pass done inline (no subagents). Fixes: LDAPS CA bundle via env (internal CA); SessionUser(key,sam) canonical identity; AD-call-inside-transaction semantics; mailer owns outbox; email plain-text + sanitized HTML; AD-19 one owner per table; single logout deferred. Independent subagent reviewers NOT run.
|
||||
- (event) spine finalized
|
||||
- (change) Update pass from UX run: AD-2 read allow-list incl. Password Policy + pwdLastSet; AD-13 accent colour + contrast warning; AD-15 per-device preferences cookies + de formal/informal; AD-16 rich-text editor -> Markdown, assets bundled; capability map FR-30..32; stack row for editor library (chosen at scaffold).
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
---
|
||||
name: 'ping-mee Self-Service Portal'
|
||||
type: architecture-spine
|
||||
purpose: build-substrate
|
||||
altitude: initiative
|
||||
paradigm: 'Modular monolith with adapters (ports and adapters) for every external system'
|
||||
scope: 'The whole Portal: one Django container, its adapters to AD, OIDC, SMTP and the picture share, its data, and its operation'
|
||||
status: final
|
||||
created: '2026-10-15'
|
||||
updated: '2026-10-15'
|
||||
binds: ['PRD FR-1..FR-32', 'PRD §8 cross-cutting requirements', 'PRD Q-1..Q-9', 'UX DESIGN.md and EXPERIENCE.md']
|
||||
sources: ['_bmad-output/ux-designs/ux-ping-mee-self-service-platform-2026-10-15/DESIGN.md', '_bmad-output/ux-designs/ux-ping-mee-self-service-platform-2026-10-15/EXPERIENCE.md', '_bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15/prd.md', '_bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15/addendum.md', '_bmad-output/briefs/brief-ping-mee-self-service-platform-2026-10-15/brief.md', 'context-resources/*.ps1']
|
||||
companions: ['_bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15/prd.md']
|
||||
---
|
||||
|
||||
# Architecture Spine — ping-mee Self-Service Portal
|
||||
|
||||
## Design Paradigm
|
||||
|
||||
Modular monolith with adapters. One Django project, one Django app per feature, and one adapter per external system. Features call adapters only through services; adapters know nothing about features.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
V[views / templates] --> S[feature services]
|
||||
S --> AUD[audit service]
|
||||
S --> ACC[accounts: session user, roles]
|
||||
S --> AD[adapters]
|
||||
SCH[scheduler tick] --> S
|
||||
AD --> DIR[directory: LDAPS]
|
||||
AD --> MAIL[mailer: SMTP]
|
||||
AD --> SHR[share: picture folders]
|
||||
AD --> IDP[idp: OIDC]
|
||||
S --> DB[(SQLite)]
|
||||
```
|
||||
|
||||
Arrows are the only allowed dependency direction. Views never call adapters. Features never import each other's models; they call each other's services.
|
||||
|
||||
## Invariants & Rules
|
||||
|
||||
### AD-1 — Adapters have exactly one owner each
|
||||
- **Binds:** all FRs touching AD, mail, the share or the IdP
|
||||
- **Prevents:** scattered LDAP, SMTP or file access, each with its own escaping and error handling
|
||||
- **Rule:** only `adapters/directory` imports `ldap`; only `adapters/mailer` imports `smtplib`; only `adapters/share` opens paths under the source or processed folders; only `adapters/idp` talks OIDC. Each adapter exposes a small interface with an in-memory fake.
|
||||
|
||||
### AD-2 — Directory access is allow-listed and injection-proof
|
||||
- **Binds:** FR-1, FR-5, FR-8, FR-9, FR-10, FR-14, FR-20; NFR §8.1
|
||||
- **Prevents:** LDAP injection, writes outside the OU, writes of unlisted attributes, the service account used anywhere else
|
||||
- **Rule:** the Service Account is used only inside `directory`. Filters are built only with LDAP filter escaping; DNs are never built from input — the DN is found by searching `sAMAccountName` inside the configured OU. Writable attributes are a fixed allow-list: `givenName`, `sn`, `displayName`, `mail`, `telephoneNumber`, `mobile`, `unicodePwd`. Readable attributes are also a fixed allow-list: the writable ones, `sAMAccountName`, `memberOf` (or a group-membership query), `thumbnailPhoto`, `pwdLastSet`, the account's enabled and locked state, and the Password Policy (the domain's `minPwdLength`, `pwdProperties`, `pwdHistoryLength`, `minPwdAge`, `maxPwdAge`, and the user's `msDS-ResultantPSO` when a fine-grained policy applies). The Service Account's read rights are limited to the same list. Everything runs over LDAPS with certificate verification against a CA bundle supplied by environment variable (the homelab uses an internal CA); verification is never disabled.
|
||||
|
||||
### AD-3 — Self-service acts on the session user only
|
||||
- **Binds:** FR-5, FR-8, FR-10..FR-15, FR-21
|
||||
- **Prevents:** editing another user through a crafted URL or form
|
||||
- **Rule:** no self-service view, service or form accepts a username, user id or DN. The acting user is read from the session as one object, `SessionUser(key, sam)`: `key` is the lower-case value used for every database key, `sam` is the exact `sAMAccountName` from AD used for AD calls and file names. No module derives either value itself.
|
||||
|
||||
### AD-4 — Audit entry in the same transaction as the change
|
||||
- **Binds:** FR-24, FR-28
|
||||
- **Prevents:** changes without a record; records without a change
|
||||
- **Rule:** every state-changing service function writes its Audit Entry (who, what, when, IP) in the same database transaction, in the service layer, never in the view. Entries never contain passwords, tokens or picture content. A service that changes AD makes the AD call inside the transaction, before commit. A failed AD call rolls back and records a failure entry in its own transaction; a commit failure after a successful AD call is logged at error level with the change details.
|
||||
|
||||
### AD-5 — Identity, roles and account state come from AD, cached briefly
|
||||
- **Binds:** FR-1, FR-2, FR-25
|
||||
- **Prevents:** stale admin rights; a disabled account staying signed in; a directory outage granting access
|
||||
- **Rule:** `accounts` resolves username, Publisher and Admin membership, and enabled/locked state from `directory` and caches the result per session for a configured time (default 60 s). A directory failure denies elevated roles and new sessions. The OIDC `preferred_username` must equal `sAMAccountName`.
|
||||
|
||||
### AD-6 — Sessions are server-side with a kill-by-user primitive
|
||||
- **Binds:** FR-3, FR-7, FR-11
|
||||
- **Prevents:** sessions that cannot be ended; mismatched idle and absolute limits
|
||||
- **Rule:** sessions live in the database with the username in its own column. Idle limit 1 h, absolute limit 4 h, both settings. `end_sessions(username, except_current=bool)` is the only way to end sessions and is used by password change, undo, sign-out and disabled-account detection. Cookies are secure, HttpOnly and SameSite=Lax.
|
||||
|
||||
### AD-7 — Throttle before any outbound call; client IP from a trusted proxy only
|
||||
- **Binds:** FR-4, FR-6, FR-10, FR-11, FR-12
|
||||
- **Prevents:** AD lockout by an attacker; spoofed IPs defeating throttling
|
||||
- **Rule:** counters live in the database, keyed by endpoint plus user and endpoint plus IP, and are checked before `directory`, `mailer` or upload processing. Client IP: if the TCP peer is in `TRUSTED_PROXIES`, take the last `X-Forwarded-For` entry (the one the proxy appended); otherwise use the peer address. `X-Real-IP` is never used.
|
||||
|
||||
### AD-8 — Sensitive actions re-verify the password; passwords live only in memory
|
||||
- **Binds:** FR-5, FR-6, FR-10
|
||||
- **Prevents:** session hijack changing the password or e-mail; password leakage
|
||||
- **Rule:** password change and e-mail change call `directory.verify_password(user, password)` after the throttle check. Passwords are never stored, logged, audited or placed in error reports; sensitive POST parameters are excluded from logging and error pages.
|
||||
|
||||
### AD-9 — Link actions: GET shows, POST applies; tokens are hashed and single-use
|
||||
- **Binds:** FR-10, FR-11
|
||||
- **Prevents:** mail scanners confirming or undoing changes; token theft from the database
|
||||
- **Rule:** confirmation (1 h) and undo (24 h) tokens are random, stored only as hashes, single-use and expiring. A GET renders a page naming the change; only a POST with CSRF applies it.
|
||||
|
||||
### AD-10 — The picture pipeline is fixed
|
||||
- **Binds:** FR-12..FR-15; Q-1, Q-7, Q-9
|
||||
- **Prevents:** unsafe image input; a different file name than the sync script reads; wrong Pending state
|
||||
- **Rule:** accepted input is JPEG, PNG, HEIC or WebP up to 10 MB; SVG and everything else is refused. Decode with pixel and size limits, apply orientation, strip all metadata, crop square, re-encode as a 512×512 JPEG. The `share` adapter writes `<sAMAccountName>.jpg` to the source folder atomically (temp file, then rename) with timeouts and records the MD5 in the database. The original upload is never stored. Pending clears when `processed/<name>.md5` (case-insensitive, trimmed) equals the stored MD5; for a removal it clears when that file differs from the previous MD5. After a configured time the Portal shows "taking longer than expected". If the share is unreachable the user sees an error and nothing changes.
|
||||
|
||||
### AD-11 — All mail goes through an outbox
|
||||
- **Binds:** FR-10, FR-11, FR-7, FR-20; Q-2
|
||||
- **Prevents:** lost or duplicated mail on restart; blocking requests on SMTP; hitting provider limits
|
||||
- **Rule:** the `mailer` module owns the outbox table; every other module calls `mailer.enqueue(template, recipient, context)` and nothing else. The scheduler calls `mailer.flush()`, which sends rows with retries, backoff and a configured rate limit. Recipients of an Announcement are resolved at publish time: all Users in the OU with a valid `mail` value, minus stored opt-outs; users without `mail` are recorded as skipped. Only opt-outs are stored. Every e-mail has a plain-text part; an HTML part, if any, passes the same sanitizer as the page (AD-16). Failures are visible in the Admin UI.
|
||||
|
||||
### AD-12 — One scheduler thread running idempotent, state-driven jobs
|
||||
- **Binds:** FR-17, FR-20, FR-24; Q-5, Q-6
|
||||
- **Prevents:** duplicate runs; jobs that depend on process memory
|
||||
- **Rule:** the app runs one gunicorn worker with threads and one scheduler thread ticking every 30 s. Every job reads the database for what is due and is safe to repeat: publish due Announcements, send the outbox, expire tokens and sessions, apply audit retention, write the database backup, reconcile with AD nightly (delete opt-outs, pending changes and picture state of users no longer in the OU; audit entries stay until retention). Each run records its status for the Admin UI.
|
||||
|
||||
### AD-13 — Configuration is partitioned and typed
|
||||
- **Binds:** FR-23, FR-26
|
||||
- **Prevents:** secrets in the database or UI; a runtime setting that opens a security hole
|
||||
- **Rule:** environment variables hold everything that identifies a system or contains a secret (AD, LDAPS, Service Account, OIDC, SMTP, share paths, group names, trusted proxies, time zone default). Runtime settings (accent colour, backgrounds, portal name, default form of address, session limits, throttle limits, audit retention) live in the database and are declared in one registry with type, default and minimum value. Saving the accent colour computes its contrast against the on-brand text colour for the light and the dark theme and returns the result and a warning when it is below 4.5:1; the save is still allowed. A missing or invalid environment variable stops startup with a clear message. Nothing in the repository names ping-mee.
|
||||
|
||||
### AD-14 — SQLite with WAL on a volume, plus consistent backup files
|
||||
- **Binds:** FR-27, FR-28; Q-5
|
||||
- **Prevents:** inconsistent copies of a live database; invisible backup failures
|
||||
- **Rule:** the database is SQLite in WAL mode on the `/data` volume. The scheduler writes consistent backup files using the SQLite online backup API into `/backups` with a configured retention. Host-side copy jobs (Veeam) copy `/backups`, not the live file. The Admin UI shows the age of the last successful backup.
|
||||
|
||||
### AD-15 — Time and language
|
||||
- **Binds:** FR-17, FR-29; Q-3, Q-8
|
||||
- **Prevents:** mismatched time zones; untranslatable strings
|
||||
- **Rule:** all timestamps are stored in UTC. Display uses the configured zone (default Europe/Berlin) and e-mails name the zone. Every user-visible string goes through Django translation from the first line of code; German and English ship, and German has a formal and an informal register. Per-device Preferences (theme: light, dark or system; language; German register) are stored in cookies and never on the server. The server reads them to render the page (a theme attribute on the root element, not inline script); `system` is resolved by CSS only. Language defaults from `Accept-Language`. Server-sent e-mails use the portal-wide default register.
|
||||
|
||||
### AD-16 — User content is rendered safely
|
||||
- **Binds:** FR-16, FR-19, FR-22; NFR §8.1
|
||||
- **Prevents:** script injection; remote code from CDNs; unsafe uploads by Admins
|
||||
- **Rule:** templates auto-escape. Announcement bodies are edited in a browser rich-text editor (toolbar: bold, italic, heading, list, link) that converts to Markdown. The server treats the submitted Markdown as untrusted: it is stored as Markdown, rendered by a Markdown parser, and sanitized with an allow-list sanitizer on every render; raw HTML is not allowed. A strict Content-Security-Policy forbids inline scripts and external script sources; all front-end assets, including the rich-text editor and the Fluent web components, are built into the image, with no runtime CDN. Admin background images follow the same re-encode and size limits as pictures and are served only to signed-in Users.
|
||||
|
||||
### AD-17 — Errors are typed and mapped in one place
|
||||
- **Binds:** FR-5, FR-9, FR-10, FR-12, FR-14, §8.3
|
||||
- **Prevents:** raw exceptions or directory messages reaching users; inconsistent messages
|
||||
- **Rule:** services raise typed errors (`ValidationFailed`, `Throttled`, `Forbidden`, `DirectoryUnavailable`, `ShareUnavailable`, `MailUnavailable`). One mapping layer turns them into translated user messages and HTTP codes. AD password-policy rejections are mapped to specific messages.
|
||||
|
||||
### AD-18 — Logs and health
|
||||
- **Binds:** FR-27, FR-28; §8.3
|
||||
- **Prevents:** secrets in logs; a healthy-looking container that is broken, or one restarted because AD is down
|
||||
- **Rule:** logs are JSON on stdout with a request ID, never containing secrets or personal content. `/healthz` checks the database only, never AD, SMTP or the share.
|
||||
|
||||
### AD-19 — Every table has exactly one owning app
|
||||
- **Binds:** all features; NFR §8.1
|
||||
- **Prevents:** two modules writing the same entity in incompatible ways
|
||||
- **Rule:** `accounts` owns sessions and throttle counters; `profile` owns pending e-mail change tokens; `picture` owns picture state; `announcements` owns announcements and opt-outs (the profile page calls its service); `mailer` owns the outbox; `adminui` owns settings; `scheduler` owns job runs; `audit` owns audit entries and is called by all. Other apps read an entity only through its owner's service.
|
||||
|
||||
## Consistency Conventions
|
||||
|
||||
| Concern | Convention |
|
||||
| --- | --- |
|
||||
| Naming | Django apps: `accounts`, `profile`, `picture`, `announcements`, `adminui`, `audit`. Adapters under `adapters/`: `directory`, `mailer`, `share`, `idp`. Services in `services.py` per app. Glossary terms from the PRD are used for class and table names. |
|
||||
| Data and formats | Primary keys are integers. Usernames stored lower-case; the exact `sAMAccountName` from AD is used for file names. Timestamps UTC. Money: none. Tokens: random, hashed at rest. |
|
||||
| State and cross-cutting | Mutations only in services, inside a transaction, with the audit entry (AD-4). Entity ownership per AD-19. Errors per AD-17. Settings via the registry (AD-13). No business logic in views or templates. |
|
||||
| Testing | Every adapter has an in-memory fake; automated tests use fakes. A manual smoke checklist runs a non-production instance of the same image against a test OU. |
|
||||
|
||||
## Stack
|
||||
|
||||
| Name | Version |
|
||||
| --- | --- |
|
||||
| Python | current supported release at scaffold; pin in image |
|
||||
| Django | 5.2 LTS (security fixes to April 2028) |
|
||||
| python-ldap | current at scaffold (maintained) |
|
||||
| Authlib | 1.7.x |
|
||||
| Pillow + pillow-heif | current at scaffold (pillow-heif 1.3+) |
|
||||
| nh3 + markdown-it-py | current at scaffold |
|
||||
| gunicorn | one worker, threads |
|
||||
| SQLite | WAL mode |
|
||||
| @fluentui/web-components | 3.x, built into the image |
|
||||
| Rich-text editor (browser, outputs Markdown) | chosen and pinned at scaffold after a web check; must be open source and bundled into the image |
|
||||
| Docker, Docker Compose | on a Linux host; share mounted by the host (CIFS) |
|
||||
|
||||
## Structural Seed
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[User browser] -->|HTTPS| RP[Zoraxy reverse proxy]
|
||||
RP -->|HTTP| P[Portal container]
|
||||
P -->|OIDC| AK[authentik]
|
||||
P -->|LDAPS| DC[Active Directory]
|
||||
P -->|SMTP| IO[IONOS mail]
|
||||
P -->|bind mount source rw, processed ro| SH[(Share via host CIFS mount)]
|
||||
P --> V[(volume /data SQLite)]
|
||||
P --> B[(volume /backups)]
|
||||
B -.copy job.-> VE[Veeam]
|
||||
SH -.read source.-> SY[PowerShell sync task on DC]
|
||||
SY -->|thumbnailPhoto| DC
|
||||
```
|
||||
|
||||
```text
|
||||
portal/
|
||||
config/ # settings, env parsing, setting registry
|
||||
adapters/ # directory, mailer, share, idp (+ fakes)
|
||||
accounts/ profile/ picture/ announcements/ adminui/ audit/
|
||||
scheduler/ # tick loop and jobs
|
||||
templates/ static/ # Fluent web components built into the image
|
||||
tests/
|
||||
Dockerfile # multi-stage, non-root
|
||||
compose.yaml
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
SESSION { string username }
|
||||
AUDIT_ENTRY { string actor }
|
||||
ANNOUNCEMENT ||--o{ OUTBOX_MAIL : queues
|
||||
OPT_OUT { string username }
|
||||
PENDING_TOKEN { string username }
|
||||
PICTURE_STATE { string username }
|
||||
SETTING { string key }
|
||||
JOB_RUN { string job }
|
||||
```
|
||||
|
||||
Users themselves are not stored: AD is the source of truth. The Portal stores only data keyed by `username`.
|
||||
|
||||
## Capability → Architecture Map
|
||||
|
||||
| Capability / Area | Lives in | Governed by |
|
||||
| --- | --- | --- |
|
||||
| Sign-in, roles, sessions (FR-1..FR-3) | `accounts`, `adapters/idp`, `adapters/directory` | AD-5, AD-6 |
|
||||
| Dashboard (FR-30) | `profile` (view only; calls `announcements` and `picture` services) | AD-1, AD-3 |
|
||||
| Preferences (FR-31) | `accounts` (cookie middleware) | AD-15 |
|
||||
| Throttling (FR-4, FR-6) | `accounts` | AD-7 |
|
||||
| Password change and Password Policy (FR-5..FR-7) | `profile` | AD-2, AD-3, AD-8 |
|
||||
| Profile data, e-mail change (FR-8..FR-11) | `profile` | AD-2, AD-3, AD-8, AD-9 |
|
||||
| Picture (FR-12..FR-15) | `picture`, `adapters/share` | AD-10 |
|
||||
| Announcements and mail (FR-16..FR-21) | `announcements`, `adapters/mailer` | AD-11, AD-12, AD-16 |
|
||||
| Admin UI and Status page (FR-22..FR-25, FR-32) | `adminui`, `audit`, `scheduler` job records | AD-4, AD-12, AD-13, AD-14, AD-16 |
|
||||
| Platform and operations (FR-26..FR-29) | `config`, `scheduler`, image | AD-12..AD-15, AD-18 |
|
||||
|
||||
## Deferred
|
||||
|
||||
- Container image location (registry or build on the host): decide at first deployment.
|
||||
- Default values for throttle limits, backup retention and the Pending timeout: set at implementation, within the setting registry.
|
||||
- The rich-text editor library: chosen at scaffold. The look and component set are in the UX documents.
|
||||
- Moving to PostgreSQL or more than one worker: only if the user count grows.
|
||||
- Alert e-mails or monitoring beyond the Admin UI: not needed now.
|
||||
- A staging environment: not available; the test OU takes its place.
|
||||
- Whether sign-out also ends the authentik session (single logout): decided at first deployment.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
topic: ping-mee self-service portal
|
||||
updated: 2026-10-15T09:24
|
||||
---
|
||||
|
||||
|
||||
- (event) Create intent; coaching/fast mode not yet chosen. Stakes not yet read.
|
||||
- (decision) Context: ping-mee.de is the user's large homelab (Jellyfin etc.); users authenticate against homelab Active Directory (domain ping-mee.local).
|
||||
- (decision) Scope v1 (user words): users self-edit email, displayname, name, surname, profile picture, phone number.
|
||||
- (decision) Scope v1: announcement feature so admin can announce planned downtimes and changes.
|
||||
- (decision) Design: resembles classic Microsoft web UI. Login via LDAPS.
|
||||
- (event) User supplied context-resources/*.ps1: existing profile-picture pipeline (fileshare source -> AD thumbnailPhoto/jpegPhoto/photo at 96px <=100KB -> Windows logon script builds account picture sizes, 448px HQ copy from share).
|
||||
- (decision) Users: family and friends, ~10 people. Yet wants polished and protected 'for big deployments' (tension with right-sizing; to probe).
|
||||
- (decision) Pictures: portal drops files on the fileshare (existing sync task does the AD write); portal renames to <username>.jpg if not already named so. Portal does NOT write thumbnailPhoto itself.
|
||||
- (decision) AD writes (other attributes): portal uses a dedicated service account with delegated privileges.
|
||||
- (decision) Announcements: delivered by e-mail AND shown on an announcement page in the portal.
|
||||
- (decision) Design: Office 365 (Fluent) look, not OWA 2010/Aero.
|
||||
- (decision) Stakes: homelab use first, keep option to deploy elsewhere someday (=> no hardcoded domain/OU/share paths).
|
||||
- (decision) Everything configurable, nothing baked in (domain, OU, paths, groups, SMTP, theme). Portal reachable from the INTERNET. No multi-domain. Admin UI wanted: upload backgrounds, change themes, settings.
|
||||
- (decision) Pictures: portal converts to JPEG, scales/sizes to need; user message 'updated within ~2 hours' (sync-task latency).
|
||||
- (decision) Writable scope: OU=Clients,OU=Organisation,DC=ping-mee,DC=local (configurable). Attributes: sn, displayName, givenName, mail, name, cn. Mail change requires confirmation e-mail.
|
||||
- (assumption) GAP: phone number was in the original request but is not in the attribute list (telephoneNumber/mobile?). Also name/cn are the RDN: changing needs ModifyDN, not attribute modify.
|
||||
- (decision) Announcements: publishers = members of a configurable AD group; SMTP server configurable (IONOS); opt-out/opt-in for e-mails wanted; one-way text primary, scheduled downtime window / severity / expiry as OPTIONAL features.
|
||||
- (decision) Deployment: Docker container on a Linux box.
|
||||
- (decision) Phone: both telephoneNumber and mobile. name and cn REMOVED from concept (no RDN renames).
|
||||
- (decision) Behind reverse proxy (TLS there). Login throttling wanted. Asked: authentik IdP between AD and portal as option (user has an authentik server).
|
||||
- (decision) Scope: ship everything (user: not a rational project, homelab is oversized by design). No phasing imposed; sequencing left to downstream.
|
||||
- (decision) E-mail announcements: default opt-IN; on opt-out user is told they won't get downtime notifications and can opt back in via portal.
|
||||
- (decision) Secrets via Docker environment variables.
|
||||
- (decision) NEW SCOPE: password change by users at any time (admin currently resets manually) - core success criterion. Success = no manual password resets AND one clean all-in-one package solving these homelab problems.
|
||||
- (decision) Password: users can CHANGE only (current+new); reset stays a manual admin process (out of scope). E-mail change requires current password + confirmation e-mail. Users with expired/must-change passwords can still log in (mechanism unresolved with OIDC).
|
||||
- (decision) Login via OIDC (authentik) for initial authentication only; all writes stay LDAPS with service account. Prefers OIDC over LDAPS login.
|
||||
- (decision) Problems solved: password change, profile info, profile pictures, announcement channel. 'Clean' = no good solution exists for these today; one all-in-one package. PowerShell scripts (sync, logon, registry) stay.
|
||||
- (decision) Mode: FAST PATH chosen. Draft brief with [ASSUMPTION] tags.
|
||||
- (assumption) Success criterion tension: forgotten passwords still need manual reset by admin, so 'no manual password work' only holds for known-password changes.
|
||||
- (assumption) Expired-password login: authentik/AD OIDC login likely fails for expired passwords; mechanism needs design.
|
||||
- (decision) Expired-password behavior with authentik unknown; to be tested. Password change confirmed: user enters current+new in portal, applied over LDAPS.
|
||||
- (decision) Security success criterion: no injections, no session takeover, no cross-user read/write, throttled logins.
|
||||
- (change) Draft v1 written: brief.md + addendum.md; security criterion made concrete, open question on expired passwords reworded.
|
||||
- (event) Polish pass (bmad-review structure+prose) applied; brief status set to final.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Addendum: Technical Context for Downstream Documents
|
||||
|
||||
## Existing picture pipeline (context-resources/)
|
||||
- `AD-ProfilePicture-Sync.ps1` (DC, scheduled task): reads `<sam>.jpg` from the fileshare source folder, falls back to a default image, writes a 96px copy and a 448px copy to a processed folder, and sets `thumbnailPhoto`, `jpegPhoto` and `photo` in AD from the 96px copy (limit 100 KB). It writes MD5 files for change detection but does not yet compare them, so every run reprocesses every user. Target OU in the script: `OU=Organisation,DC=ping-mee,DC=local`.
|
||||
- `Set-LocalAccountPicture.ps1` (user logon script): reads `thumbnailPhoto` and builds Windows account-picture sizes (32, 40, 48, 96, 192, 200, 240, 448). Large sizes use the 448px copy from the share. It uses `$roamingDir` before defining it, which is a bug.
|
||||
- `Set-AccountPictureRegistry-System.ps1` (startup): applies registry entries from manifests left by the logon script.
|
||||
- The portal's job is only to put a correct `<username>.jpg` in the share's source folder. Format, size and naming constraints come from these scripts.
|
||||
|
||||
## AD write scope
|
||||
- Writable OU: `OU=Clients,OU=Organisation,DC=ping-mee,DC=local` (configurable).
|
||||
- Attributes: `givenName`, `sn`, `displayName`, `mail`, `telephoneNumber`, `mobile`. Password via the AD password mechanism over LDAPS.
|
||||
- Rejected: editing `cn` and `name`. They are the object's RDN and would need ModifyDN renames, which change the DN and risk breaking apps.
|
||||
|
||||
## Security notes raised during discovery
|
||||
- Internet exposure plus LDAP binds can lock AD accounts if an attacker hammers logins. Throttling must sit in front of any bind. OIDC via authentik moves the first factor there.
|
||||
- E-mail change feeds announcements and any future reset path. Chain risk: hijacked session, then e-mail change. Mitigated by requiring the current password and a confirmation link.
|
||||
- Secrets (service account, SMTP) come from Docker environment variables, not the admin UI.
|
||||
|
||||
## Integrations
|
||||
- IdP: authentik via OIDC. Keep it generic OIDC so other providers work. Not verified: whether authentik can write passwords back to AD. The portal does not rely on it.
|
||||
- Mail: configurable SMTP, IONOS in the homelab.
|
||||
- Reverse proxy in front terminates TLS.
|
||||
|
||||
## Design
|
||||
- Office 365 (Fluent) look, not OWA 2010 or Windows 7 Aero. Admin can change theme and upload backgrounds.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Product Brief: ping-mee Self-Service Portal"
|
||||
status: final
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# Product Brief: ping-mee Self-Service Portal
|
||||
|
||||
## Executive Summary
|
||||
|
||||
ping-mee.de is a large homelab that family and friends use, around ten people, for services such as Jellyfin. All accounts live in one Active Directory. Today, almost anything a user wants changed (a password, a name, a phone number, a profile picture) means asking the admin. There is also no good place to tell everyone about planned downtime.
|
||||
|
||||
The ping-mee Self-Service Portal is a single web application that closes those gaps. Signed-in users change their own password and profile details and upload a profile picture. The admin publishes announcements that reach users by e-mail and on a page in the portal. It is built for the homelab first, reachable from the internet behind the existing reverse proxy, and configurable enough to run elsewhere later. The homelab is deliberately oversized for ten users, and so is this project. That is part of its purpose, not an accident.
|
||||
|
||||
## The Problem
|
||||
|
||||
- **Password changes are manual.** A user who wants a new password has to ask the admin. This is the main pain.
|
||||
- **Profile data is admin-maintained.** Name, display name, e-mail and phone are edited by hand in AD.
|
||||
- **Profile pictures are awkward.** The pipeline works (a file on a share, a scheduled sync to AD, a logon script on Windows clients), but a user has no way to supply the file themselves.
|
||||
- **No announcement channel.** There is no single place to announce downtime or changes.
|
||||
- **No existing tool fits.** Nothing handles these four things together, cleanly, in an AD-based homelab.
|
||||
|
||||
## The Solution
|
||||
|
||||
One portal, one login. After signing in through the existing authentik server (OIDC), a user can:
|
||||
|
||||
- **Change their password** by entering the current one and a new one.
|
||||
- **Edit profile data**: first name, last name, display name, e-mail, telephone number and mobile number.
|
||||
- **Upload a picture.** The portal converts it to a JPEG sized for the pipeline and writes `<username>.jpg` to the fileshare. The existing PowerShell sync then updates AD within roughly two hours, and the portal tells the user so.
|
||||
- **Read announcements** on a page in the portal, and receive them by e-mail.
|
||||
|
||||
Changing an e-mail address needs the current password and a confirmation link sent to the new address. Members of a configurable AD group can publish announcements. The admin has a UI to manage themes, background images and settings. The look follows Office 365.
|
||||
|
||||
All writes to AD go through LDAPS with a dedicated service account that is limited to a configurable OU and a short list of attributes.
|
||||
|
||||
## What Makes This Different
|
||||
|
||||
There is no moat. The value is fit and cleanliness: a portal built around this exact setup (AD, authentik, the existing picture pipeline) that does four things well in one container, instead of four partial solutions. It is also configurable throughout, so it is not tied to one domain.
|
||||
|
||||
## Who This Serves
|
||||
|
||||
- **Users** are about ten family members and friends of the homelab, with mixed technical skill. They want to change their own details without asking anyone.
|
||||
- **The admin** (the homelab owner) wants to stop doing manual password and profile changes and wants an announcement channel. This person is also the main builder and operator.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Users change their own passwords without admin involvement. [ASSUMPTION: "manual" means known-password changes only; forgotten passwords stay manual, see Open Questions.]
|
||||
- Users update their own profile fields and pictures without admin involvement.
|
||||
- Every planned downtime or change can be announced through the portal, and opted-in users receive it by e-mail.
|
||||
- Nothing is hard-coded to ping-mee: domain, OU, group, share path, SMTP and theme all come from configuration.
|
||||
- The portal runs safely on the internet. Concretely: injection attacks (LDAP, SQL, script) do not work, a stolen or hijacked session does not give lasting access, and a signed-in user can never read or write another user's data. Logins are throttled.
|
||||
|
||||
## Scope
|
||||
|
||||
**In for the first version (everything below is wanted, sequencing is for later):**
|
||||
- Login via OIDC (authentik).
|
||||
- Password change with the current password.
|
||||
- Profile edit for `givenName`, `sn`, `displayName`, `mail`, `telephoneNumber` and `mobile`.
|
||||
- E-mail change with password and confirmation link.
|
||||
- Picture upload, conversion and rename to `<username>.jpg` on the share.
|
||||
- Announcements: text, e-mail and portal page, with user opt-in (default) and opt-out. Opting out shows a notice that downtime notifications will stop and that the user can opt back in. Severity, downtime windows and expiry are optional features.
|
||||
- Admin UI for themes, backgrounds and non-secret settings.
|
||||
- Configuration and secrets via Docker environment variables.
|
||||
- Delivery as a Docker container on a Linux host.
|
||||
- Login throttling.
|
||||
|
||||
**Out:**
|
||||
- Password reset for forgotten passwords. The admin does this manually.
|
||||
- Editing `cn` or `name`, which means no object renames.
|
||||
- Multi-domain support.
|
||||
- Replacing the PowerShell scripts, which stay as they are.
|
||||
|
||||
## Open Questions and Risks
|
||||
|
||||
- **Expired passwords.** The admin wants users with expired or "must change" passwords to still be able to log in and change them. Login goes through authentik, and it may refuse those users. How authentik behaves here is unknown and has to be tested before the design is fixed.
|
||||
- **Forgotten passwords.** They still need the admin, so "no manual password work" is only partly true. Confirm that this is acceptable.
|
||||
- **Internet exposure.** The portal holds an AD service account and is reachable from outside. This is the largest risk and gets its own design attention.
|
||||
- **Scope.** Shipping everything in one go delays the first benefit to users.
|
||||
|
||||
## Vision
|
||||
|
||||
A single, polished home for everything a user needs from the homelab's identity system, and the first place the admin looks when something changes. If it works well in this homelab, it could be released for other AD-based homelabs, because nothing in it assumes ping-mee.
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
---
|
||||
tracker_id: ""
|
||||
key: ""
|
||||
type: epic
|
||||
title: "A user manages their own account"
|
||||
parent: initiative-ping-mee-portal
|
||||
covers: [CAP-2, CAP-3, CAP-4]
|
||||
after: []
|
||||
assignee: ""
|
||||
risk: high
|
||||
---
|
||||
|
||||
# A user manages their own account
|
||||
|
||||
## Description
|
||||
|
||||
Users change their own password, profile details, e-mail address and profile picture in the live portal. This is the main reason the portal exists: it removes the admin from known-password changes and routine profile edits.
|
||||
|
||||
## Outcome
|
||||
|
||||
Family and friends change their own password, details and picture without the admin; the first part of the spec's success signal.
|
||||
|
||||
## Done when
|
||||
|
||||
1. A user changes their password with the AD password policy shown first, other sessions end, and a confirmation e-mail arrives.
|
||||
2. A user edits first name, last name, display name, telephone and mobile, and the values appear in AD.
|
||||
3. An e-mail change applies only after the current password and a confirmation from the new address, and the old address receives a 24-hour undo link whose use ends sessions and notifies the Admin.
|
||||
4. A user uploads a JPEG, PNG, HEIC or WebP picture up to 10 MB, crops it, sees Pending until the sync task has processed it, and can remove it.
|
||||
5. Injection strings and attempts on another user's data change nothing, and every action leaves an audit entry.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The password, profile, e-mail and picture features and the share adapter. Not announcements or Admin pages, and no change to the PowerShell scripts. Touch point owned here: the picture file share and sync task.
|
||||
|
||||
## References
|
||||
|
||||
- parent — _bmad-output/initiative-ping-mee-portal/initiative-ping-mee-portal.md, section Requirements
|
||||
- spec — _bmad-output/specs/spec-ping-mee-self-service-platform/SPEC.md, CAP-2, CAP-3, CAP-4
|
||||
- architecture — ARCHITECTURE-SPINE.md, AD-2, AD-3, AD-4, AD-8, AD-9, AD-10
|
||||
- design — DESIGN.md and EXPERIENCE.md; mock-ups key-password.html and key-picture-pending.html
|
||||
- constraint — SPEC.md, section Constraints
|
||||
|
||||
## Notes
|
||||
|
||||
- Waits on epic-platform-and-sign-in because: it needs sign-in, sessions, the directory adapter, throttling, the audit service and the mail outbox.
|
||||
- Not yet sliced: this epic is broken into stories when it is selected.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
tracker_id: ""
|
||||
key: ""
|
||||
type: epic
|
||||
title: "The admin announces changes and runs the portal"
|
||||
parent: initiative-ping-mee-portal
|
||||
covers: [CAP-5, CAP-6, CAP-7, CAP-8, CAP-10]
|
||||
after: []
|
||||
assignee: ""
|
||||
risk: medium
|
||||
---
|
||||
|
||||
# The admin announces changes and runs the portal
|
||||
|
||||
## Description
|
||||
|
||||
Publishers announce planned downtime and changes, users receive them by e-mail and see them on the dashboard, and the Admin manages the look, reads the audit log and watches the status of backups, mail and background jobs. It finishes the portal and proves it ready for the internet.
|
||||
|
||||
## Outcome
|
||||
|
||||
Every planned downtime reaches opted-in users by e-mail and banner, and the admin can see that the portal is healthy; the second part of the spec's success signal.
|
||||
|
||||
## Done when
|
||||
|
||||
1. A Publisher writes an announcement with an always-set severity and optional window, expiry and schedule, and it appears as a banner and in the list at the right time.
|
||||
2. Every user in the OU with a valid e-mail address and no opt-out receives it, and the Publisher sees counts and reasons beforehand.
|
||||
3. The dashboard shows active announcements and the account card including the password date.
|
||||
4. The Admin changes the accent colour with a contrast warning, uploads a background, edits settings, reads the audit log and sees backup age, failed e-mails and job runs.
|
||||
5. Consistent backup files appear on schedule, the nightly AD reconciliation runs, and audit retention applies.
|
||||
6. Security tests (injection, cross-user, throttling, sessions) and the accessibility pass succeed, and a smoke checklist exists for the test OU.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Announcements, announcement e-mail, the dashboard, the Admin pages, the maintenance jobs and the final hardening. Not account features (epic-account-self-service). Touch point owned here: the Veeam copy job that copies the backup folder.
|
||||
|
||||
## References
|
||||
|
||||
- parent — _bmad-output/initiative-ping-mee-portal/initiative-ping-mee-portal.md, section Requirements
|
||||
- spec — _bmad-output/specs/spec-ping-mee-self-service-platform/SPEC.md, CAP-5, CAP-6, CAP-7, CAP-8, CAP-10
|
||||
- architecture — ARCHITECTURE-SPINE.md, AD-4, AD-11 to AD-16
|
||||
- design — DESIGN.md and EXPERIENCE.md; mock-ups key-announcement-editor.html, key-admin-appearance.html, key-admin-status.html, key-admin-audit.html, key-dashboard.html
|
||||
- constraint — SPEC.md, section Constraints
|
||||
|
||||
## Notes
|
||||
|
||||
- Waits on epic-account-self-service because: the dashboard account card shows picture status and uses the profile data it creates.
|
||||
- Not yet sliced: this epic is broken into stories when it is selected.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
---
|
||||
tracker_id: ""
|
||||
key: ""
|
||||
type: epic
|
||||
title: "A user can sign in to the live portal"
|
||||
parent: initiative-ping-mee-portal
|
||||
covers: [CAP-1, CAP-9]
|
||||
after: []
|
||||
assignee: ""
|
||||
risk: high
|
||||
---
|
||||
|
||||
# A user can sign in to the live portal
|
||||
|
||||
## Description
|
||||
|
||||
The platform baseline: the container, configuration, the Active Directory adapter, sign-in through authentik, sessions with roles, the page shell with preferences, and the scheduler with the mail outbox. When it is done, a person signs in to the portal running on the Linux host and sees their own name from Active Directory in the new layout; every later epic builds on it.
|
||||
|
||||
## Outcome
|
||||
|
||||
The admin can sign in to the live portal through authentik and see their own name from AD in the page shell, which proves every layer connects.
|
||||
|
||||
## Requirements
|
||||
|
||||
Owned from the spec: CAP-1 (sign-in, roles, sessions, throttling base) and the shell and preference parts of CAP-9. Groundwork for CAP-10 (container, configuration, health) and CAP-8 (audit foundation) is laid here; their completion is in the third epic.
|
||||
|
||||
- CAP-1 — spec SPEC.md, Capabilities (PRD FR-1 to FR-4)
|
||||
- CAP-9, shell part — PRD FR-29, FR-31; EXPERIENCE.md Foundation and Information Architecture
|
||||
- CAP-10, groundwork — PRD FR-26, FR-27; architecture AD-13, AD-18
|
||||
|
||||
## Done when
|
||||
|
||||
1. A user in the test OU signs in through the portal's own OIDC application in authentik and lands in the page shell showing their name from AD.
|
||||
2. A user outside the Publisher and Admin groups cannot open Publisher or Admin pages by direct address; a disabled or locked AD account is refused.
|
||||
3. Sessions end after 1 hour idle and 4 hours total; repeated sensitive requests are refused before any AD call.
|
||||
4. The container starts only with valid environment variables, answers its health check without AD, and runs on the Linux host behind Zoraxy.
|
||||
5. Theme, language and German form of address can be chosen and are kept per device; the layout works on a phone.
|
||||
6. The scheduler ticks and the mail outbox sends a test e-mail through the configured SMTP server.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The portal's platform and sign-in only. Not profile, password or picture features (epic-account-self-service), not announcements or the Admin pages (epic-announcements-and-administration). Touch points owned here: Active Directory, authentik, Zoraxy, IONOS mail.
|
||||
|
||||
## References
|
||||
|
||||
- parent — _bmad-output/initiative-ping-mee-portal/initiative-ping-mee-portal.md, section Requirements
|
||||
- spec — _bmad-output/specs/spec-ping-mee-self-service-platform/SPEC.md, CAP-1, CAP-9, CAP-10
|
||||
- architecture — _bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md, AD-1 to AD-7, AD-11 to AD-13, AD-15, AD-18, AD-19
|
||||
- design — _bmad-output/ux-designs/ux-ping-mee-self-service-platform-2026-10-15/DESIGN.md and EXPERIENCE.md, mock-ups key-dashboard.html
|
||||
- constraint — SPEC.md, section Constraints
|
||||
|
||||
## Notes
|
||||
|
||||
- Decision: tracer bullet is the scaffold followed by sign-in; the first visible result is the user's name in the shell at entry 6 (2026-10-15).
|
||||
- Decision: a closing Refactor sweep is proposed by default; no end-to-end suite for this epic unless the user asks (2026-10-15).
|
||||
- Decision: the checkpoints agreed in the spec's stories carry over: plan review on entries 1, 4 and 5, and a pause after entry 5 (user, 2026-10-15).
|
||||
- Assumption: Zoraxy appends the real client IP as the last `X-Forwarded-For` entry; to be checked with a test request when the container is first deployed behind it.
|
||||
- Assumption: the exact AD attribute names for the password policy are verified in the test OU during entry 4.
|
||||
- Source conflict: none found.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Breakdown of epic-platform-and-sign-in. The order of the tables is the build order.
|
||||
|
||||
[[entry]]
|
||||
id = 1
|
||||
type = "story"
|
||||
title = "Project scaffold, configuration and container"
|
||||
description = "Creates the Django project, environment-variable configuration that refuses to start when invalid, the multi-stage Docker image and compose file, a health check that does not call AD, JSON logging, security headers with a strict content-security policy, and the test setup with adapter fakes."
|
||||
verify = "The container starts with valid environment variables, refuses to start with an invalid one, and answers its health check on the Linux host."
|
||||
covers = ["CAP-10"]
|
||||
after = []
|
||||
hitl = false
|
||||
risk = "medium"
|
||||
plan_checkpoint = true
|
||||
references = ["_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-13", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-18"]
|
||||
|
||||
[[entry]]
|
||||
id = 2
|
||||
type = "story"
|
||||
title = "Prepare the test environment"
|
||||
description = "A person creates a test OU in the production AD with a service account scoped to it and the delegated rights from the spine, supplies the internal CA bundle, creates a dedicated OIDC application in the production authentik restricted to test users, notes the Zoraxy route and trusted-proxy value, and prepares a test mailbox."
|
||||
verify = "The service account can read and write a test user in the test OU over LDAPS, the authentik application lets a test user sign in, and the values are written down for the build."
|
||||
covers = ["CAP-1"]
|
||||
after = []
|
||||
hitl = true
|
||||
risk = "medium"
|
||||
notes = ["Authentik is production only; never change an existing authentik application.", "I can set up a test OU, but not a separate authentik."]
|
||||
|
||||
[[entry]]
|
||||
id = 3
|
||||
type = "story"
|
||||
title = "Audit, throttle and session foundations"
|
||||
description = "Builds the audit service, database-backed throttle counters, and server-side sessions with the username in its own column and a kill-by-user function; the other epics write audit entries and end sessions through these."
|
||||
verify = "Tests show an audit entry is written in the same transaction as a change, a throttle refuses after its limit, and ending a user's sessions removes every session of that user."
|
||||
covers = ["CAP-1", "CAP-8"]
|
||||
after = [1]
|
||||
hitl = false
|
||||
risk = "medium"
|
||||
references = ["_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-4", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-6", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-7"]
|
||||
|
||||
[[entry]]
|
||||
id = 4
|
||||
type = "story"
|
||||
title = "Directory adapter"
|
||||
description = "Builds the only code that talks to AD: lookup by sAMAccountName in the configured OU, group membership, account state, password verification, password policy read and the allow-listed writes, with an in-memory fake; sign-in and the account features call its interface."
|
||||
verify = "Against the test OU the adapter reads and writes allowed attributes over LDAPS with certificate verification, refuses others, and returns the password policy."
|
||||
covers = ["CAP-1", "CAP-2", "CAP-3"]
|
||||
after = [1, 2]
|
||||
hitl = false
|
||||
risk = "high"
|
||||
plan_checkpoint = true
|
||||
references = ["_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-1", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-2"]
|
||||
notes = ["Test only against the test OU, never the production OU."]
|
||||
|
||||
[[entry]]
|
||||
id = 5
|
||||
type = "story"
|
||||
title = "Sign-in with OIDC and roles"
|
||||
description = "Implements sign-in through the identity provider with the authorization-code flow, session creation, roles from AD group membership with a short cache, refusal of disabled or locked accounts, and the trusted-proxy client IP rule, using the directory adapter's interface."
|
||||
verify = "A test user signs in through authentik and reaches a page showing their own name; a user outside the Publisher and Admin groups cannot open those pages; a disabled account is refused."
|
||||
covers = ["CAP-1"]
|
||||
after = [2, 3, 4]
|
||||
hitl = false
|
||||
risk = "high"
|
||||
plan_checkpoint = true
|
||||
done_checkpoint = true
|
||||
references = ["_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-5", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-7"]
|
||||
|
||||
[[entry]]
|
||||
id = 6
|
||||
type = "story"
|
||||
title = "Page shell, preferences and translations"
|
||||
description = "Builds the base layout with the Fluent components bundled into the image: top bar, left menu with role-based groups, avatar menu for theme, language and German form of address, German and English text, light and dark themes, and the phone layout."
|
||||
verify = "After sign-in the shell shows the user's name, the menu groups match their role, the three preferences persist per device, and the layout works at phone width."
|
||||
covers = ["CAP-9"]
|
||||
after = [5]
|
||||
hitl = false
|
||||
risk = "medium"
|
||||
references = ["_bmad-output/ux-designs/ux-ping-mee-self-service-platform-2026-10-15/mockups/key-dashboard.html", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-15", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-16"]
|
||||
|
||||
[[entry]]
|
||||
id = 7
|
||||
type = "story"
|
||||
title = "Scheduler and mail outbox"
|
||||
description = "Builds the single 30-second scheduler tick with idempotent jobs and job-run records, the mailer adapter with the outbox, retries, backoff and rate limit, and a fake mailer; other epics only call its enqueue function."
|
||||
verify = "A queued test e-mail is sent through the configured SMTP server, a failed send is retried and recorded, and a restart sends nothing twice."
|
||||
covers = ["CAP-6", "CAP-10"]
|
||||
after = [3]
|
||||
hitl = false
|
||||
risk = "medium"
|
||||
references = ["_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-11", "_bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md#ad-12"]
|
||||
|
||||
[[entry]]
|
||||
id = 8
|
||||
type = "story"
|
||||
title = "Refactor sweep"
|
||||
description = "Cleans up the epic's code using the plans and the review findings deferred during the epic; scope pushed out of another story is a new story, not part of this one."
|
||||
verify = "The test suite still passes and the deferred review findings from entries 1 to 7 are resolved or recorded."
|
||||
covers = []
|
||||
after = [1, 2, 3, 4, 5, 6, 7]
|
||||
hitl = false
|
||||
risk = "low"
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
tracker_id: ""
|
||||
key: ""
|
||||
type: initiative
|
||||
title: "Users run their own account and the admin has one announcement channel"
|
||||
parent: none
|
||||
covers: [CAP-1, CAP-2, CAP-3, CAP-4, CAP-5, CAP-6, CAP-7, CAP-8, CAP-9, CAP-10]
|
||||
after: []
|
||||
assignee: ""
|
||||
risk: high
|
||||
---
|
||||
|
||||
# Users run their own account and the admin has one announcement channel
|
||||
|
||||
## Description
|
||||
|
||||
The ping-mee Self-Service Portal lets about ten homelab users change their own password, details and profile picture, and lets the admin announce planned downtime by e-mail and on a page. The spec owns the capabilities, constraints and non-goals; this initiative delivers them to production, internet-facing, in a form that can later be deployed elsewhere.
|
||||
|
||||
## Outcome
|
||||
|
||||
Family and friends change their own password, details and picture without the admin, and every planned downtime reaches opted-in users by e-mail and banner; the spec's success signal is the measure.
|
||||
|
||||
## Requirements
|
||||
|
||||
The numbered spec is the requirement source: CAP-1 to CAP-10 in `_bmad-output/specs/spec-ping-mee-self-service-platform/SPEC.md`. Requirement-level detail (FR-1 to FR-32) is in its PRD companion.
|
||||
|
||||
## Done when
|
||||
|
||||
1. A family member changes their own password, name, phone number and picture in the live portal with no admin help.
|
||||
2. A scheduled downtime announcement reaches every opted-in user by e-mail and shows as a banner on the dashboard.
|
||||
3. Injection, cross-user access and brute-force attempts against the live portal fail and appear in the audit log.
|
||||
4. The portal runs from one Docker image configured only by environment variables on the Linux host behind the reverse proxy.
|
||||
5. Consistent database backup files appear in the backup folder and the status page shows their age.
|
||||
6. Every capability CAP-1 to CAP-10 is live for all users, not behind a flag.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The portal and its adapters. Not an Active Directory admin console, not a password-reset flow, not a replacement for the PowerShell picture scripts or the identity provider; see the spec's non-goals. Tracer path: sign in through authentik, look up the user in the test OU, and see their name in the new page shell.
|
||||
|
||||
- Touch point: Active Directory (LDAPS) — the portal reads and writes through a service account; owner: epic-platform-and-sign-in
|
||||
- Touch point: authentik (production) — a dedicated OIDC application for the portal; owner: epic-platform-and-sign-in
|
||||
- Touch point: Zoraxy reverse proxy — trusted-proxy and client-IP settings; owner: epic-platform-and-sign-in
|
||||
- Touch point: IONOS mail — SMTP credentials for the outbox; owner: epic-platform-and-sign-in
|
||||
- Touch point: picture file share and the PowerShell sync task — write source files, read processed hashes, no script changes; owner: epic-account-self-service
|
||||
- Touch point: Veeam copy job — copies the backup folder; owner: epic-announcements-and-administration
|
||||
|
||||
## References
|
||||
|
||||
- spec — _bmad-output/specs/spec-ping-mee-self-service-platform/SPEC.md, section Capabilities
|
||||
- spec companion — _bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15/prd.md, sections Features and Cross-Cutting Requirements
|
||||
- architecture — _bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md, section Invariants & Rules (the decisions every epic adopts: AD-1 to AD-19)
|
||||
- design — _bmad-output/ux-designs/ux-ping-mee-self-service-platform-2026-10-15/DESIGN.md and EXPERIENCE.md, with the mock-ups in mockups/
|
||||
- constraint — SPEC.md, section Constraints
|
||||
|
||||
## Notes
|
||||
|
||||
- Decision: three epics cut by "usable early": platform and sign-in, then account self-service, then announcements and administration (user, 2026-10-15).
|
||||
- Decision: the spec stays in `_bmad-output/specs/spec-ping-mee-self-service-platform/` and is cited by path, because its companions use relative links.
|
||||
- Decision: the epic rule "split only for a distinct outcome, owner or a part usable early" is saved for later slicing (user, 2026-10-15).
|
||||
- Decision: no spike; the user is not worried about any one area (2026-10-15).
|
||||
- Assumption: the test environment is a test OU in the production AD with a scoped service account, and a dedicated OIDC application in the production authentik restricted to test users; there is no staging environment.
|
||||
- Parked: nothing from the spec is deferred.
|
||||
- Waits on epic-platform-and-sign-in because: every other epic uses its sign-in, directory adapter, sessions and mail outbox.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Epics of the initiative, in build order.
|
||||
|
||||
[[epic]]
|
||||
id = 1
|
||||
slug = "epic-platform-and-sign-in"
|
||||
title = "A user can sign in to the live portal"
|
||||
covers = ["CAP-1", "CAP-9"]
|
||||
|
||||
[[epic]]
|
||||
id = 2
|
||||
slug = "epic-account-self-service"
|
||||
title = "A user manages their own account"
|
||||
covers = ["CAP-2", "CAP-3", "CAP-4"]
|
||||
after = [{ epic = 1, needs = "sign-in and sessions, the directory adapter, throttling, the audit service and the mail outbox" }]
|
||||
|
||||
[[epic]]
|
||||
id = 3
|
||||
slug = "epic-announcements-and-administration"
|
||||
title = "The admin announces changes and runs the portal"
|
||||
covers = ["CAP-5", "CAP-6", "CAP-7", "CAP-8", "CAP-10"]
|
||||
after = [{ epic = 2, needs = "the profile and picture data the dashboard account card shows" }]
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
topic: ping-mee self-service portal PRD
|
||||
updated: 2026-10-15T10:22
|
||||
---
|
||||
|
||||
|
||||
- (event) Create intent. Input: brief at _bmad-output/briefs/brief-ping-mee-self-service-platform-2026-10-15/ (brief.md + addendum.md). context-resources/ PowerShell scripts were read earlier but the folder is missing from disk now (untracked; likely removed by elevated cleanup).
|
||||
- (decision) Stakes: internal tool built to a higher standard (homelab first, option to deploy elsewhere). Mode: COACHING path, entry point Vision + Features; short journeys for user, publisher, admin.
|
||||
- (decision) User approved web research on authentik behavior with expired/must-change AD passwords and password write-back.
|
||||
- (event) Research: authentik LDAP source supports 'User password writeback' to AD over ldaps (needs delegated rights); AD returns bind error 49/data 532 (expired) / 773 (must reset) which blocks the bind; authentik AD docs say nothing on expired-password handling.
|
||||
- (decision) Expired/must-change passwords: OUT of portal. User contacts admin, admin resets in AD, then user logs in and changes it. No separate expired-password page. (Resolves brief open question.)
|
||||
- (decision) Roles: three separate concepts - user, announcement publishers (configurable AD group), admin (a SEPARATE configurable AD group).
|
||||
- (assumption) Vision draft and 7-feature list (sign-in, password change, profile edit, picture, announcements, admin UI, platform/ops) presented; user answered only the two push-backs - treated as accepted unless corrected.
|
||||
- (decision) Sign-in/password: OIDC identity maps to AD via preferred_username == sAMAccountName (confirmed identical). Roles via AD group membership checked by portal. Session lifetime: 1 hour.
|
||||
- (decision) Password change: per-user failed-attempt cap before touching AD (e.g. 5 then pause; make configurable - agreed); after change, end user's other sessions AND send 'password changed' e-mail.
|
||||
- (decision) Profile: displayName is free text. E-mail change: notify OLD address and include an UNDO link valid 24h; confirmation link to NEW address (1h assumed, single use).
|
||||
- (decision) Pictures: accept jpeg, png, svg, heic, webp, max 10MB; in-browser crop/edit tool wanted; output square JPEG ~512px (assumed; sync makes 96/448 itself from source). Show current picture read from AD attribute with PENDING status until new picture is live. User can remove picture (delete <username>.jpg -> sync falls back to default).
|
||||
- (decision) Audit log of profile/password/e-mail/picture/announcement actions wanted.
|
||||
- (assumption) SVG upload is a security risk (XSS/XXE/external refs); must be rasterized server-side in a sandbox, original never served - to confirm with user.
|
||||
- (assumption) Pending detection idea: sync script writes processed/<sam>.md5 = MD5 of the source file; portal can compare with MD5 of file it wrote (needs read access to processed path) - to confirm.
|
||||
- (decision) Pending status via processed/<sam>.md5 comparison accepted; dependency: PowerShell sync script stays unchanged; portal needs read on processed path + write on source path (both configurable).
|
||||
- (decision) SVG REMOVED from accepted picture formats (security). Supported: jpeg, png, heic, webp, 10MB.
|
||||
- (decision) E-mail undo link: ends all user sessions, tells user to change password, notifies admin.
|
||||
- (decision) Audit log: 12 months retention (configurable), readable by admins in admin UI only; records who/what/when/IP, never passwords or picture contents.
|
||||
- (decision) Announcements: publisher writes title+body (plain text + Markdown, rendered safely/sanitized); draft or publish; optional severity (info/warning/outage), downtime window, expiry; edit/withdraw; scheduled publish time wanted; edit after send => explicit 'notify users again' option (no auto re-send).
|
||||
- (decision) Announcements readable only when signed in (no public status page; e-mail covers portal-down case). Active ones banner on home page. Opt-in default; opt-out = receive nothing, no outage exception.
|
||||
- (decision) Admin UI: runtime = cosmetic/behaviour settings only (theme, backgrounds, portal name, session length, attempt cap, audit retention). Environment-only = anything identifying a system or holding a secret incl. AD/LDAPS, service account, SMTP, OIDC, paths, and the publisher/admin group names. Admin background uploads re-encoded + size-limited and served publicly (login page). Admins cannot edit other users' profile/picture/password; admin manages only the portal.
|
||||
- (assumption) Feature 7 unanswered by user, set as assumptions: embedded DB (SQLite) in mounted volume; interface languages TBD (German/English); WCAG 2.1 AA target; env-var config; structured logs; secure cookies/CSRF/headers.
|
||||
- (change) Draft PRD v1 written (prd.md + addendum.md): 29 FRs across 7 features, UJ-1..4, SM-1..5 + counter-metrics, Q-1..Q-7.
|
||||
- (event) Reviewer gate: rubric walker run inline, review-rubric.md written. Grade Good; 3 high (FR-4 undefined, FR-10/11 GET links, recipient list), ~9 medium, 3 low.
|
||||
- (change) Review fixes applied: FR-4 rewritten (endpoints, own limits, OIDC throttles sign-in); POST-confirm for e-mail confirm/undo links; backgrounds signed-in only (resolves §2.2/FR-22 conflict); disabled/locked account consequence in FR-1; glossary + Audit Log, Draft, Withdrawn, Severity, Downtime Window; Vision casing. Deferred Q-8 (time zone), Q-10 (md5 timing). Q-9 recipient list pending user decision.
|
||||
- (decision) Q-9 resolved (option A): announcement recipients = all Users in the OU with a valid mail value, signed in or not, minus explicit opt-outs; users without mail are skipped and recorded; portal stores only opt-outs. Q-9 removed from open questions (numbering gap kept: Q-1..Q-8, Q-10).
|
||||
- (override) Supersedes earlier admin-UI entry: backgrounds are served to signed-in Users only (not publicly/login page). Supersedes picture entry: SVG removed.
|
||||
- (event) Memlog audit done: all decisions captured in prd.md or addendum.md; superseded entries noted. context-resources/ restored by user and verified against addendum (md5 at line 114 before AD write at 117; uppercase hex, trailing newline).
|
||||
- (event) Input reconciliation (brief, brief addendum, scripts) done: no dropped requirements; only Office 365 look is thin (one assumption in FR-22/§8.4) - handed to bmad-ux.
|
||||
- (decision) Deferred open questions (owner Ebbe, revisit at bmad-architecture): Q-1 share unreachable, Q-2 mail retry/IONOS limits, Q-3 languages, Q-4 absolute session max, Q-5 data store/backup, Q-6 removed-AD-user cleanup, Q-7 removed-picture pending, Q-8 time zone, Q-9 md5 timing. None block UX.
|
||||
- (event) Polish done (section refs, assumptions index); PRD finalized
|
||||
- (change) Update pass from UX run: FR-5 reads Password Policy from AD; FR-16 rich-text editor stored as Markdown; FR-17 severity required (default information); FR-20 recipient summary; FR-22 accent colour + contrast warning; FR-29 languages resolved (de+en, formal/informal), Q-3 resolved; NEW FR-30 dashboard, FR-31 per-device preferences, FR-32 status page (grouped in 4.8); glossary Password Policy, Preference; addendum read rights.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Addendum: Technical Context for Downstream Documents
|
||||
|
||||
Carries the detail that does not belong in `prd.md`. The brief's addendum (`_bmad-output/briefs/.../addendum.md`) holds the original context; this file adds what the PRD discovery produced.
|
||||
|
||||
## Existing picture pipeline (the three PowerShell scripts stay unchanged)
|
||||
- `AD-ProfilePicture-Sync.ps1` (DC, scheduled task): for every user in the target OU reads `<sam>.jpg` from the source path, else a default image. It writes `<sam>_96.jpg` and `<sam>_448.jpg` to the processed path, writes `<sam>.md5` (MD5 of the source file) there, and sets `thumbnailPhoto`, `jpegPhoto` and `photo` from the 96px copy (limit 100 KB). It never compares the MD5, so every run reprocesses every user.
|
||||
- `Set-LocalAccountPicture.ps1` (logon): builds Windows account-picture sizes from `thumbnailPhoto`; large sizes use the 448px copy from the share.
|
||||
- `Set-AccountPictureRegistry-System.ps1` (startup): applies registry entries from manifests.
|
||||
- The original scripts are in `context-resources/` (untracked in git). Verified against them: the MD5 file is written at line 114 and the AD attributes at line 117, so a failed AD write still leaves a matching MD5 file.
|
||||
|
||||
## Portal and the pipeline: contract
|
||||
- Portal writes: `<source path>/<username>.jpg`, square JPEG, at least 448 px (assumed 512×512).
|
||||
- Portal reads: `<processed path>/<username>.md5` to detect that the Sync Task has processed the file (the hash is the MD5 of the Portal's file). This ties the Portal to the unchanged script.
|
||||
- The hash in `<username>.md5` is an uppercase hex MD5 written with a trailing newline (`Get-FileHash` plus `Set-Content`). Compare case-insensitively and trim whitespace.
|
||||
- The sync script covers `OU=Organisation`, which includes the Portal's writable OU. File names use `sAMAccountName`.
|
||||
- Portal reads: `thumbnailPhoto` from Active Directory to show the current picture.
|
||||
- Both paths are configuration.
|
||||
|
||||
## Active Directory
|
||||
- Writable OU: configurable; example in the homelab: `OU=Clients,OU=Organisation,DC=ping-mee,DC=local`.
|
||||
- Attributes written: `givenName`, `sn`, `displayName`, `mail`, `telephoneNumber`, `mobile`, and the password via LDAPS.
|
||||
- The Service Account needs delegated rights for exactly those attributes and password change on that OU. Password writes need LDAPS.
|
||||
- Read rights also needed: `pwdLastSet`, account enabled and locked state, group membership, `thumbnailPhoto`, and the Password Policy shown on the password page (domain `minPwdLength`, `pwdProperties`, `pwdHistoryLength`, and the user's `msDS-ResultantPSO` for a fine-grained policy). [Attribute names to verify in the test OU.]
|
||||
- Expired and "must change" passwords: AD refuses the bind (LDAP error 49, data 532 or 773). The Portal does not handle these. The Admin resets the password in AD.
|
||||
- authentik's LDAP source offers its own password writeback to AD (needs `ldaps://` and delegated rights). Not used; the Portal changes passwords itself. Source: authentik Active Directory and LDAP source docs.
|
||||
|
||||
## Identity and roles
|
||||
- OIDC via authentik, but kept generic so another provider works.
|
||||
- `preferred_username` equals `sAMAccountName` (confirmed identical in the homelab).
|
||||
- Roles: the Portal checks Publisher and Admin group membership in Active Directory.
|
||||
|
||||
## Security notes
|
||||
- Throttling happens in front of every bind, to avoid lockouts by attackers.
|
||||
- E-mail change chain risk (hijacked session, e-mail change, reset via e-mail) mitigated by: current password required, confirmation to the new address, notice and 24-hour Undo Link to the old address, undo ends sessions and notifies the Admin.
|
||||
- Picture handling: HEIC, WebP, PNG and JPEG only; SVG rejected because it can carry scripts and external references. Decode with size and pixel limits; strip metadata; never serve originals.
|
||||
- Markdown in Announcements is rendered with sanitization.
|
||||
- Secrets only in Docker environment variables.
|
||||
|
||||
## Candidate technical choices (not decided)
|
||||
- Embedded database (for example SQLite) in a mounted volume is enough for ten users; a server database could be a later option.
|
||||
- A server-side image library with HEIC support is required.
|
||||
- SMB access from a Linux container: a mounted share is the simplest.
|
||||
|
||||
## Design
|
||||
- Office 365 (Fluent) look, not OWA 2010 or Aero. Themes and backgrounds are Admin-managed.
|
||||
@@ -0,0 +1,446 @@
|
||||
---
|
||||
title: "PRD: ping-mee Self-Service Portal"
|
||||
status: final
|
||||
created: 2026-10-15
|
||||
updated: 2026-10-15
|
||||
---
|
||||
|
||||
# PRD: ping-mee Self-Service Portal
|
||||
*Working title — confirm.*
|
||||
|
||||
## 0. Document Purpose
|
||||
|
||||
This PRD defines the ping-mee Self-Service Portal for the homelab owner, who is also the builder, and for the downstream workflows (UX, architecture, epics and stories). It builds on the product brief in `_bmad-output/briefs/brief-ping-mee-self-service-platform-2026-10-15/`. Vocabulary is anchored in the Glossary (§3). Features are grouped in §4 with globally numbered requirements nested under them. Inferred points are tagged `[ASSUMPTION]` inline and indexed in §10. Look and behavior are specified in `_bmad-output/ux-designs/ux-ping-mee-self-service-platform-2026-10-15/` (`DESIGN.md`, `EXPERIENCE.md`), which this PRD does not duplicate. Technical choices live in `addendum.md`, not here.
|
||||
|
||||
## 1. Vision
|
||||
|
||||
Every User of the ping-mee homelab can manage their own account in one clean place: change their password, correct their name and contact details, and set a profile picture. The Admin has one channel to tell everyone about planned downtime and changes.
|
||||
|
||||
Today every such change, however small, goes through the admin. No existing tool covers these four things together in an Active Directory homelab. The portal is built around the real setup (Active Directory, authentik, the existing picture pipeline), runs as one Docker container behind the existing reverse proxy, and is reachable from the internet. Nothing in it is tied to ping-mee: everything is configurable, so it can be deployed elsewhere later. The homelab is oversized for its ten users, and the portal is built to a standard that matches.
|
||||
|
||||
## 2. Target User
|
||||
|
||||
### 2.1 Jobs To Be Done
|
||||
- As a **User**, I want to change my own password, details and picture without asking anyone, so that I stay in control of my account.
|
||||
- As a **User**, I want to hear about planned downtime before it happens, so that I am not surprised when a service is gone.
|
||||
- As the **Admin**, I want users to handle their own changes, so that I stop doing manual password and profile work.
|
||||
- As the **Admin** and **Publisher**, I want one place to announce changes, so that nobody has to be told twice.
|
||||
- As the **Admin**, I want the portal to be safe on the internet, so that exposing it does not put the Active Directory at risk.
|
||||
|
||||
### 2.2 Non-Users (v1)
|
||||
- Anyone without an Active Directory account in the configured OU.
|
||||
- Users with an expired or "must change" password. They contact the Admin, who resets the password in Active Directory; they then sign in and change it themselves.
|
||||
- Anonymous visitors. The portal shows them nothing except the sign-in redirect.
|
||||
|
||||
### 2.3 Key User Journeys
|
||||
*Drafted from the brief and the discovery conversation. Personas are illustrative. [ASSUMPTION: confirm or replace.]*
|
||||
|
||||
- **UJ-1. Lena changes her password before it leaks.**
|
||||
- **Persona + context:** Lena, a friend who uses Jellyfin, reuses a password and wants a fresh one.
|
||||
- **Entry state:** not signed in; opens the portal address from a bookmark.
|
||||
- **Path:** signs in through authentik; opens "Password"; enters her current password and the new one twice; submits.
|
||||
- **Climax:** the portal confirms the change and signs her out of other sessions.
|
||||
- **Resolution:** a "password changed" e-mail arrives; she uses the new password everywhere.
|
||||
- **Edge case:** the new password breaks the domain's complexity rule; the portal shows the rule in plain language and nothing changes.
|
||||
|
||||
- **UJ-2. Tom sets a profile picture from his iPhone.**
|
||||
- **Persona + context:** Tom, family, has a HEIC photo and wants it as his avatar.
|
||||
- **Entry state:** signed in, on "Profile".
|
||||
- **Path:** chooses the photo; crops it square in the browser; saves.
|
||||
- **Climax:** the page shows his current picture with a "pending" mark and the note that it updates within about two hours.
|
||||
- **Resolution:** once the sync has run, the mark disappears and the new picture shows.
|
||||
- **Edge case:** the file is 14 MB; the portal rejects it before upload and says why.
|
||||
|
||||
- **UJ-3. Ebbe announces a maintenance window.**
|
||||
- **Persona + context:** Ebbe, the homelab owner, in the Publisher group, plans a server move.
|
||||
- **Entry state:** signed in, on "Announcements".
|
||||
- **Path:** writes a title and Markdown text; sets severity to warning, the downtime window and an expiry; schedules the announcement for the next morning.
|
||||
- **Climax:** at the scheduled time the announcement appears on the page and opted-in users receive the e-mail.
|
||||
- **Resolution:** after the move, Ebbe withdraws the announcement.
|
||||
- **Edge case:** he corrects the end time later; he edits the text and chooses "notify users again".
|
||||
|
||||
- **UJ-4. Marie sees an e-mail change she did not make.**
|
||||
- **Persona + context:** Marie finds an e-mail at her old address saying her address was changed.
|
||||
- **Entry state:** she is not signed in; she reads the message on her phone.
|
||||
- **Path:** opens the undo link within 24 hours.
|
||||
- **Climax:** the old address is restored and all her sessions end.
|
||||
- **Resolution:** she is told to change her password; the Admin gets a notification.
|
||||
|
||||
## 3. Glossary
|
||||
*Used verbatim throughout this PRD and by downstream workflows.*
|
||||
|
||||
- **User** — a person with an Active Directory account in the configured OU who signs in to the portal.
|
||||
- **Publisher** — a User who is a member of the configured Publisher group and may create Announcements.
|
||||
- **Admin** — a User who is a member of the configured Admin group. The Admin group is separate from the Publisher group.
|
||||
- **Portal** — the web application this PRD specifies.
|
||||
- **Service Account** — the Active Directory account the Portal uses for LDAPS writes. Its rights are limited to the configured OU and a fixed list of attributes.
|
||||
- **Profile Field** — one of: first name (`givenName`), last name (`sn`), display name (`displayName`), e-mail (`mail`), telephone number (`telephoneNumber`), mobile number (`mobile`).
|
||||
- **Session** — a User's signed-in period in the Portal. It lasts up to 1 hour of inactivity.
|
||||
- **Picture Source File** — the file `<username>.jpg` that the Portal writes to the configured source path of the Share.
|
||||
- **Share** — the file share where the Picture Source File is written; the Sync Task reads from it.
|
||||
- **Sync Task** — the existing scheduled PowerShell task that turns Picture Source Files into the Active Directory picture attributes. It is not part of the Portal.
|
||||
- **Pending** — a Picture Source File has been written but the Sync Task has not processed it yet.
|
||||
- **Announcement** — a message from a Publisher with a title, a formatted body (edited in a rich-text editor, stored as Markdown), a Severity, and an optional Downtime Window, expiry and publish time.
|
||||
- **Opt-in / Opt-out** — a User's choice to receive or not receive Announcement e-mails. New Users are opted in.
|
||||
- **Audit Entry** — a record of who did what, when, and from which IP address.
|
||||
- **Audit Log** — the collection of all Audit Entries, readable by Admins.
|
||||
- **Draft** — an Announcement that is saved but not published; only Publishers see it.
|
||||
- **Withdrawn** — an Announcement that was published and then taken down; Users no longer see it.
|
||||
- **Severity** — the level of an Announcement: information, warning or outage. Every Announcement has one; the default is information.
|
||||
- **Downtime Window** — the optional start and end time of a planned downtime stated in an Announcement.
|
||||
- **Password Policy** — the password rules Active Directory enforces for a User (minimum length, complexity, history), read by the Portal and shown on the password page.
|
||||
- **Preference** — a choice stored on the User's device, not on the server: theme (light, dark, system), language, and the German form of address (formal or informal).
|
||||
- **Undo Link** — a single-use link, valid for 24 hours, sent to the old address after an e-mail change, that reverts the change.
|
||||
|
||||
## 4. Features
|
||||
|
||||
### 4.1 Sign-in and Sessions
|
||||
**Description:** A User signs in through authentik using OIDC. The Portal maps the OIDC identity to the Active Directory account and decides the User's roles from group membership. Realizes UJ-1.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-1: Sign in with OIDC
|
||||
A User can sign in by being redirected to the configured OIDC provider and returning to the Portal. Realizes UJ-1.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The Portal matches the OIDC `preferred_username` claim to `sAMAccountName` in the configured OU.
|
||||
- A sign-in for a name not found in the OU is refused with a generic message.
|
||||
- A User whose Active Directory account is disabled or locked cannot start a Session, and an existing Session of such a User ends at its next request.
|
||||
- An unauthenticated request to any Portal page redirects to sign-in.
|
||||
|
||||
**Out of Scope:**
|
||||
- Signing in with a local password or directly against LDAPS.
|
||||
|
||||
#### FR-2: Resolve roles
|
||||
The Portal determines whether a User is a Publisher or an Admin by checking membership of the configured Publisher group and Admin group in Active Directory.
|
||||
|
||||
**Consequences (testable):**
|
||||
- A User in neither group sees no Announcement editor and no admin pages.
|
||||
- Direct requests to Publisher or Admin pages by a User without the role return an authorization error.
|
||||
- Group names come from configuration only.
|
||||
|
||||
#### FR-3: Session lifetime and sign-out
|
||||
A Session ends after 1 hour of inactivity or when the User signs out. The inactivity time is an Admin-changeable setting. [ASSUMPTION: 1 hour default; whether it also has an absolute maximum is open, see Q-4.]
|
||||
|
||||
**Consequences (testable):**
|
||||
- Requests after expiry redirect to sign-in.
|
||||
- Sign-out ends the Session on the server, not only in the browser.
|
||||
|
||||
#### FR-4: Throttle sensitive requests
|
||||
The Portal limits repeated requests to password change (FR-5), e-mail change (FR-10), the confirmation and Undo Link pages (FR-10, FR-11), and picture upload (FR-12), per User and per IP address, before any call to Active Directory. Sign-in itself is throttled by the OIDC provider, not by the Portal.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Each covered endpoint has its own configured limit and pause. [ASSUMPTION: defaults are chosen at design time.]
|
||||
- Once a limit is reached, further requests to that endpoint are refused with a "try again later" message, without contacting Active Directory or the SMTP server.
|
||||
- Throttled requests are recorded as Audit Entries (FR-28).
|
||||
- Limits are configurable.
|
||||
- Failed current-password checks follow the stricter per-User rule in FR-6.
|
||||
|
||||
### 4.2 Password Change
|
||||
**Description:** A User changes their own password by entering the current and the new password. The Portal verifies the current password and applies the new one over LDAPS. Active Directory's own complexity, history and age rules apply. Users with expired passwords are not served; they contact the Admin. Realizes UJ-1.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-5: Change password
|
||||
A User can change their password by entering the current password and the new password twice. Realizes UJ-1.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Before the User types, the page lists the User's Password Policy as read from Active Directory (including a fine-grained policy that applies to the User).
|
||||
- The current password is verified over LDAPS before the change is applied.
|
||||
- When Active Directory rejects the new password, the Portal shows the reason in plain language.
|
||||
- No password is written to logs, the audit log or error output.
|
||||
|
||||
#### FR-6: Limit failed password attempts
|
||||
The Portal counts failed current-password checks per User and refuses further attempts after a configured limit, with a configured pause. [ASSUMPTION: default 5 failures, then a 15-minute pause.]
|
||||
|
||||
**Consequences (testable):**
|
||||
- The limit applies before the bind to Active Directory, so it protects the account from lockout by the Portal.
|
||||
- The limit and pause are configurable.
|
||||
|
||||
#### FR-7: After a password change
|
||||
After a successful change the Portal ends the User's other Sessions and sends a "password changed" e-mail to the User's address.
|
||||
|
||||
**Consequences (testable):**
|
||||
- All other Sessions of that User are invalid immediately.
|
||||
- The e-mail names the time of the change and does not contain the password.
|
||||
|
||||
### 4.3 Profile Data
|
||||
**Description:** A User edits their Profile Fields. Every value is validated and escaped before it reaches Active Directory. An e-mail change is a sensitive action: it needs the current password, a confirmation from the new address, and a notice with an Undo Link to the old address. Realizes UJ-4.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-8: View and edit Profile Fields
|
||||
A User can view and edit first name, last name, display name, telephone number and mobile number. The username is shown and cannot be edited.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Saved values appear in Active Directory under the matching attributes.
|
||||
- A User can only read and write their own Profile Fields.
|
||||
- The display name is free text. [ASSUMPTION: no further rules.]
|
||||
|
||||
#### FR-9: Validate and escape input
|
||||
The Portal validates length, characters and format of each Profile Field and escapes values for LDAP.
|
||||
|
||||
**Consequences (testable):**
|
||||
- LDAP filter and DN injection strings do not alter queries or write to other objects.
|
||||
- Invalid input is rejected with a message naming the field.
|
||||
- Phone number format is checked. [ASSUMPTION: international format with digits, spaces, `+`, `-` and brackets.]
|
||||
|
||||
#### FR-10: Change e-mail with confirmation
|
||||
A User can change their e-mail by entering the current password and the new address. The change applies only after the User opens a confirmation link sent to the new address. Realizes UJ-4.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The current password is verified before the confirmation e-mail is sent, and counts against the failed-attempt limit in FR-6.
|
||||
- The confirmation link is single-use and expires after 1 hour. [ASSUMPTION]
|
||||
- Opening the link shows a page that names the pending change and applies it only after the User clicks a confirm button (POST). A plain GET of the link changes nothing, so mail scanners cannot confirm it.
|
||||
- Until confirmed, `mail` in Active Directory is unchanged.
|
||||
|
||||
#### FR-11: Notify the old address and offer undo
|
||||
When an e-mail change is applied, the Portal sends a notice to the old address that includes an Undo Link valid for 24 hours. Realizes UJ-4.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Opening the Undo Link shows a page that names the change and restores the old address in Active Directory only after the User clicks a confirm button (POST). A plain GET of the link changes nothing, so mail scanners cannot trigger an undo.
|
||||
- Undo also ends all Sessions of that User, tells the User to change their password, and notifies the Admin.
|
||||
- The Undo Link works once and expires after 24 hours.
|
||||
|
||||
### 4.4 Profile Picture
|
||||
**Description:** A User uploads a picture, crops it in the browser, and the Portal writes a square JPEG named `<username>.jpg` to the Share. The Sync Task, which stays unchanged, makes the 96px and 448px versions and writes the Active Directory attributes within about two hours. The Portal shows the current picture from Active Directory with a Pending mark until the Sync Task has processed the new file. Realizes UJ-2.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-12: Upload and convert a picture
|
||||
A User can upload a JPEG, PNG, HEIC or WebP file of up to 10 MB. The Portal re-encodes it as a square JPEG of at least 448 px [ASSUMPTION: 512×512] and writes it as the Picture Source File. Realizes UJ-2.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The file is named `<username>.jpg` regardless of the uploaded name, replacing any earlier file.
|
||||
- Other formats, including SVG, and files over 10 MB are rejected before upload with a clear message.
|
||||
- Metadata is removed from the output.
|
||||
- The original upload is not stored or served.
|
||||
|
||||
#### FR-13: Crop and edit in the browser
|
||||
A User can crop the picture to a square and adjust it before saving. [ASSUMPTION: crop, zoom and rotate; no filters.]
|
||||
|
||||
**Consequences (testable):**
|
||||
- The Portal re-validates and re-encodes the result on the server, whatever the browser sends.
|
||||
|
||||
#### FR-14: Show current picture and Pending status
|
||||
The Portal shows the User's current picture read from the Active Directory picture attribute and marks it Pending after an upload. Realizes UJ-2.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The Pending mark clears when the MD5 of the written Picture Source File equals the content of `<username>.md5` in the configured processed path.
|
||||
- The User sees the note that updates take about two hours.
|
||||
- If Pending lasts longer than a configured time, the Portal says it is taking longer than expected. [ASSUMPTION]
|
||||
|
||||
#### FR-15: Remove the picture
|
||||
A User can remove their picture, which deletes the Picture Source File so the Sync Task falls back to the default image.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The Portal shows Pending until the Sync Task has restored the default.
|
||||
|
||||
### 4.5 Announcements
|
||||
**Description:** A Publisher writes Announcements in a rich-text editor that shows the finished result (bold, italic, heading, list, link); the text is stored as Markdown. Users read them on a page after signing in, and opted-in Users receive them by e-mail. Realizes UJ-3.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-16: Create, save as draft and publish
|
||||
A Publisher can write an Announcement with a title and body, save it as a draft, and publish it. Realizes UJ-3.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Only Publishers can create, edit, withdraw or publish.
|
||||
- The body is edited in a rich-text editor with a toolbar for bold, italic, heading, list and link; the Publisher never sees Markdown syntax.
|
||||
- The body is stored as Markdown and sanitized every time it is shown: HTML and script content never executes.
|
||||
|
||||
#### FR-17: Severity and optional fields
|
||||
Every Announcement has a Severity (information, warning or outage); the default is information and it cannot be left empty. A Publisher can add a Downtime Window, an expiry and a publish time.
|
||||
|
||||
**Consequences (testable):**
|
||||
- An expired Announcement no longer appears in the active list.
|
||||
- An Announcement with a future publish time appears and is e-mailed at that time only.
|
||||
- Severity decides the colour, icon and wording of the banner (FR-19).
|
||||
|
||||
#### FR-18: Edit, withdraw and notify again
|
||||
A Publisher can edit or withdraw an Announcement. Editing never sends e-mail on its own; the Publisher can choose "notify users again".
|
||||
|
||||
**Consequences (testable):**
|
||||
- A withdrawn Announcement disappears from the Users' page.
|
||||
- "Notify again" sends one new e-mail to opted-in Users.
|
||||
|
||||
#### FR-19: Announcement page and banner
|
||||
A signed-in User can read current and past Announcements on a page, newest first. Active Announcements show a banner on the home page.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Signed-out visitors cannot read any Announcement.
|
||||
- The banner shows severity.
|
||||
|
||||
#### FR-20: Send Announcement e-mail
|
||||
When an Announcement is published, every opted-in User receives it by e-mail through the configured SMTP server.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Recipients are all Users in the configured OU with a valid `mail` value in Active Directory, whether or not they have ever signed in, except those who are opted out.
|
||||
- A User with no valid `mail` value is skipped, and the skip is recorded so the Admin can see who was not reached.
|
||||
- Before publishing, the Publisher sees how many Users will receive the e-mail and how many are left out because they opted out or have no address.
|
||||
- Users who are opted out receive nothing, regardless of severity.
|
||||
- Sending failures are recorded and visible to the Admin. [ASSUMPTION]
|
||||
|
||||
#### FR-21: Opt in and out
|
||||
A User can opt in or out of Announcement e-mails at any time. Every User is opted in by default, including Users who have never signed in; the Portal stores only explicit opt-outs. Opting out shows a notice that they will receive no downtime notifications and can opt back in on the Portal.
|
||||
|
||||
### 4.6 Admin UI
|
||||
**Description:** An Admin manages how the Portal looks and behaves, and reads the audit log. Admins cannot edit other Users' data; the Admin UI manages only the Portal.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-22: Accent colour and backgrounds
|
||||
An Admin can choose the portal-wide accent colour and upload background images. The default accent is the Fluent blue and the default look follows the Microsoft 365 admin center. [ASSUMPTION: verify the default blue against the Fluent web components at build.]
|
||||
|
||||
**Consequences (testable):**
|
||||
- When the Admin enters a colour, the Portal shows its contrast for the light and the dark theme and a warning if it is below WCAG AA (4.5:1); the Admin can still save it.
|
||||
- Uploaded images are re-encoded and limited in size.
|
||||
- Backgrounds are shown only to signed-in Users. Anonymous visitors see nothing but the sign-in redirect (§2.2).
|
||||
|
||||
#### FR-23: Runtime settings
|
||||
An Admin can change cosmetic and behaviour settings: portal name, session inactivity time, failed-attempt limit and pause, audit retention.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Settings that identify a system or contain a secret (Active Directory, LDAPS, Service Account, SMTP, OIDC, paths, group names) are not changeable here.
|
||||
|
||||
#### FR-24: Audit log viewer
|
||||
An Admin can read the audit log.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The log is not visible to non-Admins.
|
||||
- Entries older than the configured retention (default 12 months) are removed automatically.
|
||||
|
||||
#### FR-25: Admin access control
|
||||
Only members of the Admin group can open Admin UI pages.
|
||||
|
||||
### 4.7 Platform and Operations
|
||||
**Description:** The Portal runs as one Docker container on a Linux host behind a reverse proxy that terminates TLS. All connection settings and secrets come from environment variables.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-26: Configuration through environment variables
|
||||
All system-specific values come from documented environment variables with safe defaults.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Nothing in the image names ping-mee, a domain, an OU, a group, an IP address or a path.
|
||||
- Secrets never appear in logs or in the Admin UI.
|
||||
|
||||
#### FR-27: Docker delivery
|
||||
The Portal is delivered as a Docker image that runs with the Share mounted and persists its data in a mounted volume. It exposes a health check.
|
||||
|
||||
#### FR-28: Record audit entries
|
||||
The Portal records an Audit Entry for password changes, Profile Field changes, e-mail change requests, confirmations and undos, picture uploads and removals, Announcement actions, settings changes, and failed and throttled sign-in and password attempts. Each entry holds who, what, when and the IP address.
|
||||
|
||||
**Consequences (testable):**
|
||||
- No password, token or picture content appears in an entry.
|
||||
|
||||
#### FR-29: Interface language
|
||||
The interface is available in German and English. The language is chosen from the browser's language and the User can switch it (see FR-31). German has a formal and an informal form of address; the Admin sets the portal-wide default (formal).
|
||||
|
||||
### 4.8 Dashboard, Preferences and Status
|
||||
**Description:** The landing page shows what matters now, each User sets how the portal looks and speaks on their own device, and the Admin sees on one page whether the background work is healthy. See the UX documents for layout and wording. Realizes UJ-1, UJ-3.
|
||||
|
||||
**Functional Requirements:**
|
||||
|
||||
#### FR-30: Dashboard
|
||||
After signing in, a User lands on a dashboard that shows all active Announcements as banners and an account card with picture (with its Pending status), name, username, e-mail, telephone number and the date the password was last changed.
|
||||
|
||||
**Consequences (testable):**
|
||||
- Active Announcements appear newest first with their Severity.
|
||||
- The password date comes from Active Directory.
|
||||
- The card links to the profile page.
|
||||
|
||||
#### FR-31: Per-device Preferences
|
||||
A User can set theme (light, dark or system), language and, in German, the form of address (formal or informal) from a menu opened from their picture in the upper right. The choices are stored on the device.
|
||||
|
||||
**Consequences (testable):**
|
||||
- The default theme is the system theme and the default language comes from the browser.
|
||||
- The form-of-address choice is shown only when the language is German.
|
||||
- The choices take effect immediately and are not stored on the server.
|
||||
- E-mails use the portal-wide default form of address, because the device choice cannot reach them. [ASSUMPTION]
|
||||
|
||||
#### FR-32: Status page
|
||||
An Admin can see on one page the age of the last successful database backup, the e-mails that failed to send with their reason, and the last run and result of each background job.
|
||||
|
||||
**Consequences (testable):**
|
||||
- A backup older than a configured age (default 26 hours) is marked as a problem.
|
||||
- An e-mail that failed after the configured number of attempts stays listed until it is resolved.
|
||||
- No other alert (e-mail or otherwise) is sent. [Decision: the Admin watches this page.]
|
||||
|
||||
## 5. Non-Goals (Explicit)
|
||||
- Not an Active Directory admin console: Admins cannot edit other Users' data or reset passwords here.
|
||||
- No forgotten-password reset and no expired-password flow. The Admin handles both by hand.
|
||||
- No editing of `cn` or `name`, and so no object renames.
|
||||
- No multi-domain support.
|
||||
- No replacement of the PowerShell scripts or the Sync Task.
|
||||
- No public announcement page or status page.
|
||||
- No SVG uploads.
|
||||
- Not a general identity provider: sign-in stays with authentik.
|
||||
|
||||
## 6. MVP Scope
|
||||
|
||||
### 6.1 In Scope
|
||||
- Everything in §4, shipped together. [Decision: the Admin chose not to phase the first release; sequencing is left to epics and stories.]
|
||||
|
||||
### 6.2 Out of Scope for MVP
|
||||
- Everything in §5.
|
||||
|
||||
## 7. Success Metrics
|
||||
|
||||
**Primary**
|
||||
- **SM-1**: Number of known-password changes done by the Admin by hand per month — target 0 after rollout. Validates FR-5.
|
||||
- **SM-2**: Share of profile and picture changes made by Users themselves — target: all of them. Validates FR-8, FR-12.
|
||||
- **SM-3**: Share of planned downtimes announced through the Portal — target 100%. Validates FR-16, FR-20.
|
||||
|
||||
**Secondary**
|
||||
- **SM-4**: Time from Publish to e-mail delivery for opted-in Users. Validates FR-20.
|
||||
- **SM-5**: Share of Pending pictures cleared within the expected time. Validates FR-14.
|
||||
|
||||
**Counter-metrics (do not optimize)**
|
||||
- **SM-C1**: Number of Announcements or e-mails sent. More is not better; this counterbalances SM-3.
|
||||
- **SM-C2**: Session length. Do not lengthen Sessions for convenience at the cost of safety. Counterbalances SM-2.
|
||||
- **SM-C3**: Failed-attempt limits. Do not relax them to reduce support messages. Counterbalances SM-1.
|
||||
|
||||
## 8. Cross-Cutting Requirements
|
||||
|
||||
### 8.1 Security *(system-wide)*
|
||||
- Injection (LDAP, SQL, script) does not work against any input.
|
||||
- A stolen or hijacked Session does not give lasting access: sensitive actions require the current password, and sensitive changes notify the old contact.
|
||||
- A User can never read or write another User's data.
|
||||
- The Service Account has the minimum rights: only the configured OU and the listed attributes.
|
||||
- Uploaded files are re-encoded; the original is never served; decoding runs with limits against decompression bombs.
|
||||
- Cookies are secure, CSRF protection and security headers are enabled.
|
||||
|
||||
### 8.2 Privacy
|
||||
- Personal data (names, e-mail, phone, picture, audit entries) is processed only for the functions above. Audit retention is 12 months by default and configurable.
|
||||
- Data of Users removed from Active Directory is removed from the Portal. [ASSUMPTION: see Q-6.]
|
||||
|
||||
### 8.3 Reliability and Operations
|
||||
- Failures of the Share, SMTP or Active Directory are reported to the User in plain language and recorded in the audit log or logs.
|
||||
- The Portal depends on authentik being available for sign-in. [ASSUMPTION: accepted.]
|
||||
|
||||
### 8.4 Accessibility and Look
|
||||
- The interface follows the Microsoft 365 admin center look (see the UX documents) and meets WCAG 2.1 AA. [ASSUMPTION]
|
||||
|
||||
## 9. Open Questions
|
||||
- **Q-1.** *Resolved in architecture (AD-10):* if the Share is unreachable during an upload, the User sees an error and nothing changes.
|
||||
- **Q-2.** *Partly resolved (AD-11):* failed e-mails are retried with backoff and a rate limit and stay listed for the Admin. Still open: the exact retry count and the IONOS sending limits.
|
||||
- **Q-4.** *Resolved in architecture (AD-6):* the absolute session limit is 4 hours, on top of 1 hour of inactivity; both are settings.
|
||||
- **Q-5.** *Resolved in architecture (AD-14):* SQLite in WAL mode on a volume, with consistent backup files written to a backup folder that the Admin's host copy job copies. Still open: how a restore is tested.
|
||||
- **Q-6.** *Resolved in architecture (AD-12):* a nightly reconciliation removes data of Users no longer in the OU; audit entries stay until retention ends.
|
||||
- **Q-7.** *Resolved in architecture (AD-10):* for a removed picture, Pending clears when the processed MD5 differs from the previous one.
|
||||
- **Q-8.** *Resolved in architecture (AD-15):* stored in UTC, shown in the configured zone (default Europe/Berlin), zone named in e-mails.
|
||||
- **Q-9.** *Accepted limitation (AD-10):* the Sync Task writes `<username>.md5` before it sets the Active Directory attributes, so a failed attribute write can clear Pending early. The "taking longer than expected" message and the Admin's checks are the safety net.
|
||||
|
||||
## 10. Assumptions Index
|
||||
- §4.1 FR-3 — 1 hour default with an Admin-changeable value.
|
||||
- §4.1 FR-4 — default limits per endpoint are chosen at design time.
|
||||
- §4.2 FR-6 — default 5 failures, 15-minute pause.
|
||||
- §4.3 FR-8, FR-9, FR-10 — display name has no rules; phone format; 1-hour single-use confirmation link.
|
||||
- §4.4 FR-12, FR-13, FR-14 — 512×512 output; crop, zoom and rotate only; "taking longer than expected" threshold.
|
||||
- §4.5 FR-20 — sending failures visible to the Admin.
|
||||
- §4.6 FR-22 — default accent is the Fluent blue; verify at build.
|
||||
- §4.8 FR-31 — e-mails use the portal-wide form of address.
|
||||
- §4.7 FR-29 — resolved: German and English.
|
||||
- §8.2–8.4 — user data removal, authentik availability, WCAG 2.1 AA.
|
||||
- §2.3 — journeys and personas are illustrative.
|
||||
@@ -0,0 +1,57 @@
|
||||
# PRD Quality Review — ping-mee Self-Service Portal
|
||||
|
||||
## Overall verdict
|
||||
Grade: Good. The PRD has a clear thesis (an admin-free self-service loop for an AD homelab), honest scope boundaries, and unusually good security reasoning for its size. The risk is in a handful of places where a requirement reads as testable but is not yet defined: the throttling requirement, the recipient list for announcements, and link-based actions that a mail scanner can trigger by accident. None block UX work; three should be fixed before architecture.
|
||||
|
||||
## Decision-readiness — strong
|
||||
Decisions are stated as decisions and carry their reasons: separate Admin and Publisher groups (§3), no expired-password flow (§2.2), no SVG (FR-12, §5), opt-out means no mail at all (FR-20), pending detection via the MD5 file (FR-14). The cost of "ship everything" is named (§6.1 note, brief) rather than hidden. Open Questions are genuinely open.
|
||||
|
||||
### Findings
|
||||
- **low** Admin-changeable security settings (FR-23) — An Admin session can weaken the failed-attempt limit or session time. Settings changes are audited (FR-28), but there are no floor values. *Fix:* add a consequence that limits have minimum values.
|
||||
|
||||
## Substance over theater — adequate
|
||||
No persona theater (four journeys, each drives requirements) and no innovation theater (the brief admits there is no moat). §8.3 and §8.4 are thinner: "failures are reported in plain language" and "meets WCAG 2.1 AA" carry no thresholds, and there is no statement about availability expectations.
|
||||
|
||||
### Findings
|
||||
- **medium** Reliability has no targets (§8.3) — Says what is reported, not what is expected. *Fix:* state explicitly that there is no availability target for the homelab release, or set one (for example "restart-safe, no data loss on restart").
|
||||
- **low** Upload processing time (FR-12) — No bound. *Fix:* add a testable limit (for example "a 10 MB HEIC is processed within 10 seconds or fails with a message").
|
||||
|
||||
## Strategic coherence — adequate
|
||||
The thesis is coherent and the features serve it. Success metrics validate the thesis and counter-metrics are present. The weakness is that "everything ships together" leaves downstream workflows with no dependency order.
|
||||
|
||||
### Findings
|
||||
- **medium** No build-order guidance (§6.1) — Epics and stories will have to infer that sign-in and roles come first, the audit log is needed by everything, and the Admin UI depends on runtime settings. *Fix:* add a short dependency note (not phasing) under §6.1.
|
||||
- **medium** Metrics are hard to measure (SM-1, SM-2) — "0 manual changes" and "all of them" cannot be observed from the Portal, because manual work happens outside it. *Fix:* state the measurement method (for example the Admin's own count, plus audit-log counts of self-service actions).
|
||||
|
||||
## Done-ness clarity — adequate
|
||||
Most FRs have testable consequences. Weak spots are where the behavior is implied rather than defined.
|
||||
|
||||
### Findings
|
||||
- **high** FR-4 "Throttle requests" — "failed attempts" is undefined: sign-in happens at authentik, so the Portal never sees failed sign-ins. The only failed attempts the Portal sees are in the password-change and e-mail-change forms (FR-6, FR-10). Which endpoints does FR-4 cover, and are the limits per User, per IP, or both? *Fix:* list the covered endpoints (password change, e-mail change, confirmation and undo links, uploads) and state limits for each; keep FR-6 as the account-lockout protection.
|
||||
- **high** FR-10 / FR-11 links act on GET — Mail clients and security scanners open links automatically. A scanner could confirm an e-mail change or trigger an undo. *Fix:* the link opens a page that shows the pending action and requires a click (POST); add this as a consequence to both.
|
||||
- **medium** FR-12 minimum input size — Rejects over 10 MB but says nothing about small images (for example 200×200). The sync script resizes to 448 px, so a tiny source gets upscaled. *Fix:* define a minimum size (reject, or accept with a warning).
|
||||
- **medium** FR-1 disabled and locked accounts — Not covered. *Fix:* a consequence that a User whose AD account is disabled or locked cannot start or keep a Session.
|
||||
- **medium** FR-14 relies on timing in the script — `<sam>.md5` is written before the AD attributes are set. If the AD write fails afterwards, the Portal sees a match and clears Pending while the picture is not live. *Fix:* note this in the addendum and add a consequence that the Portal also compares the `thumbnailPhoto` change or accepts the limitation explicitly.
|
||||
|
||||
## Scope honesty — strong
|
||||
Non-Goals do real work (no admin console, no reset flow, no SVG, no cn/name). Assumptions are tagged inline (16) and indexed. Open Questions Q-1 to Q-7 are specific. The "everything ships together" decision is attributed to the Admin, not smoothed over.
|
||||
|
||||
### Findings
|
||||
- **high** Recipient list is undefined (FR-20, FR-21) — "New Users are opted in", but the Portal only learns about a User at first sign-in. Do Users who have never signed in get downtime e-mails? Users without a `mail` value? This decides who is told about downtime. *Fix:* add an Open Question and a requirement for the recipient source (all Users in the OU with a valid `mail`, versus only Users who have signed in).
|
||||
- **medium** Time zone is unspecified (FR-17) — Downtime windows and scheduled publish times have no time zone rule. *Fix:* add Q-8: store UTC, display in a configured time zone, and name the zone in e-mails.
|
||||
- **medium** Contradiction on anonymous visitors (§2.2 vs FR-22) — §2.2 says anonymous visitors see nothing except the redirect, but FR-22 says backgrounds are served to anonymous visitors. *Fix:* decide where backgrounds show (after sign-in only, or on a pre-sign-in landing page) and align both.
|
||||
|
||||
## Downstream usability — adequate
|
||||
IDs are contiguous and cross-references resolve. Journeys have named protagonists. Glossary is present, with some gaps.
|
||||
|
||||
### Findings
|
||||
- **medium** Glossary gaps — "audit log", "draft", "withdrawn", "severity" and "downtime window" are used as terms but not defined. *Fix:* add them, and use "Audit Entry" or "audit log" consistently.
|
||||
- **low** Casing drift in the Vision (§1) — "user" and "admin" in lower case where §3 defines User and Admin. *Fix:* capitalize, or accept the Vision as informal prose.
|
||||
|
||||
## Shape fit — strong
|
||||
A capability-first PRD with short journeys suits a single-operator internal tool with several user roles. The weight on security requirements matches the internet exposure.
|
||||
|
||||
## Mechanical notes
|
||||
- IDs: FR-1 to FR-29, UJ-1 to UJ-4, SM-1 to SM-5 and C1 to C3, Q-1 to Q-7 are contiguous with no unresolved references.
|
||||
- Assumptions Index: roundtrip is incomplete. The inline tags in FR-3, FR-12 and FR-14 are indexed; the "notify again" default and the audit-failure behavior are not tagged.
|
||||
- Required sections for an internal tool are present; no compliance section is needed, but GDPR basics sit under §8.2 as an assumption.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
topic: ping-mee Self-Service Portal
|
||||
updated: 2026-10-15T12:37
|
||||
---
|
||||
|
||||
|
||||
- (event) Create. Inputs (rich, structured, no elicitation): PRD+addendum, brief+addendum, ARCHITECTURE-SPINE, UX DESIGN.md+EXPERIENCE.md, context-resources scripts. Slug inherited from PRD: ping-mee-self-service-platform. Interactive, express.
|
||||
- (decision) Companions: ADOPTED prd.md, prd addendum.md, ARCHITECTURE-SPINE.md, DESIGN.md, EXPERIENCE.md (FR-level consequences and AD rules are load-bearing and would bloat the kernel). Sources absorbed: brief.md, brief addendum.md. No spec-authored companions.
|
||||
- (capability) CAP-1 sign-in via OIDC, roles from AD groups, server-side sessions (1h idle/4h absolute), disabled accounts cut off, sensitive-request throttling [PRD FR-1..FR-4]
|
||||
- (capability) CAP-2 user changes own password (current+new), policy shown from AD, applied over LDAPS, other sessions ended, e-mail sent [FR-5..FR-7]
|
||||
- (capability) CAP-3 user edits own profile fields; e-mail change needs password + confirmation to new address + notice and 24h undo link to old [FR-8..FR-11]
|
||||
- (capability) CAP-4 profile picture upload (JPEG/PNG/HEIC/WebP <=10MB), crop, write <sam>.jpg to share, Pending until sync processed, remove [FR-12..FR-15]
|
||||
- (capability) CAP-5 publishers write announcements (rich text stored as Markdown, severity always set, optional window/expiry/schedule), edit/withdraw/notify again; page + banners [FR-16..FR-19]
|
||||
- (capability) CAP-6 announcement e-mail to all OU users with valid mail minus opt-outs via outbox; opt-in default; failures visible to Admin [FR-20, FR-21]
|
||||
- (capability) CAP-7 admin manages the portal: accent colour with contrast warning, backgrounds, runtime settings, audit log viewer, status page [FR-22..FR-25, FR-32]
|
||||
- (capability) CAP-8 every sensitive action leaves an audit entry, kept 12 months, never holding passwords/tokens/picture content [FR-28]
|
||||
- (capability) CAP-9 dashboard, per-device preferences (theme/language/German form of address), German+English, responsive desktop+phone, WCAG 2.1 AA [FR-29..FR-31]
|
||||
- (capability) CAP-10 deployable anywhere: one Docker container, config+secrets via env vars, nothing ping-mee-specific, consistent DB backups, health check [FR-26, FR-27]
|
||||
- (constraint) Architecture: Django modular monolith with adapters, one owner per external system, 19 rules in ARCHITECTURE-SPINE (AD-1..AD-19) are binding.
|
||||
- (constraint) Single container, one worker, SQLite WAL on a volume; portal writes its own consistent backup files because Veeam copies directories.
|
||||
- (constraint) AD only via LDAPS with a service account limited to a configurable OU and fixed attribute allow-lists; no cn/name edits.
|
||||
- (constraint) Existing PowerShell sync/logon scripts stay unchanged: portal only writes <sam>.jpg to source and reads <sam>.md5 from processed.
|
||||
- (constraint) Internet-facing: no injection, no cross-user access, throttling before any AD call, passwords never stored or logged, strict CSP with no inline script or external assets.
|
||||
- (constraint) Expired, forgotten or must-change passwords are never handled by the portal; the Admin resets them in AD.
|
||||
- (decision) Companion precedence: behaviour = PRD; architecture = ARCHITECTURE-SPINE; look = DESIGN.md; UX behaviour = EXPERIENCE.md. Where PRD and UX docs differ, the 2026-10-15 update pass aligned them.
|
||||
- (question) Open: exact retry count + IONOS limits; restore test; authentik single logout; rich-text editor library + asset build; image location; confirm accepted early-clear of Pending (PRD Q-9).
|
||||
- (event) Self-validate pass 1 (coherence): rules 1-6,8 hold - each CAP has intent+success, 9 non-goals, constraints bend design, ids CAP-1..CAP-10 unique. Fixed: open questions copied stale PRD list -> PRD Q-1,2,4..9 marked resolved/accepted per spine AD-6/10/11/12/14/15, SPEC lists only truly open items.
|
||||
- (event) Self-validate pass 2 (preservation): FR-1..FR-32 each covered by a CAP range; PRD addendum, spine, DESIGN, EXPERIENCE adopted as companions so FR consequences, AD rules and screen behaviour are preserved by reference; brief absorbed (all claims present in PRD). Wrapper-only dropped: PRD document-purpose, journeys narrative personas (kept in PRD/EXPERIENCE), success-metric SM-1..5 targets (kept in PRD).
|
||||
- (event) spec finalized
|
||||
- (decision) Story Breakdown agreed (17 stories, execution order): 1 scaffold, 2 audit/throttle/session foundations, 3 directory adapter, 4 OIDC sign-in, 5 page shell+prefs, 6 scheduler+mail outbox, 7 password change, 8 profile edit, 9 e-mail change, 10 picture backend, 11 picture screens, 12 announcements core, 13 announcement editor, 14 announcement e-mail+opt-out, 15 dashboard, 16 admin pages+maintenance jobs, 17 hardening+acceptance.
|
||||
- (decision) Checkpoints: spec_checkpoint on stories 1,3,4,7,10; done_checkpoint (pause after) on 4,7,16,17. User is sole reviewer.
|
||||
- (constraint) Testing environment: a test OU can be created, but NO separate authentik - authentik is production only. Portal gets its own dedicated OIDC application/provider in prod authentik, restricted to test users while testing; never modify existing authentik applications.
|
||||
- (event) stories.yaml written (17 stories) and checked against schema rules: required fields, unique string ids, prefix-free, no status field.
|
||||
- (event) Clarification: stories.yaml check above was done with a regex structure check (17 ids/titles/descriptions, unique, quoted, prefix-free, no status, no stray unindented lines); a full YAML parse was not possible because PyYAML could not be installed.
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
id: SPEC-ping-mee-self-service-platform
|
||||
companions:
|
||||
- ../../prds/prd-ping-mee-self-service-platform-2026-10-15/prd.md
|
||||
- ../../prds/prd-ping-mee-self-service-platform-2026-10-15/addendum.md
|
||||
- ../../architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md
|
||||
- ../../ux-designs/ux-ping-mee-self-service-platform-2026-10-15/DESIGN.md
|
||||
- ../../ux-designs/ux-ping-mee-self-service-platform-2026-10-15/EXPERIENCE.md
|
||||
sources:
|
||||
- ../../briefs/brief-ping-mee-self-service-platform-2026-10-15/brief.md
|
||||
- ../../briefs/brief-ping-mee-self-service-platform-2026-10-15/addendum.md
|
||||
---
|
||||
|
||||
> **Canonical contract.** This SPEC and the files in `companions:` are the complete, preservation-validated contract for what to build, test, and validate. Source documents listed in frontmatter are for traceability — consult them only if you need narrative rationale or prose color this contract intentionally omits.
|
||||
|
||||
# ping-mee Self-Service Portal
|
||||
|
||||
## Why
|
||||
|
||||
The ping-mee homelab, used by about ten family members and friends, keeps all accounts in one Active Directory. Today every change — a password, a name, a phone number, a picture — goes through the admin by hand, and there is no single place to announce downtime. This is a pain to solve for the admin and the users, and it should be built to a standard that lets it run safely on the internet and later be deployed in other Active Directory homelabs.
|
||||
|
||||
## Capabilities
|
||||
|
||||
*Each capability's requirement-level detail (FR-N with testable consequences) is in the PRD companion; the number in brackets is the PRD range.*
|
||||
|
||||
- **CAP-1** [FR-1..FR-4]
|
||||
- **intent:** A person with an AD account signs in through the identity provider and gets only the access their AD group membership allows.
|
||||
- **success:** A user in the configured OU signs in via OIDC; a user outside the Publisher and Admin groups cannot reach those pages by direct address; sessions end after 1 hour idle or 4 hours total; a disabled or locked AD account loses access at its next request; repeated sensitive requests are refused before any AD call.
|
||||
- **CAP-2** [FR-5..FR-7]
|
||||
- **intent:** A signed-in user changes their own password.
|
||||
- **success:** With the correct current password and a policy-conforming new one, the AD password changes over LDAPS, the user's other sessions end and a confirmation e-mail is sent; a policy rejection is shown in plain words with no change; five wrong current-password attempts block further tries for the configured pause.
|
||||
- **CAP-3** [FR-8..FR-11]
|
||||
- **intent:** A signed-in user edits their own first name, last name, display name, e-mail and telephone numbers.
|
||||
- **success:** Saved values appear in the matching AD attributes; injection strings change nothing else; an e-mail change applies only after the current password, a click on a confirmation page from the new address, and sends the old address a notice with a 24-hour undo that ends the user's sessions and notifies the Admin.
|
||||
- **CAP-4** [FR-12..FR-15]
|
||||
- **intent:** A signed-in user sets or removes their profile picture.
|
||||
- **success:** A JPEG, PNG, HEIC or WebP file up to 10 MB is cropped, re-encoded and written as `<sAMAccountName>.jpg` to the source folder; the page shows Pending until `processed/<name>.md5` matches the written file; other formats and larger files are refused with a reason.
|
||||
- **CAP-5** [FR-16..FR-19]
|
||||
- **intent:** A Publisher announces planned changes and downtime.
|
||||
- **success:** A Publisher writes a titled announcement in a rich-text editor with an always-set severity and an optional downtime window, expiry and publish time; it appears as a banner and in a list for signed-in users, can be edited, withdrawn, or re-sent on request; script content in the body never runs.
|
||||
- **CAP-6** [FR-20, FR-21]
|
||||
- **intent:** Users hear about announcements by e-mail unless they opted out.
|
||||
- **success:** On publish every user in the OU with a valid e-mail address, minus explicit opt-outs, receives the e-mail through the configured SMTP server; users who opted out receive nothing; the Publisher sees counts and reasons beforehand; failures are listed for the Admin.
|
||||
- **CAP-7** [FR-22..FR-25, FR-32]
|
||||
- **intent:** An Admin manages how the portal looks and behaves and can see whether it is healthy.
|
||||
- **success:** Only Admin-group members reach the Admin pages; the accent colour is checked for contrast in both themes with a warning but allowed; runtime settings change without restart; the status page shows backup age, failed e-mails and job runs; the audit log is readable.
|
||||
- **CAP-8** [FR-28]
|
||||
- **intent:** The Admin can reconstruct who changed what.
|
||||
- **success:** Every password change, profile change, e-mail change step, picture action, announcement action, settings change and throttled or failed attempt produces an entry with who, what, when and IP, kept 12 months, holding no password, token or picture content.
|
||||
- **CAP-9** [FR-29..FR-31]
|
||||
- **intent:** Each user sees a clear landing page and sets how the portal looks and speaks on their own device.
|
||||
- **success:** The dashboard shows active announcements and an account card with the password date; theme, language and German form of address are chosen from the avatar menu and stored on the device; German and English work, with formal and informal German; the portal is usable on desktop and phone and meets WCAG 2.1 AA.
|
||||
- **CAP-10** [FR-26, FR-27]
|
||||
- **intent:** The portal runs unchanged in any environment from configuration alone.
|
||||
- **success:** One Docker image starts only when all required environment variables are valid; no image content names ping-mee, a domain, OU, group, IP or path; secrets appear in no log or UI; a health check works without AD; consistent database backup files appear in the backup folder on schedule.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Architecture is binding as written in `ARCHITECTURE-SPINE.md` (rules AD-1..AD-19): Django modular monolith with one owner per external system.
|
||||
- One container, one worker, SQLite on a volume; the portal writes its own consistent backups, because the host copy job copies directories.
|
||||
- AD is reached only over LDAPS with a service account limited to a configurable OU and fixed read and write attribute lists; `cn` and `name` are never edited.
|
||||
- The existing PowerShell sync, logon and registry scripts stay unchanged; the portal only writes `<sAMAccountName>.jpg` to the source folder and reads `<sAMAccountName>.md5` from the processed folder.
|
||||
- The portal is internet-facing: no injection, no cross-user access, throttling before any AD call, passwords never stored or logged, strict content-security policy with no inline script and no external fonts, scripts or CDNs.
|
||||
- Expired, forgotten or must-change passwords are never handled by the portal; the Admin resets them in AD.
|
||||
- Precedence when companions differ: behavior follows the PRD, architecture follows the spine, look follows `DESIGN.md`, screen behavior follows `EXPERIENCE.md`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No forgotten-password or expired-password flow.
|
||||
- No admin console for other users' data: Admins cannot edit another user's profile, picture or password.
|
||||
- No renaming of AD objects (`cn`, `name`).
|
||||
- No multi-domain support.
|
||||
- No replacement of the PowerShell scripts or the sync task.
|
||||
- No public status or announcement page; signed-out visitors see nothing.
|
||||
- No SVG uploads.
|
||||
- No alerting beyond the Admin status page; no staging environment (a test OU is used instead).
|
||||
- No identity-provider features of its own: sign-in and multi-factor stay with authentik.
|
||||
|
||||
## Success signal
|
||||
|
||||
A family member with no help from the admin changes their password, name, phone number and picture in the portal, while the admin publishes a scheduled downtime announcement that every opted-in user receives by e-mail and sees as a banner, and an attacker's injection, cross-user and brute-force attempts against the internet-facing portal all fail and are recorded.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Default values are placeholders to confirm at build: failed-attempt limit and pause (5, 15 minutes), confirmation link lifetime (1 hour), picture output (512×512 JPEG), Pending timeout, throttle limits per endpoint, backup retention, and the "backup too old" age (26 hours).
|
||||
- E-mails use the portal-wide default form of address, because a per-device choice cannot reach server-sent mail.
|
||||
- The default accent colour is the Fluent 2 default blue (`#0F6CBD`) and needs verifying against the component package.
|
||||
- Active Directory attribute names for the password policy (`minPwdLength`, `pwdProperties`, `pwdHistoryLength`, `msDS-ResultantPSO`) need verifying in the test OU.
|
||||
- Users depend on authentik being available for sign-in; no fallback.
|
||||
- Data of users removed from AD is deleted by a nightly reconciliation; audit entries stay until retention ends.
|
||||
- Sample personas and journeys in the PRD and UX documents are illustrative.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What are the exact retry count and the IONOS sending limits for announcement e-mail?
|
||||
- How is a restore from the portal's backup files tested before it is needed?
|
||||
- Does sign-out also end the authentik session?
|
||||
- Which rich-text editor library is used, and how are the front-end assets built into the image?
|
||||
- Where does the Docker image live: a registry, or built on the host?
|
||||
- Is the early-clearing of Pending after a failed AD write by the sync script an acceptable limitation (PRD Q-9; accepted in the spine, confirm at first use)?
|
||||
@@ -0,0 +1,95 @@
|
||||
- id: "1"
|
||||
title: Project scaffold, configuration and container
|
||||
description: >-
|
||||
Create the Django project, environment-variable configuration that refuses to start when invalid, the multi-stage Docker image and compose file, the health check, JSON logging, security headers with a strict content-security policy, and the test setup with adapter fakes. Covers CAP-10 and the foundations of AD-1, AD-13 and AD-18 in the architecture spine.
|
||||
spec_checkpoint: true
|
||||
invoke_dev_with: >-
|
||||
Verify and pin current versions of Python, Django (5.2 LTS) and the other stack entries on the web before binding them. Nothing in the repository may name ping-mee, a domain, an OU, a group, an IP or a path.
|
||||
- id: "2"
|
||||
title: Audit, throttle and session foundations
|
||||
description: >-
|
||||
Build the audit service, the database-backed throttle counters, and server-side sessions with the username in its own column and a kill-by-user function. Covers CAP-8 and the base of CAP-1; rules AD-4, AD-6, AD-7 and AD-19.
|
||||
- id: "3"
|
||||
title: Directory adapter
|
||||
description: >-
|
||||
Build the only code that talks to Active Directory: lookup by sAMAccountName inside the configured OU, group membership, account state, password verification, password policy read, and the allow-listed writes, plus an in-memory fake. Covers CAP-1 to CAP-3; rules AD-1, AD-2, AD-5 and AD-8.
|
||||
spec_checkpoint: true
|
||||
invoke_dev_with: >-
|
||||
Use python-ldap, not ldap3. LDAPS must verify against a CA bundle supplied by environment variable. Test only against a test OU with a test service account scoped to it, never against the production OU. Verify the password-policy attribute names in the test OU.
|
||||
- id: "4"
|
||||
title: Sign-in with OIDC and roles
|
||||
description: >-
|
||||
Implement sign-in through the identity provider using the authorization-code flow, session creation, roles from AD group membership with a short cache, disabled accounts cut off, and the trusted-proxy client IP rule. Covers CAP-1; rules AD-5, AD-6 and AD-7.
|
||||
spec_checkpoint: true
|
||||
done_checkpoint: true
|
||||
invoke_dev_with: >-
|
||||
Use Authlib. Authentik is the production instance only: the portal gets its own dedicated OIDC application and provider, restricted to test users while testing. Never change any existing authentik application. The proxy is Zoraxy: take the last X-Forwarded-For entry from a trusted proxy and never X-Real-IP.
|
||||
- id: "5"
|
||||
title: Page shell, preferences and translations
|
||||
description: >-
|
||||
Build the base layout with the Fluent web components bundled into the image: top bar, left menu with role-based groups, avatar menu for theme, language and German form of address, German and English text, light and dark themes, and the phone layout. Covers CAP-9 and the layout parts of DESIGN.md and EXPERIENCE.md.
|
||||
invoke_dev_with: >-
|
||||
No inline scripts and no external assets. Preferences live in cookies; the theme is set by the server and CSS, not by script. Follow the mock-ups in the UX folder.
|
||||
- id: "6"
|
||||
title: Scheduler and mail outbox
|
||||
description: >-
|
||||
Build the single 30-second scheduler tick with idempotent jobs, job-run records, the mailer adapter and the outbox with retries, backoff and a rate limit, and a fake mailer. Covers CAP-6 and CAP-10; rules AD-11 and AD-12.
|
||||
- id: "7"
|
||||
title: Password change
|
||||
description: >-
|
||||
Build the password page with the Active Directory password policy listed up front, the current-password check with the attempt cap, the LDAPS change, ending the user's other sessions, and the confirmation e-mail. Covers CAP-2 (FR-5 to FR-7); rules AD-2, AD-3 and AD-8.
|
||||
spec_checkpoint: true
|
||||
done_checkpoint: true
|
||||
invoke_dev_with: >-
|
||||
Test only against the test OU. Never log, store or audit a password. Plain-language messages for policy rejections.
|
||||
- id: "8"
|
||||
title: Profile edit
|
||||
description: >-
|
||||
Build the profile page for first name, last name, display name, telephone and mobile with validation and LDAP escaping. Covers part of CAP-3 (FR-8, FR-9); rules AD-2 and AD-3.
|
||||
- id: "9"
|
||||
title: E-mail change with confirmation and undo
|
||||
description: >-
|
||||
Build the e-mail change flow: current password, confirmation page from a link to the new address, notice with a 24-hour undo link to the old address, and the undo effects (restore, end sessions, notify the Admin). Covers CAP-3 (FR-10, FR-11); rules AD-8 and AD-9.
|
||||
invoke_dev_with: >-
|
||||
A link opened with GET must show a page and change nothing; only a POST with CSRF applies the change. Tokens are hashed at rest and single use.
|
||||
- id: "10"
|
||||
title: Picture backend
|
||||
description: >-
|
||||
Build upload validation, decoding of JPEG, PNG, HEIC and WebP with size and pixel limits, square re-encoding, the share adapter with atomic writes and timeouts, and the stored MD5 state. Covers CAP-4 (FR-12, FR-14, FR-15 backend); rule AD-10.
|
||||
spec_checkpoint: true
|
||||
invoke_dev_with: >-
|
||||
Pin the HEIC library after checking current versions on the web. Refuse SVG and everything else. Never store or serve the original upload. The share is mounted by the host; write only to the source folder and read the processed folder.
|
||||
- id: "11"
|
||||
title: Picture screens
|
||||
description: >-
|
||||
Build the picture page with in-browser cropping, the Pending status read from the share and Active Directory, and removal. Covers CAP-4 (FR-13 to FR-15 UI); see the picture mock-up and EXPERIENCE.md.
|
||||
- id: "12"
|
||||
title: Announcements core
|
||||
description: >-
|
||||
Build the announcement model with an always-set severity, draft, publish and withdraw, the list page and the banners, with safe Markdown rendering. Covers CAP-5 (FR-16 to FR-19 backend); rules AD-16 and AD-19.
|
||||
- id: "13"
|
||||
title: Announcement editor
|
||||
description: >-
|
||||
Build the publisher pages with the rich-text editor, downtime window, expiry, scheduled publishing, edit, and the "notify again" option. Covers CAP-5 (FR-16, FR-17, FR-18 UI); see the editor mock-up.
|
||||
invoke_dev_with: >-
|
||||
Choose a maintained open-source rich-text editor that outputs Markdown and can be bundled into the image, after checking current options on the web. The server must treat the submitted Markdown as untrusted.
|
||||
- id: "14"
|
||||
title: Announcement e-mail and opt-out
|
||||
description: >-
|
||||
Build recipient resolution (all users in the OU with a valid address minus opt-outs), the opt-in and opt-out page with its notice, the recipient summary for the Publisher, and the failure listing. Covers CAP-6 (FR-20, FR-21); rule AD-11.
|
||||
- id: "15"
|
||||
title: Dashboard
|
||||
description: >-
|
||||
Build the landing page with active announcement banners and the account card, including the password last changed date. Covers CAP-9 (FR-30); see the dashboard mock-up.
|
||||
- id: "16"
|
||||
title: Admin pages and maintenance jobs
|
||||
description: >-
|
||||
Build the Admin pages (accent colour with contrast warning, backgrounds, runtime settings, audit log viewer, status page) and the maintenance jobs (database backup with retention, nightly AD reconciliation, audit retention). Covers CAP-7, CAP-8 and CAP-10; rules AD-12 to AD-14.
|
||||
done_checkpoint: true
|
||||
invoke_dev_with: >-
|
||||
Backups use the SQLite online backup API into the backup folder; the status page shows the age of the last good backup.
|
||||
- id: "17"
|
||||
title: Hardening and acceptance
|
||||
description: >-
|
||||
Run security tests (injection, cross-user access, throttling, session handling), the accessibility pass, and write deployment notes and a smoke checklist for the test OU. Covers all capabilities and the success signal in SPEC.md.
|
||||
done_checkpoint: true
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
topic: ping-mee Self-Service Portal UX
|
||||
updated: 2026-10-15T11:57
|
||||
---
|
||||
|
||||
|
||||
- (event) Create intent. Source candidates (paths only): brief, brief addendum, prd, prd addendum, ARCHITECTURE-SPINE (AD-16 strict CSP, vendored Fluent web components 3.x, server-rendered Django, de+en i18n).
|
||||
- (decision) Feel (user words): a utility, simple and straight to the point; users are NOT tech savvy; imagined like the M365 admin center: clean, simple, 'just a big page for settings'; not too much for the user; settings intuitive and informative.
|
||||
- (decision) Themes: light AND dark, default follows system theme (prefers-color-scheme), user can override. Form factors: desktop AND mobile both must work (responsive web).
|
||||
- (assumption) Working mode not stated by user - proceeding in COACHING path (suggested); stakes = internal tool.
|
||||
- (decision) IA: option B (short left menu, one topic per page) PLUS a big dashboard as the landing page showing announcements and some account information.
|
||||
- (decision) 'Informative' = transparency about what each setting does (explain effect of each field/action, e.g. password rules before typing, picture 'updates within ~2h'/pending).
|
||||
- (decision) Voice: German formal (Sie) by default with a setting to switch to informal (du). Language auto-detected from browser language, switchable.
|
||||
- (decision) Colour: Microsoft blue by default; colour must be changeable in settings. Theme (light/dark/system), formal/informal, and language switch live in ONE menu opened from the profile picture in the upper right corner; stored PER DEVICE.
|
||||
- (assumption) To confirm: is the colour change an Admin (global, PRD FR-22) setting or per-user? Is formal/informal German-only? Dashboard 'account info' contents.
|
||||
- (decision) Accent colour is PORTAL-WIDE (Admin sets it; matches PRD FR-22); user menu keeps theme/language/formality only. Formal/informal is a GERMAN-ONLY setting (hidden when language is English).
|
||||
- (decision) Dashboard account card also shows 'Password last changed on ...' (from AD pwdLastSet). No further pages wanted beyond the IA table.
|
||||
- (assumption) UNANSWERED: should informal setting apply to e-mails? Assumed: e-mails use the portal-wide default register (per-device choice cannot reach server-sent mail). Also assumed default brand blue = Fluent 2 web components default; verify at scaffold.
|
||||
- (event) Rendered 3 key-screen mocks in .working/: key-dashboard.html (desktop light + menu open, phone dark), key-password.html, key-picture-pending.html. German formal copy. Password rules in the mock are SAMPLE text; real rules need domain policy (new requirement: portal reads AD password policy).
|
||||
- (assumption) Mock layout assumptions: top bar in brand blue (dark: deep navy), left nav 6 items (collapses to burger on phone), banner for active announcement (left accent bar by severity), card grid on dashboard (stacked on phone).
|
||||
- (decision) Mock review feedback: look approved ('really good', M365-admin-centre feel right); wording 'perfect'; dashboard 'clean'. Defect: phone dashboard cut off on right -> fixed (main min-width:0, rows stack on phone, text wraps).
|
||||
- (decision) States, wording rules and accessibility floor drafted by facilitator accepted by user ('no' changes): saved/error/too-many-attempts/pending/loading/empty/unavailable states; plain-language, no jargon (no LDAP/Verzeichnis/Token); strings in one translation file x3 (de-Sie, de-du, en); WCAG 2.1 AA, keyboard, skip link, icon+text not colour alone, 44px touch targets, errors linked to fields and announced.
|
||||
- (decision) Accent colour check: portal computes contrast for the chosen colour (light and dark theme) and shows a CHECK result plus a WARNING when below AA; Admin may still save (warn, not block).
|
||||
- (decision) Password rules: portal READS the policy from AD and lists it before typing (NEW requirement - affects PRD FR-5 and ARCHITECTURE-SPINE AD-2 read allow-list; consider Fine-Grained Password Policy msDS-ResultantPSO).
|
||||
- (decision) User wants MOCKS of Publisher and Admin screens (announcement editor, admin appearance, status, audit log) to give opinion.
|
||||
- (event) Rendered 4 admin/publisher mocks in .working/: key-announcement-editor, key-admin-appearance (contrast check+warning, computed 1.41:1 white vs #FFD60A, 14.02:1 dark text), key-admin-status (backup age, failed mail, jobs), key-admin-audit. Nav gains groups 'Veröffentlichen' (Publisher) and 'Administration' (Darstellung, Einstellungen, Protokoll, Status). Awaiting user opinion.
|
||||
- (decision) Mock feedback: admin/publisher mocks approved otherwise ('cannot add anything'). CHANGE 1: announcement body uses a WYSIWYG editor (toolbar, formatted as you type) instead of visible Markdown; stored format stays Markdown (editor converts), server still sanitizes on render; e-mail keeps plain-text part. Affects PRD FR-16 wording + ARCHITECTURE-SPINE AD-16; editor library undecided (needs web verification; must be vendored, CSP-safe).
|
||||
- (decision) CHANGE 2: Importance (severity) is ALWAYS set, not optional: default Information, shown in the main card. Optional card keeps only downtime window, expiry, schedule. Affects PRD FR-17 (severity was listed optional).
|
||||
- (event) Mockups promoted from .working/ to mockups/ (7 files). All 7 IA surfaces with a layout driver are mocked; spine-only: Profil, Benachrichtigungen, Ankündigungen (read list), confirm/undo pages, Einstellungen.
|
||||
- (event) spines finalized
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: ping-mee Portal
|
||||
description: Calm, utility-style self-service portal for a homelab, in the spirit of the Microsoft 365 admin center. Fluent 2 web components; this DESIGN.md specifies the brand-layer delta and the portal's own components.
|
||||
status: final
|
||||
updated: 2026-10-15
|
||||
sources:
|
||||
- _bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15/prd.md
|
||||
- _bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md
|
||||
colors:
|
||||
# Fluent 2 neutrals are inherited. Only the brand and the portal's semantic colours are listed.
|
||||
# Brand is portal-wide and Admin-changeable; this is the default (Fluent 2 default brand blue). [ASSUMPTION: verify against @fluentui/web-components 3.x at scaffold]
|
||||
brand: '#0F6CBD'
|
||||
on-brand: '#FFFFFF'
|
||||
brand-dark: '#479EF5'
|
||||
on-brand-dark: '#0A0A0A'
|
||||
top-bar-dark: '#0F2A43'
|
||||
background: '#FAFAFA'
|
||||
surface: '#FFFFFF'
|
||||
nav-surface: '#F5F5F5'
|
||||
text: '#242424'
|
||||
text-secondary: '#616161'
|
||||
line: '#E0E0E0'
|
||||
background-dark: '#1F1F1F'
|
||||
surface-dark: '#292929'
|
||||
nav-surface-dark: '#242424'
|
||||
text-dark: '#FFFFFF'
|
||||
text-secondary-dark: '#C8C8C8'
|
||||
line-dark: '#3D3D3D'
|
||||
severity-info: '#0F6CBD'
|
||||
severity-warning: '#F7630C'
|
||||
severity-outage: '#B10E1C'
|
||||
severity-outage-dark: '#FF99A4'
|
||||
success: '#107C10'
|
||||
success-dark: '#6CCB5F'
|
||||
pending-surface: '#FFF4CE'
|
||||
pending-text: '#5C4400'
|
||||
pending-surface-dark: '#4A3B00'
|
||||
pending-text-dark: '#FFE9A8'
|
||||
typography:
|
||||
# System font stack only: the security policy forbids external fonts.
|
||||
family:
|
||||
fontFamily: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif'
|
||||
page-title:
|
||||
fontSize: 24px
|
||||
fontWeight: '600'
|
||||
lineHeight: '1.25'
|
||||
page-title-phone:
|
||||
fontSize: 20px
|
||||
fontWeight: '600'
|
||||
lineHeight: '1.25'
|
||||
card-title:
|
||||
fontSize: 16px
|
||||
fontWeight: '600'
|
||||
body:
|
||||
fontSize: 14px
|
||||
fontWeight: '400'
|
||||
lineHeight: '1.45'
|
||||
label:
|
||||
fontSize: 14px
|
||||
fontWeight: '600'
|
||||
small:
|
||||
fontSize: 13px
|
||||
hint:
|
||||
fontSize: 12px
|
||||
mono-editor:
|
||||
fontFamily: 'Consolas, monospace'
|
||||
fontSize: 13px
|
||||
rounded:
|
||||
sm: 4px
|
||||
md: 6px
|
||||
lg: 8px
|
||||
pill: 10px
|
||||
full: 9999px
|
||||
spacing:
|
||||
card-padding: 16px 18px
|
||||
card-gap: 14px
|
||||
page-padding: 24px 28px
|
||||
page-padding-phone: 16px
|
||||
nav-width: 220px
|
||||
top-bar-height: 48px
|
||||
touch-target-min: 44px
|
||||
components:
|
||||
top-bar:
|
||||
background: '{colors.brand}'
|
||||
foreground: '{colors.on-brand}'
|
||||
height: '{spacing.top-bar-height}'
|
||||
top-bar-dark:
|
||||
background: '{colors.top-bar-dark}'
|
||||
foreground: '{colors.text-dark}'
|
||||
nav-item:
|
||||
foreground: '{colors.text}'
|
||||
radius: '{rounded.sm}'
|
||||
nav-item-active:
|
||||
background: '{colors.surface}'
|
||||
border: 'inset 3px left {colors.brand}'
|
||||
fontWeight: '600'
|
||||
card:
|
||||
background: '{colors.surface}'
|
||||
border: '1px solid {colors.line}'
|
||||
radius: '{rounded.lg}'
|
||||
padding: '{spacing.card-padding}'
|
||||
banner:
|
||||
radius: '{rounded.md}'
|
||||
border: '1px solid, left 5px, colour by severity'
|
||||
button-primary:
|
||||
background: '{colors.brand}'
|
||||
foreground: '{colors.on-brand}'
|
||||
radius: '{rounded.sm}'
|
||||
button-secondary:
|
||||
background: 'transparent'
|
||||
foreground: '{colors.text}'
|
||||
border: '1px solid'
|
||||
radius: '{rounded.sm}'
|
||||
chip-pending:
|
||||
background: '{colors.pending-surface}'
|
||||
foreground: '{colors.pending-text}'
|
||||
radius: '{rounded.pill}'
|
||||
input:
|
||||
border: '1px solid, bottom 2px'
|
||||
radius: '{rounded.sm}'
|
||||
avatar:
|
||||
radius: '{rounded.full}'
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
|
||||
A calm utility. The portal should feel like the Microsoft 365 admin center's settings pages: clean, plain and confident, one clear task per page, with nothing decorative. The people who use it are not technical, so the design earns trust through restraint and legibility, not through personality. It follows Fluent 2 so the controls behave the way these users already know from Microsoft products.
|
||||
|
||||
Light and dark are equal citizens. The portal follows the system theme by default and the user can override it per device. The accent colour is portal-wide and set by the Admin.
|
||||
|
||||
Mock-ups: `mockups/key-dashboard.html`, `mockups/key-password.html`, `mockups/key-picture-pending.html`, `mockups/key-announcement-editor.html`, `mockups/key-admin-appearance.html`, `mockups/key-admin-status.html`, `mockups/key-admin-audit.html`. The spine wins on conflict.
|
||||
|
||||
## Colors
|
||||
|
||||
- **`{colors.brand}`** is the one accent: top bar, primary buttons, active-nav marker and focus emphasis. It is Admin-changeable (FR-22). In dark mode the lighter `{colors.brand-dark}` is used with `{colors.on-brand-dark}` text.
|
||||
- **Neutrals** (`background`, `surface`, `nav-surface`, `text`, `text-secondary`, `line`) carry almost everything; the dark variants have the `-dark` suffix. Cards sit on `{colors.surface}` above `{colors.background}`.
|
||||
- **Severity colours** colour only the left edge and icon of an announcement banner: `{colors.severity-info}`, `{colors.severity-warning}`, `{colors.severity-outage}`. Colour is never the only signal; each severity also has an icon and a word.
|
||||
- **Pending** uses `{colors.pending-surface}` and `{colors.pending-text}` for the "being updated" chip and nothing else.
|
||||
- **Success** (`{colors.success}`) marks healthy job status and saved confirmations.
|
||||
- **Contrast.** All text and control pairs meet WCAG 2.1 AA in both themes. The Admin colour check (EXPERIENCE.md, Admin Appearance) computes contrast for the chosen brand colour against `{colors.on-brand}` (light) and `{colors.on-brand-dark}` (dark) and warns when below 4.5:1.
|
||||
|
||||
## Typography
|
||||
|
||||
One family, the system stack `{typography.family}`, because the security policy allows no external fonts. A small ramp: `{typography.page-title}` for the page heading (`{typography.page-title-phone}` on phones), `{typography.card-title}` for cards, `{typography.body}` for text, `{typography.label}` for field labels, `{typography.small}` and `{typography.hint}` for explanations. The announcement editor's source view, if shown, uses `{typography.mono-editor}`. Sentence case everywhere; no all-caps except the small group headings in the menu.
|
||||
|
||||
## Layout & Spacing
|
||||
|
||||
Left menu (`{spacing.nav-width}`) plus a content area on desktop; the menu collapses behind a menu button on phones. Content is left-aligned in cards on a single column; two columns of cards only on the dashboard and the status page, stacked on phones. Page padding is `{spacing.page-padding}` (`{spacing.page-padding-phone}` on phones); cards are separated by `{spacing.card-gap}`. Lines of explanatory text stay below about 640 px wide. Touch targets are at least `{spacing.touch-target-min}` on phones. On phones, label/value rows stack and long values wrap, so nothing is cut off at the right edge.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
Flat. Depth comes from a 1 px `{colors.line}` border and the surface/background step, not from shadows. The only shadows are on the profile menu (a popover) and on dialogs.
|
||||
|
||||
## Shapes
|
||||
|
||||
Small, even radii: `{rounded.sm}` for buttons and inputs, `{rounded.md}` for banners, `{rounded.lg}` for cards, `{rounded.pill}` for chips, `{rounded.full}` for avatars.
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Visual spec |
|
||||
|---|---|
|
||||
| Top bar | `{components.top-bar}`: portal name at left, profile picture at right (opens the profile menu). On phones a menu button replaces the left menu. Dark mode uses `{components.top-bar-dark}`. |
|
||||
| Left menu | Items `{components.nav-item}`; active item `{components.nav-item-active}`. Group headings "Veröffentlichen" and "Administration" appear only for Publishers and Admins, in small upper-case text. |
|
||||
| Card | `{components.card}`; title `{typography.card-title}`. |
|
||||
| Announcement banner | `{components.banner}`: left edge in the severity colour, icon, bold title, one or two lines of text. At most the active announcements, newest first. |
|
||||
| Pending chip | `{components.chip-pending}` with the text "Wird aktualisiert". |
|
||||
| Buttons | Primary `{components.button-primary}`; secondary `{components.button-secondary}`. One primary per card. |
|
||||
| Fields | `{components.input}`; label above, hint below in `{typography.hint}`; error text below in the outage colour with an icon. |
|
||||
| Profile menu | Popover from the avatar: name and username, theme (Hell, Dunkel, System), language (Deutsch, English), address form (Sie, Du; German only), sign-out. |
|
||||
| Rich text editor | Toolbar above a text area that shows the result: bold, italic, heading, list, link. Same border as inputs. |
|
||||
| Tables | Compact rows with `{colors.line}` separators, used for the audit log, job status and failed mail. |
|
||||
| Filter chips | Pill buttons; the selected chip uses `{colors.brand}`. |
|
||||
| Colour field (Admin) | Swatch, hex field, contrast table (light and dark), warning box. |
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
- **Do** keep one primary action per card and say what it will do and what happens next.
|
||||
- **Do** show state in words as well as colour.
|
||||
- **Do** keep pages short; split a long page before adding scrolling sections.
|
||||
- **Don't** add decoration, illustrations or animation beyond Fluent's own.
|
||||
- **Don't** use more than the one brand accent.
|
||||
- **Don't** hide a state in colour alone, or in hover.
|
||||
- **Don't** load fonts, icons or scripts from the network.
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: ping-mee Portal
|
||||
status: final
|
||||
updated: 2026-10-15
|
||||
sources:
|
||||
- _bmad-output/prds/prd-ping-mee-self-service-platform-2026-10-15/prd.md
|
||||
- _bmad-output/architecture/architecture-ping-mee-self-service-platform-2026-10-15/ARCHITECTURE-SPINE.md
|
||||
---
|
||||
|
||||
# ping-mee Portal — Experience Spine
|
||||
|
||||
## Foundation
|
||||
|
||||
Responsive web, server-rendered pages. Desktop and phone both work. UI system: Fluent 2 web components (version 3), built into the image. `DESIGN.md` is the visual identity reference and names the brand layer; this spine is the behavior. Users are family and friends who are not technical, so every page does one job and explains it. Languages: German and English. German has two address forms, formal "Sie" (default) and informal "du".
|
||||
|
||||
## Information Architecture
|
||||
|
||||
| Surface | Reached from | Purpose | Who |
|
||||
|---|---|---|---|
|
||||
| Startseite (dashboard) | Landing page, menu | Active announcements, account card, last announcements | everyone |
|
||||
| Profil | Menu, account card | Edit first name, last name, display name, e-mail, telephone, mobile | everyone |
|
||||
| Profilbild | Menu | Upload, crop, remove; shows Pending | everyone |
|
||||
| Passwort | Menu | Change password | everyone |
|
||||
| Ankündigungen | Menu, dashboard | Read current and past announcements | everyone |
|
||||
| Benachrichtigungen | Menu | Opt in or out of announcement e-mails | everyone |
|
||||
| E-Mail bestätigen / rückgängig | Link in an e-mail | Confirm or undo an e-mail change | link holder |
|
||||
| Ankündigungen verwalten | Menu group "Veröffentlichen" | List, write, schedule, edit, withdraw | Publishers |
|
||||
| Darstellung | Menu group "Administration" | Accent colour with contrast check, background image | Admins |
|
||||
| Einstellungen | same | Portal name, session limits, throttle limits, audit retention | Admins |
|
||||
| Protokoll | same | Read the audit log | Admins |
|
||||
| Status | same | Backup age, failed e-mails, background jobs | Admins |
|
||||
| Profile menu | Avatar, upper right | Theme, language, address form (German only), sign-out | everyone |
|
||||
|
||||
On phones the left menu opens from a menu button. Dialogs never stack more than one level deep. Every stated need has a surface, and every surface is reached by a flow below.
|
||||
|
||||
→ Composition reference: `mockups/key-dashboard.html`, `mockups/key-password.html`, `mockups/key-picture-pending.html`, `mockups/key-announcement-editor.html`, `mockups/key-admin-appearance.html`, `mockups/key-admin-status.html`, `mockups/key-admin-audit.html`. Spine wins on conflict.
|
||||
|
||||
## Voice and Tone
|
||||
|
||||
Microcopy. Brand posture lives in `DESIGN.md`. All text is translatable and kept in one translation file with three registers: German formal, German informal, English. The e-mails use the portal-wide default register. [ASSUMPTION: a per-device choice cannot reach server-sent mail.]
|
||||
|
||||
| Do | Don't |
|
||||
|---|---|
|
||||
| "Ihre Änderungen wurden gespeichert." | "Operation erfolgreich." |
|
||||
| "Ihr neues Bild erscheint innerhalb von etwa 2 Stunden." | "Der Sync-Task läuft periodisch." |
|
||||
| "Zu viele Versuche. Bitte warten Sie einige Minuten." | "HTTP 429" |
|
||||
| "Das ist gerade nicht möglich. Bitte versuchen Sie es später erneut." | Directory, LDAP, token or other system words |
|
||||
| Say what the action does and what happens next. | Leave the user guessing after a click. |
|
||||
| Short sentences, one idea each. | Long explanations before the form. |
|
||||
|
||||
The informal setting changes only the form of address ("Sie" becomes "du", with matching verbs and possessives) and applies only to German.
|
||||
|
||||
## Component Patterns
|
||||
|
||||
Behavioral. Visual specs live in `DESIGN.md.Components`.
|
||||
|
||||
| Component | Use | Behavioral rules |
|
||||
|---|---|---|
|
||||
| Announcement banner | Dashboard | Shows every active announcement, newest first, severity in colour, icon and word. Cannot be dismissed while active. |
|
||||
| Account card | Dashboard | Shows picture (with Pending chip if waiting), name, username, e-mail, telephone, "Passwort zuletzt geändert" from AD. Link to Profil. |
|
||||
| Form | Profil, Passwort, Profilbild | Explains the effect of the action above the first field. Validates on submit, shows errors next to the field and in a summary at the top. Primary button shows a spinner while working and is disabled. |
|
||||
| Password rules | Passwort | The rules read from the domain's password policy are listed before the user types. [New requirement: the portal reads the policy from AD.] |
|
||||
| Sensitive action | Passwort, E-Mail change | Asks for the current password in the same form. Never remembers it. |
|
||||
| Picture editor | Profilbild | Choose a file, crop to a square and adjust in the browser, then save. Accepts JPEG, PNG, HEIC, WebP up to 10 MB; says why when it refuses a file. |
|
||||
| Rich text editor | Ankündigungen verwalten | Toolbar: bold, italic, heading, list, link. The text shows as the finished result. Stored as Markdown, sanitized when shown. |
|
||||
| Importance | Announcement editor | Always set; default "Information". Cannot be left empty. |
|
||||
| Recipient summary | Announcement editor | States how many users get the e-mail and why others do not (opted out, no address). |
|
||||
| Contrast check | Darstellung | Shows contrast for light and dark. Below 4.5:1 shows a warning; saving stays possible. |
|
||||
| Profile menu | Every page | Theme, language, address form (shown only in German), sign-out. Choices are stored per device and apply immediately. |
|
||||
| Filter chips | Protokoll | One selected at a time; "Alle" by default. |
|
||||
|
||||
## State Patterns
|
||||
|
||||
| State | Surface | Treatment |
|
||||
|---|---|---|
|
||||
| Saved | Any form | A success message at the top: what was saved and what happens next. |
|
||||
| Validation error | Any form | Message under the field, linked to it, plus a summary; focus moves to the summary. |
|
||||
| Password rejected by policy | Passwort | The reason in plain words, for example "Das Passwort ist zu kurz." Nothing is changed. |
|
||||
| Too many attempts | Passwort, E-Mail change, links, upload | "Zu viele Versuche. Bitte warten Sie einige Minuten." No technical detail; the pause is not shown in numbers. |
|
||||
| Pending | Profilbild, dashboard | Yellow chip "Wird aktualisiert" and the two-hour note. If it lasts longer than the configured time: "Das dauert länger als erwartet." |
|
||||
| Loading | Buttons | Spinner on the button; the page stays usable. Cold page loads are plain server-rendered pages with no skeleton. |
|
||||
| Empty | Ankündigungen, lists | A friendly line, for example "Derzeit gibt es keine Ankündigungen." |
|
||||
| Unavailable | Any action needing AD, the share or mail | "Das ist gerade nicht möglich. Bitte versuchen Sie es später erneut." Nothing changes. |
|
||||
| Session ended | Any page | The next action leads to sign-in with "Aus Sicherheitsgründen wurden Sie abgemeldet." [ASSUMPTION] |
|
||||
| Not allowed | Publisher or Admin page for a normal user | The page does not exist for them: no menu entry; a direct address shows "Dazu fehlt Ihnen die Berechtigung." |
|
||||
| Link used or expired | Confirm and undo pages | "Dieser Link ist abgelaufen oder wurde schon verwendet." with a way back to the portal. |
|
||||
| Contrast warning | Darstellung | Warning box with the reason; "Trotzdem speichern" and "Andere Farbe wählen". |
|
||||
| Backup older than 26 hours / failed mail | Status | The value turns red with a sentence saying what it means. |
|
||||
| No account match | Sign-in | A generic refusal; no hint whether the name exists. |
|
||||
|
||||
## Interaction Primitives
|
||||
|
||||
- **Keyboard:** everything works with the keyboard; the focus outline is always visible; a "Zum Inhalt springen" link is the first tab stop. Escape closes the profile menu and dialogs.
|
||||
- **Touch:** targets are at least 44 px; no hover-only controls.
|
||||
- **Preferences per device:** theme (Hell, Dunkel, System; default System), language (default from the browser; switchable), and the German address form (default formal). They live in the profile menu from the avatar in the upper right and take effect without leaving the page. They are not stored on the server.
|
||||
- **Confirmation links:** a link opens a page that names the change; only a button press applies it.
|
||||
- **Banned:** infinite scroll, hover-only actions, dialogs stacked more than one deep, countdown timers visible to ordinary users, any action that runs on opening a link.
|
||||
|
||||
## Accessibility Floor
|
||||
|
||||
Behavioral. Visual contrast lives in `DESIGN.md`. Target: WCAG 2.1 AA.
|
||||
|
||||
- Full keyboard use and a skip link.
|
||||
- Colour is never the only signal; banners carry an icon and a word.
|
||||
- Form errors are linked to their fields and announced to screen readers.
|
||||
- Headings are in order; each page has one main heading.
|
||||
- Labels are visible, never only placeholders.
|
||||
- Text can be enlarged to 200 percent without loss of content.
|
||||
- The Admin accent colour is checked for contrast before it can be applied, with a warning.
|
||||
|
||||
## Responsive & Platform
|
||||
|
||||
Two layouts. Wide: left menu plus content. Narrow (phones): content only, with the menu behind a menu button; label/value rows stack; the two-column card grids become one column. The break is around 720 px. [ASSUMPTION: exact breakpoint decided at build.] Dark and light follow the system by default.
|
||||
|
||||
## Inspiration & Anti-patterns
|
||||
|
||||
Inspiration: the Microsoft 365 admin center's settings pages (calm, one topic per page, explanations beside controls). Anti-patterns: dense admin tables for ordinary users; long walls of help text; jargon from directory services; many accent colours.
|
||||
|
||||
## Key Flows
|
||||
|
||||
Journeys mirror the PRD (UJ-1 to UJ-4).
|
||||
|
||||
- **UJ-1. Lena changes her password before it leaks.** Lena signs in through authentik and lands on the dashboard. She opens Passwort. The page tells her the effect (other devices are signed out, she gets an e-mail) and lists the rules. She enters her current password and the new one twice and presses "Passwort ändern". *Climax:* the page says her password was changed. *Resolution:* she receives a confirmation e-mail. *Edge:* a rule is broken; the reason shows in plain words and nothing changes.
|
||||
- **UJ-2. Tom sets a profile picture from his iPhone.** On Profilbild Tom picks a HEIC photo, crops it square and saves. *Climax:* the page shows his current picture with the chip "Wird aktualisiert" and the two-hour note. *Resolution:* when the sync has run, the chip disappears. *Edge:* a 14 MB file is refused with a reason before upload.
|
||||
- **UJ-3. Ebbe announces a maintenance window.** In Ankündigungen verwalten Ebbe writes a title, sets importance to Warnung, writes the text in the editor, sets the maintenance window and an expiry, and schedules it for the next morning. The recipient summary shows who will be e-mailed. *Climax:* at the scheduled time the banner appears on every dashboard and the e-mail goes out. *Resolution:* after the move he withdraws it. *Edge:* he corrects the end time and chooses "Benutzer erneut benachrichtigen".
|
||||
- **UJ-4. Marie sees an e-mail change she did not make.** Marie opens the undo link from the notice at her old address. The page names the change and shows one button. *Climax:* she presses "Änderung rückgängig machen" and her old address is back. *Resolution:* she is told to change her password, her sessions end, and the Admin is notified.
|
||||
|
||||
## Open items
|
||||
|
||||
- The password rules text depends on reading the domain policy from AD; this is a new requirement for the PRD (FR-5) and the architecture spine (AD-2 read allow-list).
|
||||
- The WYSIWYG editor changes PRD FR-16 and AD-16; the editor library is chosen at build and must be built into the image.
|
||||
- Severity is required with the default "Information" (PRD FR-17 listed it as optional).
|
||||
- The Einstellungen page was not mocked; it is a labelled list of fields with an explanation beside each.
|
||||
- The session-ended message and the exact phone breakpoint are assumptions.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Darstellung (Admin) - contrast warning state</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{text-align:left;color:var(--sub);font-weight:600;padding:6px 8px;border-bottom:1px solid var(--line)}
|
||||
td{padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.ok{color:#107c10}.dark .ok{color:#6ccb5f}
|
||||
.bad{color:#b10e1c;font-weight:600}.dark .bad{color:#ff99a4}
|
||||
.ta{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:8px 10px;font-size:13px;min-height:110px;white-space:pre-wrap;font-family:Consolas,monospace;color:var(--text)}
|
||||
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .two{grid-template-columns:1fr}
|
||||
.radio{display:inline-block;margin-right:14px;font-size:14px}
|
||||
.radio:before{content:"";display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid var(--fieldline);margin-right:6px;vertical-align:-2px}
|
||||
.radio.sel:before{border-color:var(--brand);background:radial-gradient(var(--brand) 40%,transparent 45%)}
|
||||
.swatch{display:inline-block;width:40px;height:28px;border-radius:4px;border:1px solid var(--fieldline);vertical-align:middle;margin-right:8px}
|
||||
.warnbox{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:10px 14px;font-size:13px;margin-top:10px}
|
||||
.filters span{display:inline-block;border:1px solid var(--fieldline);border-radius:14px;padding:2px 10px;font-size:12px;margin:0 6px 6px 0}
|
||||
.filters span.sel{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
|
||||
.navh{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);padding:12px 12px 4px}
|
||||
.tb{display:flex;gap:2px;border:1px solid var(--fieldline);border-bottom:0;border-radius:4px 4px 0 0;background:var(--nav);padding:4px}
|
||||
.tb span{padding:3px 10px;font-size:13px;border-radius:3px}
|
||||
.wys{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:0 0 4px 4px;background:var(--field);padding:10px 12px;font-size:14px;min-height:120px}
|
||||
.preview{border:1px dashed var(--fieldline);border-radius:6px;padding:10px 12px;font-size:13px}
|
||||
</style><body><h1 class="cap">Darstellung (Admin) - contrast warning state</h1><p class="cap">Accent colour with the contrast check for both themes and the warning; saving stays possible. Background upload. Illustrates: FR-22 and the check-and-warn decision. Colour values are computed.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="nav"><a class=""><i></i>Startseite</a><a class=""><i></i>Profil</a><a class=""><i></i>Profilbild</a><a class=""><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a><div class="navh">Veröffentlichen</div><a class=""><i></i>Ankündigungen verwalten</a><div class="navh">Administration</div><a class="on"><i></i>Darstellung</a><a class=""><i></i>Einstellungen</a><a class=""><i></i>Protokoll</a><a class=""><i></i>Status</a></div><div class="main">
|
||||
<h2>Darstellung</h2>
|
||||
<p class="lead">Farbe und Hintergrund gelten für alle Benutzer des Portals.</p>
|
||||
<div class="card" style="max-width:640px"><h3>Akzentfarbe</h3>
|
||||
<div class="field"><label>Farbe</label><span class="swatch" style="background:#FFD60A"></span><span class="in" style="display:inline-block;width:120px">#FFD60A</span> <span class="small"> Standard: #0F6CBD</span></div>
|
||||
<table><tr><th>Anzeige</th><th>Text auf der Farbe</th><th>Kontrast</th><th>Ergebnis</th></tr>
|
||||
<tr><td>Hell</td><td>Weiß</td><td>1.4 : 1</td><td class="bad">zu niedrig (Minimum 4,5 : 1)</td></tr>
|
||||
<tr><td>Dunkel</td><td>Fast schwarz</td><td>14.0 : 1</td><td class="ok">ausreichend</td></tr></table>
|
||||
<div class="warnbox"><b>Warnung:</b> Im hellen Design ist weißer Text auf dieser Farbe schwer lesbar. Sie können sie trotzdem speichern, aber manche Benutzer erkennen Schaltflächen dann schlechter.</div>
|
||||
<p style="margin:14px 0 0"><span class="btn">Trotzdem speichern</span><span class="btn sec">Andere Farbe wählen</span></p></div>
|
||||
<div class="card" style="max-width:640px"><h3>Hintergrundbild</h3>
|
||||
<p class="small" style="margin:0 0 8px">Das Bild erscheint nur für angemeldete Benutzer. Es wird beim Hochladen neu kodiert; bis 10 MB.</p>
|
||||
<span class="btn sec">Bild hochladen</span><span class="btn sec">Kein Bild</span></div>
|
||||
</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="main">
|
||||
<h2>Darstellung</h2>
|
||||
<p class="lead">Farbe und Hintergrund gelten für alle Benutzer des Portals.</p>
|
||||
<div class="card" style="max-width:640px"><h3>Akzentfarbe</h3>
|
||||
<div class="field"><label>Farbe</label><span class="swatch" style="background:#FFD60A"></span><span class="in" style="display:inline-block;width:120px">#FFD60A</span> <span class="small"> Standard: #0F6CBD</span></div>
|
||||
<table><tr><th>Anzeige</th><th>Text auf der Farbe</th><th>Kontrast</th><th>Ergebnis</th></tr>
|
||||
<tr><td>Hell</td><td>Weiß</td><td>1.4 : 1</td><td class="bad">zu niedrig (Minimum 4,5 : 1)</td></tr>
|
||||
<tr><td>Dunkel</td><td>Fast schwarz</td><td>14.0 : 1</td><td class="ok">ausreichend</td></tr></table>
|
||||
<div class="warnbox"><b>Warnung:</b> Im hellen Design ist weißer Text auf dieser Farbe schwer lesbar. Sie können sie trotzdem speichern, aber manche Benutzer erkennen Schaltflächen dann schlechter.</div>
|
||||
<p style="margin:14px 0 0"><span class="btn">Trotzdem speichern</span><span class="btn sec">Andere Farbe wählen</span></p></div>
|
||||
<div class="card" style="max-width:640px"><h3>Hintergrundbild</h3>
|
||||
<p class="small" style="margin:0 0 8px">Das Bild erscheint nur für angemeldete Benutzer. Es wird beim Hochladen neu kodiert; bis 10 MB.</p>
|
||||
<span class="btn sec">Bild hochladen</span><span class="btn sec">Kein Bild</span></div>
|
||||
</div></div></div></div></div></body></html>
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Protokoll (Admin)</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{text-align:left;color:var(--sub);font-weight:600;padding:6px 8px;border-bottom:1px solid var(--line)}
|
||||
td{padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.ok{color:#107c10}.dark .ok{color:#6ccb5f}
|
||||
.bad{color:#b10e1c;font-weight:600}.dark .bad{color:#ff99a4}
|
||||
.ta{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:8px 10px;font-size:13px;min-height:110px;white-space:pre-wrap;font-family:Consolas,monospace;color:var(--text)}
|
||||
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .two{grid-template-columns:1fr}
|
||||
.radio{display:inline-block;margin-right:14px;font-size:14px}
|
||||
.radio:before{content:"";display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid var(--fieldline);margin-right:6px;vertical-align:-2px}
|
||||
.radio.sel:before{border-color:var(--brand);background:radial-gradient(var(--brand) 40%,transparent 45%)}
|
||||
.swatch{display:inline-block;width:40px;height:28px;border-radius:4px;border:1px solid var(--fieldline);vertical-align:middle;margin-right:8px}
|
||||
.warnbox{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:10px 14px;font-size:13px;margin-top:10px}
|
||||
.filters span{display:inline-block;border:1px solid var(--fieldline);border-radius:14px;padding:2px 10px;font-size:12px;margin:0 6px 6px 0}
|
||||
.filters span.sel{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
|
||||
.navh{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);padding:12px 12px 4px}
|
||||
.tb{display:flex;gap:2px;border:1px solid var(--fieldline);border-bottom:0;border-radius:4px 4px 0 0;background:var(--nav);padding:4px}
|
||||
.tb span{padding:3px 10px;font-size:13px;border-radius:3px}
|
||||
.wys{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:0 0 4px 4px;background:var(--field);padding:10px 12px;font-size:14px;min-height:120px}
|
||||
.preview{border:1px dashed var(--fieldline);border-radius:6px;padding:10px 12px;font-size:13px}
|
||||
</style><body><h1 class="cap">Protokoll (Admin)</h1><p class="cap">Audit log with filter chips, 12-month note, IP column. Illustrates: FR-24 and FR-28. Sample data.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="nav"><a class=""><i></i>Startseite</a><a class=""><i></i>Profil</a><a class=""><i></i>Profilbild</a><a class=""><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a><div class="navh">Veröffentlichen</div><a class=""><i></i>Ankündigungen verwalten</a><div class="navh">Administration</div><a class=""><i></i>Darstellung</a><a class=""><i></i>Einstellungen</a><a class="on"><i></i>Protokoll</a><a class=""><i></i>Status</a></div><div class="main">
|
||||
<h2>Protokoll</h2><p class="lead">Wer hat wann was geändert? Einträge werden 12 Monate aufbewahrt. Passwörter und Bildinhalte werden nie gespeichert.</p>
|
||||
<div class="filters"><span class="sel">Alle</span><span>Passwort</span><span>Profil</span><span>E-Mail-Änderung</span><span>Profilbild</span><span>Ankündigungen</span><span>Einstellungen</span><span>Gedrosselt</span></div>
|
||||
<div class="card"><table><tr><th>Zeit</th><th>Benutzer</th><th>Aktion</th><th>IP-Adresse</th></tr>
|
||||
<tr><td>15.10. 09:41</td><td>lmeier</td><td>Passwort geändert</td><td>203.0.113.24</td></tr>
|
||||
<tr><td>15.10. 09:40</td><td>lmeier</td><td>Falsches aktuelles Passwort (1 von 5)</td><td>203.0.113.24</td></tr>
|
||||
<tr><td>15.10. 08:12</td><td>tkoch</td><td>Profilbild hochgeladen</td><td>198.51.100.7</td></tr>
|
||||
<tr><td>14.10. 20:03</td><td>msommer</td><td>E-Mail-Änderung rückgängig gemacht</td><td>198.51.100.33</td></tr>
|
||||
<tr><td>14.10. 17:30</td><td>ebass</td><td>Ankündigung veröffentlicht</td><td>192.0.2.10</td></tr></table></div>
|
||||
</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="main">
|
||||
<h2>Protokoll</h2><p class="lead">Wer hat wann was geändert? Einträge werden 12 Monate aufbewahrt. Passwörter und Bildinhalte werden nie gespeichert.</p>
|
||||
<div class="filters"><span class="sel">Alle</span><span>Passwort</span><span>Profil</span><span>E-Mail-Änderung</span><span>Profilbild</span><span>Ankündigungen</span><span>Einstellungen</span><span>Gedrosselt</span></div>
|
||||
<div class="card"><table><tr><th>Zeit</th><th>Benutzer</th><th>Aktion</th><th>IP-Adresse</th></tr>
|
||||
<tr><td>15.10. 09:41</td><td>lmeier</td><td>Passwort geändert</td><td>203.0.113.24</td></tr>
|
||||
<tr><td>15.10. 09:40</td><td>lmeier</td><td>Falsches aktuelles Passwort (1 von 5)</td><td>203.0.113.24</td></tr>
|
||||
<tr><td>15.10. 08:12</td><td>tkoch</td><td>Profilbild hochgeladen</td><td>198.51.100.7</td></tr>
|
||||
<tr><td>14.10. 20:03</td><td>msommer</td><td>E-Mail-Änderung rückgängig gemacht</td><td>198.51.100.33</td></tr>
|
||||
<tr><td>14.10. 17:30</td><td>ebass</td><td>Ankündigung veröffentlicht</td><td>192.0.2.10</td></tr></table></div>
|
||||
</div></div></div></div></div></body></html>
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Status (Admin)</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{text-align:left;color:var(--sub);font-weight:600;padding:6px 8px;border-bottom:1px solid var(--line)}
|
||||
td{padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.ok{color:#107c10}.dark .ok{color:#6ccb5f}
|
||||
.bad{color:#b10e1c;font-weight:600}.dark .bad{color:#ff99a4}
|
||||
.ta{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:8px 10px;font-size:13px;min-height:110px;white-space:pre-wrap;font-family:Consolas,monospace;color:var(--text)}
|
||||
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .two{grid-template-columns:1fr}
|
||||
.radio{display:inline-block;margin-right:14px;font-size:14px}
|
||||
.radio:before{content:"";display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid var(--fieldline);margin-right:6px;vertical-align:-2px}
|
||||
.radio.sel:before{border-color:var(--brand);background:radial-gradient(var(--brand) 40%,transparent 45%)}
|
||||
.swatch{display:inline-block;width:40px;height:28px;border-radius:4px;border:1px solid var(--fieldline);vertical-align:middle;margin-right:8px}
|
||||
.warnbox{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:10px 14px;font-size:13px;margin-top:10px}
|
||||
.filters span{display:inline-block;border:1px solid var(--fieldline);border-radius:14px;padding:2px 10px;font-size:12px;margin:0 6px 6px 0}
|
||||
.filters span.sel{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
|
||||
.navh{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);padding:12px 12px 4px}
|
||||
.tb{display:flex;gap:2px;border:1px solid var(--fieldline);border-bottom:0;border-radius:4px 4px 0 0;background:var(--nav);padding:4px}
|
||||
.tb span{padding:3px 10px;font-size:13px;border-radius:3px}
|
||||
.wys{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:0 0 4px 4px;background:var(--field);padding:10px 12px;font-size:14px;min-height:120px}
|
||||
.preview{border:1px dashed var(--fieldline);border-radius:6px;padding:10px 12px;font-size:13px}
|
||||
</style><body><h1 class="cap">Status (Admin)</h1><p class="cap">Backup age, failed e-mails and job runs, all in one place because nothing else alerts the Admin. Illustrates: AD-12, AD-14 status surfaces.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="nav"><a class=""><i></i>Startseite</a><a class=""><i></i>Profil</a><a class=""><i></i>Profilbild</a><a class=""><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a><div class="navh">Veröffentlichen</div><a class=""><i></i>Ankündigungen verwalten</a><div class="navh">Administration</div><a class=""><i></i>Darstellung</a><a class=""><i></i>Einstellungen</a><a class=""><i></i>Protokoll</a><a class="on"><i></i>Status</a></div><div class="main">
|
||||
<h2>Status</h2><p class="lead">Hier sehen Sie, ob die Hintergrundaufgaben laufen. Probleme bei Datensicherung und E-Mails erscheinen nur hier.</p>
|
||||
<div class="two">
|
||||
<div class="card"><h3>Datensicherung</h3><p style="margin:0"><b class="ok">Letzte erfolgreiche Sicherung: vor 3 Stunden</b></p><p class="small">Ihre Veeam-Kopie sichert diesen Ordner. Ist die Sicherung älter als 26 Stunden, wird sie hier rot.</p></div>
|
||||
<div class="card"><h3>E-Mail-Versand</h3><p style="margin:0"><b class="bad">2 E-Mails fehlgeschlagen</b></p><p class="small">Der Versand wird automatisch wiederholt. Nach 5 Versuchen bleibt die E-Mail hier stehen.</p></div></div>
|
||||
<div class="card"><h3>Fehlgeschlagene E-Mails</h3>
|
||||
<table><tr><th>Empfänger</th><th>Betreff</th><th>Versuche</th><th>Grund</th></tr>
|
||||
<tr><td>tom.k@example.com</td><td>Geplante Wartung am Samstag</td><td>3</td><td>Postfach voll</td></tr>
|
||||
<tr><td>marie.s@example.com</td><td>Geplante Wartung am Samstag</td><td>2</td><td>Server nicht erreichbar</td></tr></table></div>
|
||||
<div class="card"><h3>Hintergrundaufgaben</h3>
|
||||
<table><tr><th>Aufgabe</th><th>Letzter Lauf</th><th>Ergebnis</th></tr>
|
||||
<tr><td>Geplante Ankündigungen</td><td>vor 20 Sekunden</td><td class="ok">in Ordnung</td></tr>
|
||||
<tr><td>E-Mail-Versand</td><td>vor 20 Sekunden</td><td class="ok">in Ordnung</td></tr>
|
||||
<tr><td>Abgleich mit dem Konto-Verzeichnis</td><td>heute 03:00 Uhr</td><td class="ok">in Ordnung</td></tr>
|
||||
<tr><td>Protokoll aufräumen</td><td>heute 03:05 Uhr</td><td class="ok">in Ordnung</td></tr></table></div>
|
||||
</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="main">
|
||||
<h2>Status</h2><p class="lead">Hier sehen Sie, ob die Hintergrundaufgaben laufen. Probleme bei Datensicherung und E-Mails erscheinen nur hier.</p>
|
||||
<div class="two">
|
||||
<div class="card"><h3>Datensicherung</h3><p style="margin:0"><b class="ok">Letzte erfolgreiche Sicherung: vor 3 Stunden</b></p><p class="small">Ihre Veeam-Kopie sichert diesen Ordner. Ist die Sicherung älter als 26 Stunden, wird sie hier rot.</p></div>
|
||||
<div class="card"><h3>E-Mail-Versand</h3><p style="margin:0"><b class="bad">2 E-Mails fehlgeschlagen</b></p><p class="small">Der Versand wird automatisch wiederholt. Nach 5 Versuchen bleibt die E-Mail hier stehen.</p></div></div>
|
||||
<div class="card"><h3>Fehlgeschlagene E-Mails</h3>
|
||||
<table><tr><th>Empfänger</th><th>Betreff</th><th>Versuche</th><th>Grund</th></tr>
|
||||
<tr><td>tom.k@example.com</td><td>Geplante Wartung am Samstag</td><td>3</td><td>Postfach voll</td></tr>
|
||||
<tr><td>marie.s@example.com</td><td>Geplante Wartung am Samstag</td><td>2</td><td>Server nicht erreichbar</td></tr></table></div>
|
||||
<div class="card"><h3>Hintergrundaufgaben</h3>
|
||||
<table><tr><th>Aufgabe</th><th>Letzter Lauf</th><th>Ergebnis</th></tr>
|
||||
<tr><td>Geplante Ankündigungen</td><td>vor 20 Sekunden</td><td class="ok">in Ordnung</td></tr>
|
||||
<tr><td>E-Mail-Versand</td><td>vor 20 Sekunden</td><td class="ok">in Ordnung</td></tr>
|
||||
<tr><td>Abgleich mit dem Konto-Verzeichnis</td><td>heute 03:00 Uhr</td><td class="ok">in Ordnung</td></tr>
|
||||
<tr><td>Protokoll aufräumen</td><td>heute 03:05 Uhr</td><td class="ok">in Ordnung</td></tr></table></div>
|
||||
</div></div></div></div></div></body></html>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Ankündigung verfassen (Publisher)</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{text-align:left;color:var(--sub);font-weight:600;padding:6px 8px;border-bottom:1px solid var(--line)}
|
||||
td{padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.ok{color:#107c10}.dark .ok{color:#6ccb5f}
|
||||
.bad{color:#b10e1c;font-weight:600}.dark .bad{color:#ff99a4}
|
||||
.ta{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:8px 10px;font-size:13px;min-height:110px;white-space:pre-wrap;font-family:Consolas,monospace;color:var(--text)}
|
||||
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .two{grid-template-columns:1fr}
|
||||
.radio{display:inline-block;margin-right:14px;font-size:14px}
|
||||
.radio:before{content:"";display:inline-block;width:12px;height:12px;border-radius:50%;border:2px solid var(--fieldline);margin-right:6px;vertical-align:-2px}
|
||||
.radio.sel:before{border-color:var(--brand);background:radial-gradient(var(--brand) 40%,transparent 45%)}
|
||||
.swatch{display:inline-block;width:40px;height:28px;border-radius:4px;border:1px solid var(--fieldline);vertical-align:middle;margin-right:8px}
|
||||
.warnbox{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:10px 14px;font-size:13px;margin-top:10px}
|
||||
.filters span{display:inline-block;border:1px solid var(--fieldline);border-radius:14px;padding:2px 10px;font-size:12px;margin:0 6px 6px 0}
|
||||
.filters span.sel{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
|
||||
.navh{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--sub);padding:12px 12px 4px}
|
||||
.tb{display:flex;gap:2px;border:1px solid var(--fieldline);border-bottom:0;border-radius:4px 4px 0 0;background:var(--nav);padding:4px}
|
||||
.tb span{padding:3px 10px;font-size:13px;border-radius:3px}
|
||||
.wys{border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:0 0 4px 4px;background:var(--field);padding:10px 12px;font-size:14px;min-height:120px}
|
||||
.preview{border:1px dashed var(--fieldline);border-radius:6px;padding:10px 12px;font-size:13px}
|
||||
</style><body><h1 class="cap">Ankündigung verfassen (Publisher)</h1><p class="cap">Desktop light, phone dark. Title, importance (always set, default Information) and a WYSIWYG text editor; optional downtime window, expiry and schedule; recipient count with the reason for each gap. Illustrates: UJ-3, publisher flow. The left menu shows the extra entries for Publishers and Admins.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="nav"><a class=""><i></i>Startseite</a><a class=""><i></i>Profil</a><a class=""><i></i>Profilbild</a><a class=""><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a><div class="navh">Veröffentlichen</div><a class="on"><i></i>Ankündigungen verwalten</a><div class="navh">Administration</div><a class=""><i></i>Darstellung</a><a class=""><i></i>Einstellungen</a><a class=""><i></i>Protokoll</a><a class=""><i></i>Status</a></div><div class="main">
|
||||
<h2>Ankündigung verfassen</h2>
|
||||
<p class="lead">Ihre Ankündigung erscheint auf der Startseite und kann per E-Mail an alle angemeldeten Empfänger gehen.</p>
|
||||
<div class="card"><div class="field"><label>Titel</label><div class="in" style="color:var(--text)">Geplante Wartung am Samstag</div></div>
|
||||
<div class="field"><label>Wichtigkeit</label><span class="radio">Information</span><span class="radio sel">Warnung</span><span class="radio">Ausfall</span>
|
||||
<div class="hint">Bestimmt die Farbe der Meldung auf der Startseite. Ohne Auswahl gilt „Information“.</div></div>
|
||||
<div class="field"><label>Text</label>
|
||||
<div class="tb"><span><b>F</b></span><span><i>K</i></span><span>Überschrift</span><span>Liste</span><span>Link</span></div>
|
||||
<div class="wys">Jellyfin und die <b>Dateifreigabe</b> sind am Samstag von 08:00 bis 12:00 Uhr nicht erreichbar.<ul style="margin:8px 0 0;padding-left:20px"><li>Grund: Umzug auf neue Hardware</li><li>Alles andere läuft normal</li></ul></div>
|
||||
<div class="hint">So sehen es Benutzer auf der Startseite und in der E-Mail.</div></div></div>
|
||||
<div class="card"><h3>Weitere Angaben (optional)</h3>
|
||||
<div class="two"><div class="field"><label>Wartungsfenster: Beginn</label><div class="in">18.10.2026 08:00</div></div><div class="field"><label>Wartungsfenster: Ende</label><div class="in">18.10.2026 12:00</div></div></div>
|
||||
<div class="hint" style="margin:-6px 0 12px">Zeitzone: Europe/Berlin. Die Zeitzone steht auch in der E-Mail.</div>
|
||||
<div class="two"><div class="field"><label>Ablaufdatum</label><div class="in">18.10.2026 13:00</div><div class="hint">Danach verschwindet die Ankündigung von der Startseite.</div></div>
|
||||
<div class="field"><label>Veröffentlichen</label><span class="radio">Sofort</span><span class="radio sel">Geplant</span><div class="in" style="margin-top:6px">17.10.2026 08:00</div></div></div></div>
|
||||
<div class="card"><h3>E-Mail</h3><div class="radio sel">Benutzer per E-Mail benachrichtigen</div>
|
||||
<p class="small" style="margin:6px 0 0">Empfänger: <b>9 von 12 Benutzern</b>. 2 haben E-Mails abgeschaltet, 1 hat keine E-Mail-Adresse hinterlegt.</p></div>
|
||||
<span class="btn">Veröffentlichen</span><span class="btn sec">Als Entwurf speichern</span>
|
||||
</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">EB</span></div><div class="body"><div class="main">
|
||||
<h2>Ankündigung verfassen</h2>
|
||||
<p class="lead">Ihre Ankündigung erscheint auf der Startseite und kann per E-Mail an alle angemeldeten Empfänger gehen.</p>
|
||||
<div class="card"><div class="field"><label>Titel</label><div class="in" style="color:var(--text)">Geplante Wartung am Samstag</div></div>
|
||||
<div class="field"><label>Wichtigkeit</label><span class="radio">Information</span><span class="radio sel">Warnung</span><span class="radio">Ausfall</span>
|
||||
<div class="hint">Bestimmt die Farbe der Meldung auf der Startseite. Ohne Auswahl gilt „Information“.</div></div>
|
||||
<div class="field"><label>Text</label>
|
||||
<div class="tb"><span><b>F</b></span><span><i>K</i></span><span>Überschrift</span><span>Liste</span><span>Link</span></div>
|
||||
<div class="wys">Jellyfin und die <b>Dateifreigabe</b> sind am Samstag von 08:00 bis 12:00 Uhr nicht erreichbar.<ul style="margin:8px 0 0;padding-left:20px"><li>Grund: Umzug auf neue Hardware</li><li>Alles andere läuft normal</li></ul></div>
|
||||
<div class="hint">So sehen es Benutzer auf der Startseite und in der E-Mail.</div></div></div>
|
||||
<div class="card"><h3>Weitere Angaben (optional)</h3>
|
||||
<div class="two"><div class="field"><label>Wartungsfenster: Beginn</label><div class="in">18.10.2026 08:00</div></div><div class="field"><label>Wartungsfenster: Ende</label><div class="in">18.10.2026 12:00</div></div></div>
|
||||
<div class="hint" style="margin:-6px 0 12px">Zeitzone: Europe/Berlin. Die Zeitzone steht auch in der E-Mail.</div>
|
||||
<div class="two"><div class="field"><label>Ablaufdatum</label><div class="in">18.10.2026 13:00</div><div class="hint">Danach verschwindet die Ankündigung von der Startseite.</div></div>
|
||||
<div class="field"><label>Veröffentlichen</label><span class="radio">Sofort</span><span class="radio sel">Geplant</span><div class="in" style="margin-top:6px">17.10.2026 08:00</div></div></div></div>
|
||||
<div class="card"><h3>E-Mail</h3><div class="radio sel">Benutzer per E-Mail benachrichtigen</div>
|
||||
<p class="small" style="margin:6px 0 0">Empfänger: <b>9 von 12 Benutzern</b>. 2 haben E-Mails abgeschaltet, 1 hat keine E-Mail-Adresse hinterlegt.</p></div>
|
||||
<span class="btn">Veröffentlichen</span><span class="btn sec">Als Entwurf speichern</span>
|
||||
</div></div></div></div></div></body></html>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Dashboard (Startseite)</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
</style><body><h1 class="cap">Dashboard (Startseite)</h1><p class="cap">Desktop light with the profile menu open (theme, language, formality). Phone dark: menu collapsed, cards stacked. Illustrates: IA Dashboard, announcement banner (warning), account card including password age.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">LM</span></div><div class="body"><div class="nav"><a class="on"><i></i>Startseite</a><a class=""><i></i>Profil</a><a class=""><i></i>Profilbild</a><a class=""><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a></div><div class="main">
|
||||
<h2>Guten Tag, Lena</h2><p class="lead">Hier sehen Sie aktuelle Hinweise und Ihre Kontodaten auf einen Blick.</p>
|
||||
<div class="banner"><b>Geplante Wartung: Samstag, 18. Oktober, 08:00–12:00 Uhr (Europe/Berlin)</b>
|
||||
Jellyfin und die Dateifreigabe sind in dieser Zeit nicht erreichbar. Alles andere läuft normal.</div>
|
||||
<div class="grid">
|
||||
<div class="card"><h3>Ihr Konto</h3>
|
||||
<div style="display:flex;gap:14px;align-items:center;margin-bottom:10px"><div class="pic">LM</div><div><b>Lena Meier</b><div class="small">Benutzername: lmeier</div></div></div>
|
||||
<div class="row"><span>E-Mail</span><span>lena.meier@example.com</span></div>
|
||||
<div class="row"><span>Telefon</span><span>+49 170 1234567</span></div>
|
||||
<div class="row"><span>Passwort zuletzt geändert</span><span>3. September 2026</span></div>
|
||||
<p style="margin:12px 0 0"><span class="btn sec">Profil bearbeiten</span></p></div>
|
||||
<div class="card"><h3>Letzte Ankündigungen</h3>
|
||||
<div class="row"><span>12. Okt.</span><span>Neue Serie in Jellyfin verfügbar</span></div>
|
||||
<div class="row"><span>29. Sep.</span><span>Server-Umzug abgeschlossen</span></div>
|
||||
<div class="row"><span>14. Sep.</span><span>Kurze Unterbrechung heute Abend</span></div>
|
||||
<p style="margin:12px 0 0"><span class="btn sec">Alle Ankündigungen</span></p></div></div>
|
||||
</div></div><div class="menu"><b>Lena Meier</b><div class="small">lmeier</div><hr style="border:0;border-top:1px solid var(--line)"><div>Design: Hell · Dunkel · <b>System</b></div><div>Sprache: <b>Deutsch</b> · English</div><div>Anrede: <b>Sie</b> · Du</div><hr style="border:0;border-top:1px solid var(--line)"><div>Abmelden</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">LM</span></div><div class="body"><div class="main">
|
||||
<h2>Guten Tag, Lena</h2><p class="lead">Hier sehen Sie aktuelle Hinweise und Ihre Kontodaten auf einen Blick.</p>
|
||||
<div class="banner"><b>Geplante Wartung: Samstag, 18. Oktober, 08:00–12:00 Uhr (Europe/Berlin)</b>
|
||||
Jellyfin und die Dateifreigabe sind in dieser Zeit nicht erreichbar. Alles andere läuft normal.</div>
|
||||
<div class="grid">
|
||||
<div class="card"><h3>Ihr Konto</h3>
|
||||
<div style="display:flex;gap:14px;align-items:center;margin-bottom:10px"><div class="pic">LM</div><div><b>Lena Meier</b><div class="small">Benutzername: lmeier</div></div></div>
|
||||
<div class="row"><span>E-Mail</span><span>lena.meier@example.com</span></div>
|
||||
<div class="row"><span>Telefon</span><span>+49 170 1234567</span></div>
|
||||
<div class="row"><span>Passwort zuletzt geändert</span><span>3. September 2026</span></div>
|
||||
<p style="margin:12px 0 0"><span class="btn sec">Profil bearbeiten</span></p></div>
|
||||
<div class="card"><h3>Letzte Ankündigungen</h3>
|
||||
<div class="row"><span>12. Okt.</span><span>Neue Serie in Jellyfin verfügbar</span></div>
|
||||
<div class="row"><span>29. Sep.</span><span>Server-Umzug abgeschlossen</span></div>
|
||||
<div class="row"><span>14. Sep.</span><span>Kurze Unterbrechung heute Abend</span></div>
|
||||
<p style="margin:12px 0 0"><span class="btn sec">Alle Ankündigungen</span></p></div></div>
|
||||
</div></div></div></div></div></body></html>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Passwort ändern</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
</style><body><h1 class="cap">Passwort ändern</h1><p class="cap">Desktop light, phone dark. Rules shown before typing; effect of the change stated up front; forgotten or expired passwords point to the Admin. Illustrates: password flow (UJ-1). The listed rules are sample text; the real ones must come from the domain policy.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">LM</span></div><div class="body"><div class="nav"><a class=""><i></i>Startseite</a><a class=""><i></i>Profil</a><a class=""><i></i>Profilbild</a><a class="on"><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a></div><div class="main">
|
||||
<h2>Passwort ändern</h2>
|
||||
<p class="lead">Dieses Passwort gilt für alle ping-mee-Dienste. Nach der Änderung werden Sie auf anderen Geräten abgemeldet, und Sie erhalten eine Bestätigung per E-Mail.</p>
|
||||
<div class="card" style="max-width:520px">
|
||||
<div class="field"><label>Aktuelles Passwort</label><div class="in">••••••••••</div><div class="hint">Wir prüfen es, bevor etwas geändert wird.</div></div>
|
||||
<div class="field"><label>Neues Passwort</label><div class="in"> </div>
|
||||
<ul class="rules"><li>Mindestens 12 Zeichen</li><li>Groß- und Kleinbuchstaben, eine Zahl</li><li>Nicht eines Ihrer letzten Passwörter</li></ul></div>
|
||||
<div class="field"><label>Neues Passwort wiederholen</label><div class="in"> </div></div>
|
||||
<span class="btn">Passwort ändern</span><span class="btn sec">Abbrechen</span></div>
|
||||
<p class="small">Passwort vergessen oder abgelaufen? Wenden Sie sich bitte an den Administrator.</p>
|
||||
</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">LM</span></div><div class="body"><div class="main">
|
||||
<h2>Passwort ändern</h2>
|
||||
<p class="lead">Dieses Passwort gilt für alle ping-mee-Dienste. Nach der Änderung werden Sie auf anderen Geräten abgemeldet, und Sie erhalten eine Bestätigung per E-Mail.</p>
|
||||
<div class="card" style="max-width:520px">
|
||||
<div class="field"><label>Aktuelles Passwort</label><div class="in">••••••••••</div><div class="hint">Wir prüfen es, bevor etwas geändert wird.</div></div>
|
||||
<div class="field"><label>Neues Passwort</label><div class="in"> </div>
|
||||
<ul class="rules"><li>Mindestens 12 Zeichen</li><li>Groß- und Kleinbuchstaben, eine Zahl</li><li>Nicht eines Ihrer letzten Passwörter</li></ul></div>
|
||||
<div class="field"><label>Neues Passwort wiederholen</label><div class="in"> </div></div>
|
||||
<span class="btn">Passwort ändern</span><span class="btn sec">Abbrechen</span></div>
|
||||
<p class="small">Passwort vergessen oder abgelaufen? Wenden Sie sich bitte an den Administrator.</p>
|
||||
</div></div></div></div></div></body></html>
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<!doctype html><html lang="de"><meta charset="utf-8"><title>Profilbild - Pending state</title><style>
|
||||
/* Governing spec sections: EXPERIENCE.md Information Architecture, Component Patterns, State Patterns;
|
||||
DESIGN.md Colors, Components. Mock only - the spines win on conflict. Offline: no JS, no network.
|
||||
Brand blue is the Fluent 2 default (#0F6CBD) [ASSUMPTION, verify at scaffold]. German formal (Sie) is the default voice. */
|
||||
:root{--brand:#0F6CBD;--on-brand:#fff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;padding:24px;font-family:"Segoe UI",system-ui,sans-serif;background:#e8e8e8;color:#242424}
|
||||
h1.cap{font-size:14px;font-weight:600;margin:0 0 4px}
|
||||
p.cap{font-size:12px;margin:0 0 16px;color:#555;max-width:900px}
|
||||
.stage{display:flex;gap:32px;align-items:flex-start;flex-wrap:wrap}
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.25);border:1px solid #bbb}
|
||||
.desk{width:980px}.phone{width:360px;border-radius:28px;border:8px solid #222}
|
||||
.light{--bg:#fafafa;--card:#fff;--text:#242424;--sub:#616161;--line:#e0e0e0;--nav:#f5f5f5;--warn-bg:#fff9f5;--warn-line:#f7630c;--chip-bg:#fff4ce;--chip-text:#5c4400;--field:#fff;--fieldline:#8a8a8a}
|
||||
.dark{--bg:#1f1f1f;--card:#292929;--text:#ffffff;--sub:#c8c8c8;--line:#3d3d3d;--nav:#242424;--warn-bg:#3a2a1f;--warn-line:#f7630c;--chip-bg:#4a3b00;--chip-text:#ffe9a8;--field:#333;--fieldline:#9a9a9a;--brand:#479ef5;--on-brand:#0a0a0a}
|
||||
.app{background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:560px}
|
||||
.top{height:48px;background:var(--brand);color:var(--on-brand);display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-weight:600}
|
||||
.dark .top{background:#0f2a43;color:#fff}
|
||||
.av{width:32px;height:32px;border-radius:50%;background:#fff;color:var(--brand);display:grid;place-items:center;font-size:12px;font-weight:700}
|
||||
.dark .av{background:#479ef5;color:#0a0a0a}
|
||||
.body{display:flex;flex:1}
|
||||
.nav{width:220px;background:var(--nav);border-right:1px solid var(--line);padding:12px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:4px;color:var(--text);font-size:14px;margin-bottom:2px}
|
||||
.nav a.on{background:var(--card);font-weight:600;box-shadow:inset 3px 0 0 var(--brand)}
|
||||
.nav i{width:14px;height:14px;border-radius:3px;background:var(--sub);opacity:.6;display:inline-block}
|
||||
.main{flex:1;min-width:0;padding:24px 28px}
|
||||
.phone .main{padding:16px}
|
||||
.phone .row{flex-direction:column;gap:2px}
|
||||
.phone .row span:first-child{width:auto}
|
||||
.row span:last-child{min-width:0;overflow-wrap:anywhere}
|
||||
.phone .card{padding:14px}
|
||||
h2{font-size:24px;font-weight:600;margin:0 0 4px}.phone h2{font-size:20px}
|
||||
.lead{font-size:14px;color:var(--sub);margin:0 0 18px;max-width:640px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:16px 18px;margin-bottom:14px}
|
||||
.card h3{font-size:16px;margin:0 0 8px}
|
||||
.banner{border:1px solid var(--warn-line);background:var(--warn-bg);border-left-width:5px;border-radius:6px;padding:14px 16px;margin-bottom:14px;font-size:14px}
|
||||
.banner b{display:block;margin-bottom:4px}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.phone .grid{grid-template-columns:1fr}
|
||||
.row{display:flex;gap:10px;font-size:14px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.row span:first-child{width:130px;color:var(--sub);flex:none}
|
||||
.pic{width:72px;height:72px;border-radius:50%;background:var(--brand);color:var(--on-brand);display:grid;place-items:center;font-size:26px;font-weight:600;flex:none}
|
||||
.btn{display:inline-block;background:var(--brand);color:var(--on-brand);border-radius:4px;padding:8px 16px;font-size:14px;font-weight:600;margin:0 8px 6px 0}
|
||||
.btn.sec{background:transparent;color:var(--text);border:1px solid var(--fieldline)}
|
||||
.field{margin-bottom:14px}.field label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.field .in{height:34px;border:1px solid var(--fieldline);border-bottom-width:2px;border-radius:4px;background:var(--field);padding:6px 10px;font-size:14px;color:var(--sub)}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:4px}
|
||||
.chip{display:inline-block;background:var(--chip-bg);color:var(--chip-text);border-radius:10px;padding:2px 10px;font-size:12px;font-weight:600}
|
||||
ul.rules{margin:6px 0 0;padding-left:20px;font-size:13px;color:var(--sub)}
|
||||
.small{font-size:13px;color:var(--sub)}
|
||||
.menu{position:absolute;right:16px;top:44px;background:var(--card);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font-size:13px;width:240px;box-shadow:0 6px 20px rgba(0,0,0,.25)}
|
||||
.menu div{padding:4px 0}
|
||||
.rel{position:relative}
|
||||
.burger{display:inline-block;width:18px;height:2px;background:currentColor;box-shadow:0 6px 0 currentColor,0 -6px 0 currentColor}
|
||||
</style><body><h1 class="cap">Profilbild - Pending state</h1><p class="cap">Desktop light, phone dark. Pending chip plus the two-hour note; the current picture stays until the sync has run. Illustrates: picture flow (UJ-2), pending state.</p><div class="stage"><div class="frame desk light"><div class="app rel"><div class="top"><span>ping-mee Portal</span><span class="av">LM</span></div><div class="body"><div class="nav"><a class=""><i></i>Startseite</a><a class=""><i></i>Profil</a><a class="on"><i></i>Profilbild</a><a class=""><i></i>Passwort</a><a class=""><i></i>Ankündigungen</a><a class=""><i></i>Benachrichtigungen</a></div><div class="main">
|
||||
<h2>Profilbild</h2>
|
||||
<p class="lead">Ihr Bild erscheint in den ping-mee-Diensten, zum Beispiel in Jellyfin.</p>
|
||||
<div class="card" style="max-width:560px">
|
||||
<div style="display:flex;gap:16px;align-items:center;margin-bottom:12px"><div class="pic" style="width:96px;height:96px;font-size:34px">LM</div>
|
||||
<div><span class="chip">Wird aktualisiert</span><p class="small" style="margin:8px 0 0">Ihr neues Bild wurde gespeichert. Es erscheint innerhalb von etwa 2 Stunden. Bis dahin sehen Sie hier noch das bisherige Bild.</p></div></div>
|
||||
<span class="btn">Neues Bild auswählen</span><span class="btn sec">Bild entfernen</span>
|
||||
<div class="hint" style="margin-top:10px">Erlaubt sind JPEG, PNG, HEIC und WebP bis 10 MB. Sie können das Bild vor dem Speichern zuschneiden.</div></div>
|
||||
</div></div></div></div><div class="frame phone dark"><div class="app rel"><div class="top"><span class="burger"></span><span>ping-mee Portal</span><span class="av">LM</span></div><div class="body"><div class="main">
|
||||
<h2>Profilbild</h2>
|
||||
<p class="lead">Ihr Bild erscheint in den ping-mee-Diensten, zum Beispiel in Jellyfin.</p>
|
||||
<div class="card" style="max-width:560px">
|
||||
<div style="display:flex;gap:16px;align-items:center;margin-bottom:12px"><div class="pic" style="width:96px;height:96px;font-size:34px">LM</div>
|
||||
<div><span class="chip">Wird aktualisiert</span><p class="small" style="margin:8px 0 0">Ihr neues Bild wurde gespeichert. Es erscheint innerhalb von etwa 2 Stunden. Bis dahin sehen Sie hier noch das bisherige Bild.</p></div></div>
|
||||
<span class="btn">Neues Bild auswählen</span><span class="btn sec">Bild entfernen</span>
|
||||
<div class="hint" style="margin-top:10px">Erlaubt sind JPEG, PNG, HEIC und WebP bis 10 MB. Sie können das Bild vor dem Speichern zuschneiden.</div></div>
|
||||
</div></div></div></div></div></body></html>
|
||||
Reference in New Issue
Block a user