# Mentat — shift planning application (product spec) > **Living document.** This spec captures intent and direction. It is **not** fixed: details will change as we learn, prototype, and get feedback. When we disagree with something here, we update this file and move on. --- ## 1. Purpose A web application for upcoming planning periods (often a full calendar month): collect shift **wishes**, then **produce an actual shift plan** for that period — replacing (or assisting) today’s fully **manual** scheduling after wishes are in. Team members submit wishes in free text, optionally with **weights**. Admins define **when** wishes are due, review submissions, and drive the planning pipeline. The system makes deadlines visible, reminds people before they pass, and aims to turn structured constraints into a **published shift plan** contributors can rely on. The detailed mechanics of “wish → plan” are expected to evolve (see §6.7); the intent is **automation with sensible human checkpoints**, not only a wish inbox. --- ## 2. Goals - Clear visibility of **when** wishes must be submitted for each planning month. - **Fair** use of weights so no one can dominate the pool without checks (exact rules TBD). - **Low friction** for contributors: plain-text wishes, simple weight selection. - **Configurable** deadlines and notices (not hard-coded “one month ahead”). - **Email notifications** when a submission window is open or nearing its end. - **Automated shift planning** for the target month: ingest wishes and business rules, run a **constraint-based** solver, and output a **shift plan** (with review before it is treated as final). - **Reduce manual work** compared to today’s process, while keeping admins able to override or fix edge cases. - Reflect **real scheduling constraints**: continuous coverage, **long shift blocks**, standing per-person rules, workload and **vacation** fairness — without hard-coding a specific industry label in the product (see §3). - **German-first:** user-facing UI, emails, and guidance should be in **German**; wish input and system feedback must work naturally in German. --- ## 3. Domain context — how our planning works today > **Why this section exists.** The product does **not** need to be branded or specialized around any medical or personal situation in code. Treat this as **requirements flavor**: continuous home coverage with a small assistant team. The solver and data model should stay **general** (coverage intervals, people, constraints) even though the scenario below motivates features. ### 3.1 Setting - Scheduling **personal assistants** who provide **24/7 presence** at a fixed location — someone must be responsible at all times. - Planning usually rolls up to a **calendar month** (e.g. April has **30 days** of coverage). Total **person-days** or **hours** on duty across the month come from how **blocks** tile the timeline (there are not “extra” calendar days beyond that month). ### 3.2 Shift blocks (long contiguous stretches) - We often assign **long blocks**, not classic short clinic-style shifts: - Typical: **24 h** or **48 h** contiguous shifts. - Sometimes **72 h** blocks when people prefer fewer handovers. - The engine should treat **multi-hour / multi-day contiguous assignments** as first-class, including **handovers** between blocks (handover **clock time** is a strong, configurable rule — see §3.8). ### 3.3 Standing preferences vs one-off wishes - Assistants may want **repeatable rules** that apply **every period** unless edited — for example: - Prefer **48 h or 72 h** blocks vs prefer **single-day** segments. - Minimum **rest between own assignments**: e.g. at least **two calendar days** or **48 hours** between shifts. - Each standing preference **must use the same weight scale** as monthly wishes (§6.3): contributors assign importance explicitly rather than maintaining two parallel systems (what reads as “almost hard” in practice is simply a **high weight**, unless later modeled as a separate truly-hard rule — **TBD**). - Other preferences stay **period-specific** via the monthly wish flow. - The product keeps **profile-level standing preferences** distinct from **period-specific wishes** in storage and UX, but **not** by using different weight vocabularies — combined fairness checks apply across **both** where relevant (exact aggregation **TBD**). ### 3.4 When wishes are “too much” — helping employees - **Same person, clashing wishes:** two (or more) wishes from **one** employee can **contradict each other** once interpreted (or even structurally, if we add typed checks). This should be **called out explicitly** and checked **as early as possible** — see §6.2 and §6.10. It is a distinct case from “my wishes vs someone else’s” or “wishes vs global coverage.” - Combinations may also be **impossible together** across people or vs coverage (sketch: “avoid too many consecutive weeks on” **and** “still work at least N days” — may clash with teammates’ rules or staffing). - We want **feedback**: what conflicts, which asks were relaxed, **plain-language** hints — exact UX **not decided** (wizard vs conflict list vs simulate). - When the system flags an issue, contributors should be able to attach **plain-text clarification** (§6.2) so admins / later interpretation steps understand intent without blocking save — unless we choose “cannot finalize until resolved” for specific conflict types (**TBD**). ### 3.5 Admin-configured fairness: workload and vacation - Admins configure each employee’s **normal expected workload** for a period — e.g. target **share of duty** or target **days / shifts on** per month (depends on headcount and agreements). - **Vacation** is modeled explicitly when relevant: - Periods may include **vacation days** that reduce availability or enter fairness accounting — configurable. - **Practice we use:** if **nobody** asks for vacation, we may still **assign vacation days** so everyone lands near **agreed “normal” load**, while we **cover exactly** the calendar days of the month (e.g. 30 days in April). Policy belongs in **admin configuration + solver objectives**, not hard-coded literals. ### 3.6 Coverage invariant vs exceptions - **Default:** continuous **on-site / on-call coverage** for every instant in the planned horizon (subject to how we discretize time). - **Exceptions:** the **care recipient** (or admin on their behalf) marks intervals where **physical assistance is not required** — away, cared for elsewhere, etc. During those intervals, **nobody needs to be present** for coverage. **Compensation / paid booking** for those same intervals may still follow agreed rules — see §3.9 (separate from who is “at work” for coverage). ### 3.7 Admin visibility after planning - After planning completes, **admins** benefit from **email** (or in-app digest) summarizing **vacation / off-duty** outcomes **per employee**, especially when vacation was requested or **auto-balanced**. - When **paid booking** for **non-coverage** intervals is modeled (§3.9), the same or a related summary can list **who received paid credit** for those blocks (for payroll reconciliation). ### 3.8 Handover time-of-day — near-inviolable - Block boundaries **change over** at a fixed **clock time** most of the time (today: **2:30 PM** / **14:30** local). This is the **default**; it is **admin-configurable** per org / per planning period (exact scope **TBD**). - **Exceptions:** specific **calendar dates** (or ranges — **TBD**) may use a **different** handover time than the default — e.g. an appointment, travel, or one-off agreement. The product must make this **obvious to admins**: at any time they can see **which days** differ from the default and **what** time applies (e.g. month view, list, or inline on the plan — **UX TBD**). Same **no-overlap** rule applies at whatever instant is configured for that day. - **No overlapping on-duty coverage at handover:** at that instant, one person’s block **ends** and the next person’s block **starts** — **one** person is responsible, not two. We do **not** model a **double-staffed** or **overlap** “handover window” (no two people both on shift for the same slot at 2:30). *If* a future policy required **intentional** overlap, that would be an **explicit** add-on, not the default. - In practice this rule is **almost never broken**; in the solver it should be modeled as **very high weight** or **near-hard** (break only if no feasible plan exists otherwise). - Edge cases (midnight-spanning blocks, DST, travel) — **open**; base case is **one handover time per calendar day** (default or override for that date). ### 3.9 Paid booking when assistance is not required - Even when **no presence** is needed, local agreement may still **allocate the interval for payroll**: who is “booked” and **paid** for that time. - Typical pattern: split that **paid, non-coverage** time **in proportion** to each assistant’s **normal expected share** of the month (sketch: if **A** targets **10** “duty days” in the month and **B** targets **5**, a **shared** non-coverage block might allocate **paid booking** in a **2∶1** ratio — exact formula and units are **admin/policy**). - **Planning distinction:** receiving **paid credit** for an interval does **not** mean the person must be **on site** then. They should **remain assignable** for real **coverage** shifts elsewhere in the month; the **booking** is a **compensation / entitlement** artifact, not a **physical availability** lock for coverage (unless we later model optional “cannot be booked twice” payroll rules). ### 3.10 Employee lifecycle - Each user may carry **contract start / end dates** (nullable if open-ended). - The solver **excludes** a user from coverage for any instant outside their contract window. - Pending wishes from deactivated (or future-hired) users are **preserved** so nothing is silently lost; admins see them flagged in the wish overview and decide what to do. - Standing preferences (§6.8) remain with the user profile regardless of contract dates; they only matter for periods that overlap the active window. --- ## 4. Example workflow (real-world anchor) We might plan **June** while still in **April**. For example: wishes for June must be submitted by **25 April**. The app should allow admins to set **per-period deadlines** independently of “how many months ahead” — the relationship between “planning month” and “deadline date” is configurable. --- ## 5. Roles | Role | Intent | |------------|--------| | **Contributor** | Logs in, sees relevant deadlines, submits or edits wishes within the open window, picks weights according to policy. | | **Admin** | Configures planning periods and deadlines, sees overview of submissions, runs or supervises the **planning pipeline** (wish interpretation → constraints → solved plan), reviews/publishes the shift plan, adjusts rules/settings as needed. | (Exact permission split — one admin vs many, super-admin, etc. — can evolve.) --- ## 6. Functional requirements (draft) ### 6.1 Authentication - **Email + Passwort** (Argon2id-Hashing in der Datenbank; mindestens 8 Zeichen). - **Sitzungen:** zufälliges Token im **httpOnly**-Cookie; in Postgres wird nur ein **SHA-256-Hash** des Tokens gespeichert (`sessions` mit Ablaufzeit). - **Selbstregistrierung (`ALLOW_REGISTRATION`):** neue Konten sind **Mitwirkende**; nach Registrierung gilt: **E-Mail-Verifizierung per Link**, danach **Freischaltung durch Admin**. Ohne beides kein Zugriff auf die App (klare deutschsprachige Hinweise beim Login). - **Tokens:** Einmal-Links für E-Mail-Verifizierung und Passwort-Reset liegen als **SHA-256-Hashes** in `user_tokens` mit Ablaufzeit. - **Rollen:** **Admin** und **Mitwirkende** (`users.role`); Admins pflegen Nutzer über **`/admin/users`** (Freigabe / Ablehnen); **Löschen** nur solange noch keine Admin-Entscheidung gefallen ist (`approved_at`/`rejected_at` siehe Migration). - **Passwort:** **Ändern** bei eingeloggten Nutzer:innen (`/account/password`); **Passwort vergessen** per E-Mail-Link (`/forgot-password`, `/reset-password`). - **E-Mail (transaktional):** Verifizierung, Reset, Benachrichtigung an alle Admins nach erfolgreicher E-Mail-Bestätigung — SMTP über **`/.env.example`** (`SMTP_*`, `MAIL_FROM`, optional `PUBLIC_APP_URL` für stabile Links). Für Betrieb auf **Uberspace:** dedizierte Mailbox (`uberspace mail user add …`), SMTP-Host **`*.uberspace.de`**, Port **587** oder **465** — siehe **`README.md`** (Outbound mail). - Umgebungsvariablen siehe **`/.env.example`** (`DATABASE_URL`, `SESSION_SECRET`, `ALLOW_REGISTRATION`, optional `BOOTSTRAP_ADMIN_*`). - Für Betrieb auf **Uberspace**: Postgres **einmal** nach UberLab einrichten (siehe **`README.md`**), dann **`npm run migrate`**, dann Start der App. ### 6.2 Contributor experience - **Dashboard / home**: upcoming planning period(s), **deadline date(s)** for submitting wishes, and clear status (open / closed / not yet open). - **Wish submission** for a given period: - **Plain text (German)** as **multiple wish entries** per planning period (not one blob). - **Weight per wish entry** (see §6.3): - **Typ**: **Unverhandelbar** vs **Wunsch** - If **Wunsch**: **Wichtigkeit** has **4 levels** (Leicht / Normal / Wichtig / Sehr wichtig) - **Validation timing** - **Self-consistency / same-employee clashes** (§3.4): whenever we can judge without the full multi-person solver — including **two wishes from the same person that rule each other out** — run checks **immediately** on save/edit (target: **instant** UI feedback). These checks should be **cheap** structurally or over **interpreted** constraints once interpretation exists. - **Heavier checks** (full feasibility against teammates + coverage): if performance ever matters, **still persist** the submission first, then run analysis **asynchronously** and surface results on the page when ready (sketch timing: order of **seconds**, e.g. ~**15 s**, not batch-only overnight — exact SLA **TBD**). - When a wish is flagged (self-clash, ambiguous parse), the contributor resolves it by **editing the wish text or its structured form directly** — no separate clarification field is stored. The wish itself is always editable while the submission window is open, so any flagged wish is fixable in place. - Ability to **update** wishes while the window is open (unless we later lock edits). - After a plan exists for a period: **view the published shift plan** (and history of changes if we add versioning — TBD). ### 6.3 Weight model and fairness - Contributors assign weights from a **defined set** (no free-form numbers). - **Two-axis model (per wish entry):** - **`Typ`**: **Unverhandelbar** (hard-ish) vs **Wunsch** (soft) - If **Wunsch**: **Wichtigkeit** is one of **4 levels**: **Leicht**, **Normal**, **Wichtig**, **Sehr wichtig** - **Shared palette:** the same weight vocabulary is used for **monthly wishes** and **standing profile preferences** (§3.3, §6.8) so importance is comparable; we do not maintain a second weight system for “permanent” rules. - **Fairness guardrails (initial):** - We introduce a **cap** on how many **Unverhandelbar** wishes a contributor should submit per period; in early UX this is a **strong warning**, not a save-block. - Aggregation/normalization across contributors (and across standing + period wishes) remains **TBD** and will be refined as we prototype solver objectives. ### 6.4 Deadlines and planning periods - Each **planning period** (typically aligned to a month, e.g. “June 2026”) has: - A **submission opens at** (optional) and **submission due by**. - **Current implementation:** both are **date-only** (no time-of-day stored). - **Semantics:** “open” starts at the beginning of that date, and “due” ends at the end of that date, in the **organization timezone** (timezone choice still open). - Admins set these; **no assumption** that the deadline is “one month before” the planned month — only that it must be **configurable**. ### 6.5 Notifications - **Language:** emails are sent in **German** (templates and copy German-first). - **Email** when: - A submission window **opens** (optional). - A deadline is **approaching** (e.g. configurable “remind X days before”). - Optionally when a window **closes** (summary or reminder). - After a **plan is finalized** (or published): optional email to **admin(s)** summarizing **vacation / off-duty** and, when applicable, **paid non-coverage booking** per employee (see §3.7). - Delivery mechanism (queue, provider, templates) **TBD**. ### 6.6 Admin interface - Create/edit **planning periods** and their **deadlines**. - See **who has submitted** / not submitted (privacy level TBD). - Configure **notification** rules (lead times, channels at minimum email). - Configure **weight** options and **fairness** rules as we define them. - Configure **expected workload** per employee per period (targets that drive fairness / vacation balancing — **exact metrics TBD**, see §3.5). - Configure **vacation**: capacity, voluntary requests vs **system-assigned** vacation days when needed to hit fairness targets while still covering the month. - Configure **principal / location availability** (optional): intervals where **physical assistance is not required** (see §3.6). - Configure **default handover time-of-day** (example **14:30**) and optional **per-date overrides** when a day needs a **different** handover (see §3.8). - **See at a glance** which days in the planning horizon use a **non-default** handover (and the exact time), so scheduling decisions are never opaque — including while editing overrides and when reviewing the draft or published plan. - Configure **paid booking rules** for **non-coverage** intervals: how entitlement is split across assistants (e.g. proportional to monthly targets — §3.9), independent of **who is on duty** for **coverage** elsewhere. - Trigger or schedule **plan generation** after wishes are collected (or on demand); **review** machine-produced constraints and the **solved plan** before locking it. - Handle **infeasible** solutions (no assignment satisfies all hard constraints): surfacing conflicts, relaxing rules, or manual edit paths — **workflow TBD**. - Future: export, reporting — optional early; add when needed. **Note (current implementation):** the admin wish overview lives at **`/admin/wishes?periodId=...`**. If `periodId` is missing/invalid, the UI defaults to the most recent planning period by `(year, month)`. ### 6.7 From wishes to shift plan (intended pipeline, not final) Today, building the plan **manually** from the same wishes is the baseline. The product direction is to automate that path, possibly in **two broad steps** (exact shape to be validated in prototypes): 1. **Interpretation — natural language → structured rules** Free-text wishes (and weights) are turned into a **normalized representation**: typed constraints or preferences (e.g. “no night shift before date X”, “prefer weekends off”) using **lightweight AI assistance** — simple models or APIs, with **human review** so garbage or ambiguous text does not directly drive the solver without a checkpoint. 2. **Solve — constraints → shift plan** Those rules are combined with **organizational hard constraints** (legal limits, staffing minima, role coverage, etc. — mostly **not** inferred from prose; supplied as data/config). A **constraint solver** (or similar optimization backend) searches for an assignment that satisfies hard constraints and optimizes or ranks soft preferences (including wish weights). **Outputs:** a concrete **shift plan** for the planning month (who works when), plus optional diagnostics (which soft wishes were dropped or downgraded, conflicts, etc.). This section is deliberately **experimental**: we may merge steps, add a manual “rule editor” without AI first, or swap solver technology after spikes. ### 6.8 Employee profiles — standing constraints - Assistants can maintain **long-lived** preferences or limits (rest between own shifts, preferred block lengths such as 24 / 48 / 72 h vs single-day segments, etc.). - Each item carries a **weight** from the **same palette as monthly wishes** (§6.3); users choose how strongly each standing rule should count relative to one-off wishes for a given month. - These feed the solver as **weighted preferences** (and, where we introduce truly **hard** organizational rules, those remain separate from this scale — **TBD**) alongside monthly wishes (precedence rules TBD). ### 6.9 Coverage model - Default: **continuous on-site / on-call coverage** across the planned month at the configured granularity (see §3.2). - **Exceptions** only where explicitly modeled (principal unavailable — §3.6): **presence** not required for those intervals. - **Separate ledger (when enabled):** **paid booking** / compensation splits for non-coverage intervals per §3.9 — must **not** incorrectly mark people as physically **busy** for coverage during those slots; they stay **eligible** for real **duty** assignments on the rest of the timeline unless a different rule applies. - **Handover alignment:** shifts should **tile edge-to-edge** at the **effective** handover clock for **each calendar day** — default or **date-specific override** (§3.8): **no overlap** between assignees at that boundary unless we later introduce overlap as an explicit policy — unless the solver reports an unavoidable violation. ### 6.10 Feasibility feedback for contributors - **Within one employee:** detect and explain **internal** clashes first (same contributor, conflicting wishes — §3.4); this should feel **immediate** where technically possible (§6.2). - **Across the team / vs coverage:** when checks are heavier, prefer **save-then-compute** with **in-page** status (queued / running / done) rather than silent failure; message when e.g. “your wishes can’t all be satisfied together” or “they don’t match what you asked for earlier in this form.” - Provide **actionable** copy: what conflicts, what might give — format **TBD** (see §3.4). - **No separate clarification field.** When automated messages flag a wish, the resolution path is **editing the wish (text or structured form) directly** — see amended §6.2. ### 6.11 Plan consumption via Google Calendar - After admin review of the draft plan, **publish** writes events to an **org-configured Google Calendar**. - Each event stores a stable **`externalEventId`** so re-publish is **idempotent** (updates / removes the same events, never duplicates them). - **Unpublish** removes the events from the Google Calendar. - The in-app calendar view is an acceptable v1 read-only surface for contributors; the real distribution channel is Google Calendar. - **ICS export** is an optional fallback for contributors outside the Google ecosystem — added later if needed. - **Auth model** (OAuth authorized by an admin account vs Google service account with per-calendar sharing) is **TBD** — decided in the Epic 11 implementation (see §9). ### 6.12 Audit log & plan versioning - Append-only rows for **plan publish events**, **admin overrides** of solver output, and **user-approval decisions** (approve / reject registration). - v1 has **no dedicated UI** — admins can query via DB or a minimal admin page stub. - Plan versioning is bounded: at minimum the **published** plan and the **current draft** are distinguishable; richer version history and diffing is deferred to Epic 12. ### 6.13 Weight → solver mapping (placeholder) - Initial proposal, expected to evolve during solver prototyping: - **`Unverhandelbar`** → **hard** constraint. - **`Wunsch`** / **Leicht** → weight **1**; **Normal** → **3**; **Wichtig** → **7**; **Sehr wichtig** → **15**. - The mapping is stored in **one place** (config) so calibration does not require UI or schema churn. - Calibration method (e.g. solve historical months with different weights and compare to the admin's manual plan) is **TBD** — see §9. ### 6.14 Generic rule schema - Wishes and standing preferences compile to a small, **extensible tagged-union expression language** — see [`docs/rule-schema.md`](docs/rule-schema.md) for the sketch grammar. - Two families: - **Time-set rules** (`avoid`, `prefer`, `require`, `forbid`) over a recursive `TimeSetExpr` (specific date with optional time-of-day, weekday, date range, union, intersection, complement, …). - **Shape preferences** (`preferLengthHours`, `minRestHours`, `maxConsecutiveBlocks`, …) tied to no particular calendar slot. - Each rule carries a weight from the shipped palette (§6.3) and an optional free-text `note`. - **Raw free text and structured form are stored side by side**; neither is lost when the other is produced. - **New kinds of requests are added by introducing new variants**, not by widening existing fields. This keeps the parser, validator, renderer, and solver contract stable. - Every rule variant ships with a **renderer** that produces either concrete calendar slots (time-set) or a German sentence (shape). This is an invariant — see §6.15. ### 6.15 Rule rendering for contributors - Contributors **never see raw structured JSON**. The editor and the wish list show either: 1. **Highlighted slots** on the planning period's calendar for time-set rules, or 2. A **German sentence summary** for shape preferences. - This is a product invariant, not a UI detail: every new rule variant must come with a visual or sentence-level rendering before it can ship. - Admin-facing views may additionally expose the structured form for debugging, but never as the **primary** affordance for contributors. --- ## 7. Non-goals (for now) - **End-to-end unmanned planning:** no requirement that the first release produces a perfect plan with zero admin touch — **human review** of interpreted rules and/or the draft plan is acceptable (and likely). - **Payroll / HR system integration** — not assumed for an early version. - **Guarantee** of feasibility: some wish sets may be unsatisfiable together; the product should **surface** that rather than silently wrong plans. - Mobile-native apps — web-first; responsive layout is enough initially. - **Swap / on-call / sick-leave handling in v1** — post-publish lifecycle is deferred (see Epic 12 in [`docs/epics.md`](docs/epics.md)). - **Multi-tenancy.** The app runs for **one** organization; no tenant isolation work is planned. - **Contributor-side editing of structured rules as raw data.** Contributors always interact via calendar / sentence renderings (§6.15); raw structured JSON is an internal artifact. - **Holidays (Feiertage) as a first-class concept.** Contributors express holiday availability through the normal wish flow. --- ## 8. Technical direction (non-binding) - **Modern** web frontend (framework TBD). - **Backend** with a **database** for users, periods, wishes, weights, **interpreted constraints**, solver configuration, and **resulting shift plans**. - API between frontend and backend; deployment and hosting **TBD**. - **Language / i18n:** user-facing UI and email copy are **German-only** (see §9). Still keep user-facing strings centralized so copy can evolve cleanly. - **Testing:** maintain **unit tests** for important logic; ensure contributors can run the project **locally** for QA (see §6.2, and operational notes in §9). - **AI layer (optional / staged):** small, well-bounded NL → structured rules; **auditability** and admin override are more important than fully automatic trust. - **Validation path:** lightweight **per-user** checks (especially **same-person wish clashes**) inline or in fast endpoints; heavier **multi-person feasibility** optionally via **async jobs** with **polling / SSE / websocket** so the UI updates within **seconds** after save (§6.2). - **Solver layer:** constraint programming, MIP/ILP, or another appropriate engine — **tooling TBD** (e.g. OR-Tools, MiniZinc, custom heuristics). Must accommodate **long contiguous blocks** (24–72 h or arbitrary lengths), **handovers** at an **effective per-day** clock time (**default** plus optional **date overrides** — §3.8), **single** worker covering each instant (**no default overlap** between adjacent shifts), rest gaps, optional **coverage gaps** when principal is unavailable, **fairness / vacation** objectives, and optionally a **second accounting dimension** (paid **booking** of non-coverage time **without** consuming coverage availability). May run **asynchronously** for larger instances. - Clear **separation** between (a) wish text and weights, (b) normalized machine- and human-edited rules, and (c) solver **input/output** artifacts for debugging and fairness. - **Observability.** Structured JSON logs; error tracking (Sentry or equivalent, vendor TBD — see §9); minimum metrics: request latency, email delivery outcome, solver runtime, Google Calendar publish outcome. - **Rate limiting.** Per-IP **and** per-account limits on `/login`, `/register`, `/forgot-password`, `/reset-password` to protect the existing auth lifecycle (§6.1). - **Accessibility.** Target **WCAG 2.1 AA**: keyboard navigable, visible focus, form labels and error messages in German (via `apps/web/src/strings/de.ts`). - **Testing.** Unit + integration tests for important logic; later, **Playwright** E2E for auth, wish submission, and the rule editor. Run **locally** via `npm run` scripts — **no CI gating** for now (see §9 for when to revisit). - **Backups / DR.** Nightly Postgres dump on Uberspace; a restore drill is documented in [`docs/ops-runbook.md`](docs/ops-runbook.md) and must be exercised at least once before relying on the app in production. - **Google Calendar integration.** OAuth or service-account credentials stored **outside the repo**. The publish layer is **idempotent** and records an `externalEventId` per event so re-publish updates or removes events instead of duplicating them. Auth model decided in Epic 11; see §9. This section exists to align expectations; we may swap stacks if constraints change. --- ## 9. Open questions (to resolve while building) - Identity: **Open signup nur wenn `ALLOW_REGISTRATION` aktiv** — sonst Einladung/Bootstrap (z. B. `BOOTSTRAP_ADMIN_*`). Magic-Link / SSO später möglich. - Timezones: single org timezone vs per-user; and whether deadlines should remain **date-only** or move to full **datetime** later. - **Language:** **German-only** for the user-facing product; use **“du”** tone in UI + emails. Decide whether to store both raw wish text and a normalized interpretation artifact. - One wish list per period vs multiple labeled wishes; max text length. - Exact **weight** schema and **fairness** algorithm. - Email provider and bounce/unsubscribe handling. - Whether contributors see anonymized **aggregate** weight usage from others (for transparency). - **Rule schema:** what structured form do wishes compile to; how much is **fixed** vs free-form tags. - **AI:** which services/models, cost/latency, and how admins **edit** interpreted rules before solving. - **Solver:** technology choice; modeling of shifts (granularity); objective function (maximize satisfied weighted wishes vs minimize violations). - **Phasing:** ship **wish collection + manual plan** first vs require a **thin** solver MVP in v1. - How to quantify **“normal” load** (person-days, hours, shifts count) and tie it to **vacation assignment** when nobody volunteers. - **Handovers:** **default time** vs **per-date overrides** (how many exceptions per month, ranges vs singles); **DST** behavior; confirm **zero overlap** modeling everywhere time ranges meet; optional future **explicit overlap minutes** only if policy demands it; when near-hard handover is **impossible**, escalation UX (relax vs admin edit). - **Paid booking vs duty:** whether payroll requires storing **hours booked**, **days credited**, or exports only — and legal/tax — **outside** core scheduling unless we scope reporting in. - **Self-clash detection** on **free text** only after NL interpretation: do we block **save** until interpret, or always allow save with “unverified until …” state? - **Sync vs async** threshold: what runs in the request vs a **job** (target **~seconds** for user-visible follow-up checks). - **Local QA & seeding:** dev-only ability to create a small demo dataset (admin + a few employees + one planning period) to validate features locally. Must be **disabled in production**. - **Staging/testing environment:** ability to deploy a separate **test team / staging** so you can click around without touching the live data once the app is in real use. - **Backwards compatibility:** once in use, schema and behavior changes should be **backwards compatible** (or use explicit migrations + staged rollouts) so updates don’t break existing data. - **DSGVO posture:** retention, export on request, deletion on account removal, admin-only visibility of wishes — short policy in [`docs/privacy.md`](docs/privacy.md); concrete data-export / deletion endpoints still to be specified. - **Google Calendar auth model:** **OAuth** (admin-authorized account, refresh-token storage) vs **service account** (JSON key, per-calendar sharing). Decided in Epic 11; rotation cadence documented in [`docs/ops-runbook.md`](docs/ops-runbook.md) afterwards. - **Observability / error-tracking vendor:** Sentry vs self-hosted (GlitchTip, …) vs plain structured logs + alerting — TBD alongside the Epic Ops slice. - **Weight → solver calibration:** how to pick the concrete numbers in §6.13 (e.g. replay historical months and compare to the admin's manual plan) — settled during Epic 9. - **Remote repo hosting + CI:** explicitly **deferred**. Codebase is local and deploys via the Uberspace script. Revisit only when collaborators or a staging host are added. --- ## 10. Revision history | Date | Change | |-----------|--------| | 2026-04-19 | Initial draft (wishes, deadlines, weights, admin, email). | | 2026-04-19 | Added shift **plan generation** goal; **NL → rules → constraint solver** pipeline; updated non-goals and technical direction. | | 2026-04-19 | Added **§3 domain context**: 24×7 PA-style coverage, **24–72 h** blocks, standing rules, workload/vacation fairness, principal-unavailability gaps, admin **post-plan vacation** email; functional §6.5–6.10 and cross-refs. | | 2026-04-19 | Added **§3.8** configurable **handover time** (e.g. 14:30, near-hard), **§3.9** **paid booking** during non-coverage (proportional split, separate from on-site availability); admin + technical + open questions updated. | | 2026-04-19 | Standing preferences use the **same weights** as monthly wishes (**§3.3**, **§6.3**, **§6.8**). | | 2026-04-19 | **Same-employee wish clashes**: explicit checks (**§3.4**, **§6.2**, **§6.10**); instant where cheap, async save-then-verify with in-page feedback (e.g. ~15 s sketch); **plain-text clarification** when flagged; **§8** validation path. | | 2026-04-19 | **Handover exceptions:** per-date overrides vs default; **admins** can **see which days** differ (**§3.8**, **§6.6**, **§6.9**); **§8** solver note. | | 2026-04-19 | Clarified **instant handover**, **no overlapping shifts** at boundaries (single coverage at 14:30 unless overlap is an explicit future policy). | | 2026-04-19 | Added **German-first** requirement for UI + emails (**§2**, **§6.2**, **§6.5**, **§8**, **§9**). | | 2026-04-19 | Locked **German-only** + **“du”** tone; added testing/QA, staging environment, and backwards-compatibility requirements (**§8**, **§9**). | | 2026-04-19 | Epic 1 **Authentication**: E-Mail/Passwort, DB-Sessions (Cookie + Hash), Rollen admin/contributor (**§6.1**, **§9** Identity). | | 2026-04-19 | Epic 1 erweitert: E-Mail-Verifizierung, Admin-Freigabe, Admin-Mail bei Verifizierung, SMTP/`user_tokens`, Passwort ändern & vergessen (**§6.1**). | | 2026-04-22 | Amended **§6.2** and **§6.10** to drop the separate **plain-text clarification** field; flagged wishes are resolved by editing the wish directly. Added **§3.10** employee lifecycle, **§6.11** Google-Calendar publish flow, **§6.12** audit log & plan versioning, **§6.13** weight→solver mapping placeholder, **§6.14** generic expression-based rule schema, **§6.15** rule-rendering invariant for contributors. Extended **§7** (non-goals: swap / on-call, multi-tenancy, raw-JSON editing, holidays), **§8** (observability, rate limiting, accessibility, local testing, backups, Google Calendar), and **§9** (DSGVO, calendar auth model, tracking vendor, weight calibration, CI explicitly deferred). | --- *End of spec — update §10 when meaningful decisions land.*