@smartkomm/plexus-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +863 -2
  2. package/dist/cli.mjs +5432 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +224 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/agents/use-shared-tools.md +52 -0
  7. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  8. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  9. package/dist/skill/references/audit/answer-access-request.md +115 -0
  10. package/dist/skill/references/config/attach-a-template.md +117 -0
  11. package/dist/skill/references/config/init-tenant.md +121 -0
  12. package/dist/skill/references/config/onboard-tenant.md +71 -0
  13. package/dist/skill/references/config/rename-program-stage.md +99 -0
  14. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  15. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  16. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  17. package/dist/skill/references/config/update-tenant-config.md +174 -0
  18. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  19. package/dist/skill/references/export/create-named-export.md +47 -0
  20. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  21. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  22. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  23. package/dist/skill/references/finance/record-submission-date.md +33 -0
  24. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  25. package/dist/skill/references/marketing/demo-video-links.md +101 -0
  26. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  27. package/dist/skill/references/notifications/mail-level.md +90 -0
  28. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  29. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  30. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  31. package/dist/skill/references/offer/addresses.md +129 -0
  32. package/dist/skill/references/offer/appointment-task.md +79 -0
  33. package/dist/skill/references/offer/assignment-rule.md +195 -0
  34. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  35. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  36. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  37. package/dist/skill/references/offer/author-smartform.md +102 -0
  38. package/dist/skill/references/offer/bind-node-input.md +76 -0
  39. package/dist/skill/references/offer/completeness-check.md +137 -0
  40. package/dist/skill/references/offer/countersign-node.md +91 -0
  41. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  42. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  43. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  44. package/dist/skill/references/offer/edit-smartform.md +121 -0
  45. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  46. package/dist/skill/references/offer/engage-push-node.md +147 -0
  47. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  48. package/dist/skill/references/offer/node-tasks.md +162 -0
  49. package/dist/skill/references/offer/optional-assessment.md +65 -0
  50. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  51. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  52. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  53. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  54. package/dist/skill/references/offer/task-family-request.md +69 -0
  55. package/dist/skill/references/offer/task-result-files.md +52 -0
  56. package/dist/skill/references/offer/template-document-node.md +113 -0
  57. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  58. package/dist/skill/references/resolver/manage-contract-resolver.md +161 -0
  59. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  60. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  61. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  62. package/dist/skill/references/signing/README.md +113 -0
  63. package/dist/skill/references/signing/connect-a-school.md +90 -0
  64. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  65. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  66. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  67. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  68. package/dist/skill/references/test-actions/drive-a-test-application.md +69 -0
  69. package/dist/skill/references/users/add-platform-admin.md +59 -0
  70. package/dist/skill/references/users/add-platform-user.md +59 -0
  71. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  72. package/dist/skill/references/users/change-login-email.md +55 -0
  73. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  74. package/dist/skill/references/users/delete-permission-group.md +76 -0
  75. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  76. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  77. package/dist/skill/references/users/set-password.md +57 -0
  78. package/dist/skill/references/users/use-test-account.md +79 -0
  79. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  80. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  81. package/dist/spec.snapshot.json +156821 -0
  82. package/package.json +53 -4
@@ -0,0 +1,101 @@
1
+ # Personalised demo video links with viewing statistics
2
+
3
+ **When to use:** send a named person a link to the public demo video page — to one
4
+ video or to the whole page — and later see whether they opened it, when, how often
5
+ and how long they watched each video. One sentence such as "Link für Erika
6
+ Mustermann zum Video Vorlagen" becomes one `demo link create`.
7
+
8
+ ## Prerequisites
9
+ - A token of a platform operator with `_platform.demoLinks.manage` (every full
10
+ platform admin holds it). A school's own token cannot create links — needs a
11
+ platform operator.
12
+ - Getting such a token: an operator holding `_platform.apiTokens.manage` mints one
13
+ for themselves with `plexus-cli tokens create <name>`, or for you with
14
+ `plexus-cli tokens create <name> --for <your email>` (then you need
15
+ `_platform.apiTokens.manage` too — needs a platform operator).
16
+ - `--env prod` for real recipients; every writing command against production also
17
+ needs `--allow-prod`.
18
+
19
+ ## Steps
20
+ ```bash
21
+ # 1. Which videos exist (id, date, title, length) — the id is what --video takes.
22
+ plexus-cli --env prod demo videos
23
+
24
+ # 2a. One recipient, one video → prints the finished URL.
25
+ plexus-cli --env prod demo link create --allow-prod \
26
+ --to "Erika Mustermann" --email erika@example.com --video vorlagen --campaign herbst
27
+
28
+ # 2b. Without --video the link opens the whole page.
29
+ plexus-cli --env prod demo link create --allow-prod --to "Erika Mustermann"
30
+
31
+ # 2c. Many recipients from a CSV (header row: name, email or E-Mail, video — email and
32
+ # video optional; a row's video wins over --video). Writes the links as a CSV that
33
+ # German Excel opens directly (";" separated, umlauts intact).
34
+ plexus-cli --env prod demo link create --allow-prod \
35
+ --from recipients.csv --video vorlagen --campaign herbst --out links.csv
36
+
37
+ # Machine-readable (code + url per recipient):
38
+ plexus-cli --env prod demo link create --allow-prod --to "Erika Mustermann" --json
39
+ ```
40
+
41
+ Send each recipient **their own** URL. The link's `?r=<code>` names the recipient;
42
+ the rest of the URL is the page's usual jump to the video.
43
+
44
+ The recipient CSV must be **UTF-8**: in Excel, *Save as → „CSV UTF-8 (durch
45
+ Trennzeichen getrennt) (*.csv)"*. A file in the older „CSV (Trennzeichen-getrennt)"
46
+ format is refused instead of turning „Müller" into „M�ller". `,` and `;` both work;
47
+ an unknown column or a row with more values than the header (an unquoted comma in a
48
+ name) is refused, naming the row.
49
+
50
+ ## Verify
51
+ ```bash
52
+ plexus-cli --env prod demo link list --campaign herbst
53
+ plexus-cli --env prod demo link stats --campaign herbst # or: demo link stats <code>
54
+ ```
55
+ `stats` shows one row per recipient and video:
56
+ - `besuche` — page loads a person engaged with (see the gotchas),
57
+ - `erster` / `letzter` — first and latest visit, German time,
58
+ - `gesehen` — seconds actually played, summed over all visits, / video length,
59
+ - `%` — how much of the video the best single visit actually played. Skipping ahead
60
+ to the end does not count: someone who drags the slider to the end and stops shows
61
+ a few seconds and a low `%`.
62
+
63
+ A recipient who never opened the link shows `0` and `—`.
64
+
65
+ To stop counting a link (e.g. it was posted publicly by mistake):
66
+ ```bash
67
+ plexus-cli --env prod demo link revoke --allow-prod <code>
68
+ ```
69
+ The page keeps working for everyone; the statistics recorded so far stay readable.
70
+
71
+ To erase a link on request (the name, the address and everything recorded):
72
+ ```bash
73
+ plexus-cli --env prod demo link delete --allow-prod <code> # shows what would go
74
+ plexus-cli --env prod demo link delete --allow-prod <code> --confirm # deletes, cannot be undone
75
+ ```
76
+
77
+ ## Idempotency / gotchas
78
+ - **When a visit counts.** Only after the page was visible for 5 seconds or the
79
+ visitor scrolled, tapped, clicked, typed or pressed play. Mail scanners that only fetch
80
+ the link (Outlook Safe Links and the like) never count; a scanner that renders the
81
+ page for a moment rarely does. **A play is the hard signal**: `gesehen` above
82
+ `0:00` means a person watched. `besuche = 0` right after sending is expected.
83
+ - **A forwarded link counts for the original recipient.** The link knows only whom it
84
+ was created for; if they forward it, the colleague's viewing is counted under their
85
+ name. Create a separate link per person.
86
+ - **Every page load is a visit**, a reload too. Switching view or video on the page
87
+ is not a new visit.
88
+ - **Watching time is counted from playback**, every 10 seconds and when the viewer
89
+ pauses or leaves: closing the tab abruptly can lose up to 10 seconds.
90
+ - **No cookie, nothing stored in the browser, no session replay** for pages opened
91
+ through such a link. Visits and playback are deleted after 12 months; the link
92
+ itself (name, address) stays until `demo link delete`.
93
+ - `create` is not idempotent: running it twice gives the same person two links.
94
+ `revoke` is: revoking twice keeps the first time.
95
+ - An unknown video id is refused, and the error lists every valid id. A CSV batch is
96
+ created all or nothing (at most 500 recipients per call).
97
+ - A heavily shared link records at most 30 page loads an hour; beyond that its
98
+ visits are not counted.
99
+
100
+ ## Related
101
+ - `plexus-cli demo link stats --json` for further processing (e.g. `jq`).
@@ -0,0 +1,77 @@
1
+ # Debug a mail delivery
2
+
3
+ **When:** "no mail arrived", a red `deadLastHour` badge in the platform
4
+ console, or a `backend:convex` GlitchTip issue tagged `area=notifications`.
5
+
6
+ ## 1. Is the sweeper alive, and is anything dead?
7
+
8
+ ```bash
9
+ plexus-cli --env staging notifications sweeper-status
10
+ plexus-cli --env staging notifications list --status dead
11
+ ```
12
+
13
+ `ranAt` older than a few minutes ⇒ the cron is not firing (deploy broke?).
14
+ `deadLastHour > 0` ⇒ go to step 3 with an id from the list.
15
+
16
+ ## 2. Find the delivery for a person
17
+
18
+ ```bash
19
+ plexus-cli --env staging notifications list --to parent@example.org --since 2d
20
+ plexus-cli --env staging notifications list --template account.loginLink --since 24h
21
+ ```
22
+
23
+ For a sign-in problem start with `users login-trail` instead — see
24
+ [Trace why someone could not sign in](./trace-a-login.md).
25
+
26
+ Statuses: `queued` (waiting / backoff), `sending` (in flight), `sent` (relay
27
+ accepted — NOT "in the inbox"), `skipped` (a successful non-send: recipient
28
+ gone, precondition no longer true, reserved TLD — in `live` every RFC 2606
29
+ address, in `redirect` only `.invalid`), `dead` (gave up).
30
+
31
+ ## 3. Read the evidence
32
+
33
+ ```bash
34
+ plexus-cli --env staging notifications show <deliveryId>
35
+ ```
36
+
37
+ | `errorCode` | Means | Do |
38
+ | --- | --- | --- |
39
+ | `delivery_mode_invalid` / `config_missing` | deployment env is incomplete | needs a platform operator: complete the deployment's mail settings, then `retry` |
40
+ | `smtp_auth` | relay rejected the credentials | needs a platform operator: rotate the relay password in the deployment's settings, then `retry` |
41
+ | `smtp_policy` | relay rejected OUR sender at `MAIL FROM` | `MAIL_FROM_ADDRESS` not allowed for this account — ask the mail admin |
42
+ | `smtp_content` with 4xx at `DATA` | milter (DKIM) hiccup — retried automatically | if it stays: the relay's DKIM key/permissions — ask the mail admin |
43
+ | `smtp_recipient_rejected` | 5xx at `RCPT TO` | the address does not exist — nothing to retry |
44
+ | `recipient_revoked` / `recipient_changed` / `recipient_deleted` | identity changed after enqueue | expected; re-raise the event if still wanted |
45
+ | `never_claimed` | the dispatch action fails BEFORE it can claim | bundle/import problem — needs a platform operator (deployment logs of the action), fix, `retry` |
46
+ | `unknown_outcome` | action died after SMTP began | the recipient MAY have received it; decide, then `retry` |
47
+
48
+ `lastSmtpCommand` / `lastSmtpResponseCode` / `lastSmtpResponse` carry the raw
49
+ relay line. `possibleDuplicate: true` means attempt N may already have landed.
50
+
51
+ ## 4. Prove the configuration end to end
52
+
53
+ ```bash
54
+ plexus-cli --env staging notifications self-test --to <your-address>
55
+ ```
56
+
57
+ Verifies the connection + credentials, sends one mail, returns the SMTP
58
+ transcript. On staging the mail is redirected like every other; on prod it
59
+ really goes to `--to`.
60
+
61
+ ## 5. Retry
62
+
63
+ ```bash
64
+ plexus-cli --env staging notifications retry <deliveryId>
65
+ ```
66
+
67
+ Only `dead` rows. It is a fresh attempt through the normal claim path — the
68
+ retry budget is widened by exactly one.
69
+
70
+ ## Gotchas
71
+
72
+ - `sent` = accepted by the relay. The last hop (relay → GMX) is invisible; on
73
+ staging the platform operator reads the catch-all mailbox.
74
+ - A `451` is not always transient: the relay says `451 4.7.1` for a
75
+ sender-policy violation and for a broken DKIM key. Look at the COMMAND.
76
+ - Locally nothing is ever sent (`dry`): the mail is in the Dev Rail →
77
+ `notifications:listDevPreviews`.
@@ -0,0 +1,90 @@
1
+ # Choose which mail a school sends (mail level)
2
+
3
+ > **Version check first.** A deployment on a release older than the mail level
4
+ > ignores `--field mailLevel` WITHOUT an error and keeps reading the retired
5
+ > `tenant.enabledMailCategories` (`[]` = sign-in only, every category = all).
6
+ > If `config export` shows no `mailLevel`, stop and ask the platform operator.
7
+ > The same skew means a tenant scaffolded with a newer CLI against an older
8
+ > deployment starts at sign-in mail only.
9
+
10
+ **When to use:** a school wants only the sign-in links for FAMILIES for now
11
+ („Anmeldelinks ja, alles andere nein"), or is ready for every mail.
12
+
13
+ **The level is family mail only.**
14
+ Letters to the school's own staff (`audience: "internal"`: new tasks, passed
15
+ deadlines, family messages, mentions) go out at BOTH levels; each staff member
16
+ decides them in „Einstellungen" (personal categories, runbook „Which mail a
17
+ staff member wants"). Do not use the level to quiet staff mail.
18
+
19
+ **Current stance:** every tenant — prod, staging and the `test` demo tenant —
20
+ runs `loginOnly` until the platform operator decides otherwise. Switching a real
21
+ school to `all` is that decision, not a routine step. Locally, a reseeded `test` tenant is at `loginOnly` too; to see a
22
+ rendered admission, family-task or school-chat letter set it to `all` on your
23
+ own deployment (`--env local`). Staff letters render at both levels.
24
+
25
+ ## What it is
26
+
27
+ - A school has ONE setting, its **mail level** (`tenant.mailLevel`, required in
28
+ every tenant config):
29
+ - `loginOnly` — „An Familien nur Anmeldelinks": to families, sign-in links,
30
+ the access a school hands out, address confirmations („Bewerbung
31
+ begonnen/geparkt" included) and security notices about one's own account
32
+ (e.g. a changed login address).
33
+ - `all` — „Alle E-Mails": to families additionally admission, family task
34
+ reminders and the school's chat messages.
35
+ - Every letter still declares a **category** in code (`login`, `security`,
36
+ `admission`, `tasks`, `schoolMessages`; for staff the personal `staffTasks`,
37
+ `staffAppointments`, `staffDeadlines`, `familyMessages`, `mentions`). It names
38
+ the letter in a skipped delivery's reason; a school cannot switch single
39
+ categories.
40
+ - The `notifications` module must be enabled for `all`; with the module off only
41
+ sign-in and security mail go out whatever the level says.
42
+ - Refused by `config set` / `config apply`: a missing or unknown level
43
+ (`[notification/mailLevel]`), `all` without the `notifications` module
44
+ (`[notification/levelWithoutModule]`), and the retired spellings
45
+ `tenant.enabledMailCategories` (`[notification/mailCategoriesRetired]`)
46
+ and `tenant.mailEnabled` (`[notification/mailEnabledRetired]`) — a scratch
47
+ export from before the mail level must be rewritten, not applied.
48
+ - `MAIL_DELIVERY_MODE` of the deployment stays the outer boundary. Never change
49
+ it for this.
50
+
51
+ ## Set it (dry run first)
52
+
53
+ ```bash
54
+ # sign-in mail only
55
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
56
+ --key <slug> --field mailLevel --value loginOnly
57
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
58
+ --key <slug> --field mailLevel --value loginOnly --confirm
59
+
60
+ # everything (what the introduction of the level wrote for every school that sent mail)
61
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
62
+ --key <slug> --field mailLevel --value all --confirm
63
+ ```
64
+
65
+ Through a scratch export instead: `config export` → set
66
+ `"tenant": { …, "mailLevel": "loginOnly" }` → `config apply --from snapshot:…`.
67
+
68
+ ## Verify
69
+
70
+ - `plexus-cli --env <env> config export --tenant <slug>` shows `mailLevel`
71
+ (always emitted).
72
+ - Platform rail → **Mandanten** (needs `_platform.config.read`): the column
73
+ „E-Mails" says „Nur Anmeldelinks" (amber) or „Alle E-Mails" — as the send
74
+ path answers it, so module off reads „Nur Anmeldelinks" too.
75
+ - Dashboard of the school: banner „Hinweis zur Konfiguration" on top of the
76
+ page, gap `mailDisabled` („Diese Schule versendet nur Anmeldelinks …") —
77
+ unless the school hides that notice: `config set --tenant <slug> --target tenant
78
+ --key <slug> --field hideMailLevelNotice --value true --confirm` (mail-module setting, shown under
79
+ „notifications" in Platform → Mandanten; the test tenant does). Only this
80
+ notice goes; a configuration error always shows.
81
+ - A suppressed letter is a row: `plexus-cli --env <env> notifications …` lists it
82
+ `skipped`, `tenant_delivery_disabled`, last error
83
+ `the school's mail level does not send "<category>" mail`.
84
+
85
+ ## Gotchas
86
+
87
+ - A skipped delivery is never re-sent when the level goes back to `all` — the
88
+ setting decides at enqueue AND at send time, not retroactively.
89
+ - `applicant.accessSent` („Zugang versenden → Per Mail senden") is `login`: it
90
+ goes out at `loginOnly`.
@@ -0,0 +1,155 @@
1
+ # Reword a letter for one school
2
+
3
+ **When to use:** a school wants one of the mails Plexus sends in its name to say
4
+ something else — a different opening paragraph, its own sign-off — and you want
5
+ to prove the new words actually reach a rendered mail before anyone relies on it.
6
+
7
+ ## Prerequisites
8
+
9
+ - `--tenant <slug>` on every command: a letter is reworded **per school**, never
10
+ platform-wide.
11
+ - Permissions `notifications._self.access` (read) and
12
+ `notifications.mail.configure` (write). The local CLI bootstrap has both.
13
+ - Locally: a running local development deployment (`--env local`). **Nothing is
14
+ ever sent from a local deployment** (`MAIL_DELIVERY_MODE=dry`) — the rendered mail
15
+ lands in `notification_previews`, which is exactly what makes this verifiable.
16
+
17
+ ## Steps
18
+
19
+ ```bash
20
+ # 1. What can this school reword, and what has it already changed?
21
+ plexus-cli --env local --tenant test notifications letters list
22
+
23
+ # 2. One letter, block by block, with the placeholders the LETTER may use
24
+ # printed above them — they belong to the letter, not to one paragraph, so a
25
+ # school may move a value into whichever block it likes. They are an
26
+ # ALLOWLIST: an undeclared {{x}} is refused, never silently printed. One
27
+ # marked "(Pflicht)" must appear somewhere in the BODY or the save is refused
28
+ # — that is what replaced the lock on the two sign-in letters.
29
+ plexus-cli --env local --tenant test notifications letters show applicant.tasksOpen --locale de
30
+
31
+ # 3. Reword ONE block, in ONE language. Untouched blocks are not written and
32
+ # keep following the platform text of that language; the other language is
33
+ # not touched at all (a German rewording never words the English letter).
34
+ plexus-cli --env local --tenant test notifications letters set applicant.tasksOpen --locale de \
35
+ --slot intro="bei {{schoolName}} warten {{taskCount}} Aufgaben auf Sie."
36
+ plexus-cli --env local --tenant test notifications letters set applicant.tasksOpen --locale en \
37
+ --slot intro="{{taskCount}} tasks at {{schoolName}} are waiting for you."
38
+
39
+ # A paragraph with line breaks belongs in a file, not in a shell argument:
40
+ # plexus-cli --env local --tenant test notifications letters set applicant.tasksOpen --locale de \
41
+ # --slot-file intro=./intro.txt
42
+
43
+ # 4. The shared letter frame (greeting, sign-off, sender name) is separate —
44
+ # it is the same frame for EVERY letter this school sends, worded in both
45
+ # languages at once (each letter carries its own language's line).
46
+ plexus-cli --env local --tenant test notifications chrome show
47
+ plexus-cli --env local --tenant test notifications chrome set \
48
+ --closing-de="Herzliche Grüße
49
+ Ihr Grundschul-Team" \
50
+ --closing-en="Warm regards
51
+ Your primary school team"
52
+ ```
53
+
54
+ ## Verify
55
+
56
+ Reading back the catalog only proves the row was stored. What has to be proven is
57
+ that the words reached a **rendered mail**:
58
+
59
+ ```bash
60
+ # a) The stored deviation
61
+ plexus-cli --env local --tenant test notifications letters list # "reworded: de, en"
62
+ plexus-cli --env local --tenant test notifications letters show applicant.tasksOpen --locale en
63
+
64
+ # b) Trigger the letter (for applicant.tasksOpen: open an external task on an
65
+ # application — e.g. a workflow task addressed to the family), then find its
66
+ # delivery and read the body that was actually rendered:
67
+ plexus-cli --env local notifications list --limit 20
68
+ plexus-cli --env local notifications show <deliveryId> --json | grep -A12 '"letter"'
69
+ ```
70
+
71
+ `letter.text` (and `letter.html`) carry the reworded sentence, and
72
+ `letter.version.reworded` names the blocks in which the school's wording stood,
73
+ `letter.version.locale` the language it went out in — the RECIPIENT ACCOUNT's
74
+ (`users.language`). To see the English letter go out, the recipient's account
75
+ must have chosen English; the preview in Administration → Benachrichtigungen
76
+ shows either language without one. The same rows are the Dev Rail →
77
+ **Mail** panel, which is the human version of the same check (and the place to
78
+ follow a sign-in link this deployment "sent").
79
+
80
+ Roll back — one block, or the whole letter:
81
+
82
+ ```bash
83
+ plexus-cli --env local --tenant test notifications letters reset applicant.tasksOpen --locale de --slot intro
84
+ plexus-cli --env local --tenant test notifications letters reset applicant.tasksOpen --locale en --all
85
+ plexus-cli --env local --tenant test notifications chrome set --closing-de="" --closing-en="" # back to the platform sign-off
86
+ ```
87
+
88
+ The next rendered mail carries the platform text again — no republish, no deploy.
89
+
90
+ ## Idempotency / gotchas
91
+
92
+ - **A deviation is a delta, not a copy.** Only the blocks a school rewrote are
93
+ stored. A block left alone keeps following the code, so a later platform
94
+ improvement to it still reaches that school. Setting a block to *exactly* the
95
+ platform text therefore stores nothing — the command's `deviating blocks` line
96
+ is the backend's answer, and it will not list that block.
97
+ - **`set` merges.** Passing `--slot intro=…` does not wipe a deviation on
98
+ `outro`. The way back is `reset`, which names what to drop.
99
+ - **`reset` without `--slot` needs `--all`.** Dropping every deviation of a
100
+ letter must be typed, not be what happens when a flag is forgotten.
101
+ - **Some letters have no blocks at all.** The two sign-in letters
102
+ (`applicant.magicLink`, `applicant.verifyEmail`) are category `login`: the mail
103
+ that carries a login link is not rewordable by anyone, so it declares nothing
104
+ to reword. They show `— (not editable)` in the list.
105
+ - **An unknown block or placeholder throws, naming every finding.** Nothing is
106
+ written — there is no partial save.
107
+ - **`--locale de|en` is required** on `show`, `set` and `reset`: every
108
+ letter exists in German and English, each worded on its own, and a command
109
+ that silently picked one would reword the letter you were not looking at. A
110
+ recipient gets the language of their own account; a school that rewords only
111
+ the German letter sends its English families the platform's English text —
112
+ the workshop says so on the letter.
113
+ - **This is configuration, not operational state.** A change made here is
114
+ exported, diffed and promoted like any other tenant setting — see
115
+ "Promote the wording to another environment" below. On staging/prod prefer
116
+ the config route so the change is reviewable; the direct `letters set` is for
117
+ local exploration and for a one-off correction with the platform operator watching.
118
+
119
+ ## Promote the wording to another environment
120
+
121
+ The deviations live in the tenant config under `notificationLetters` (identity
122
+ `templateId:locale:scopeKey`) and the frame under
123
+ `tenant.notificationChrome`, so the ordinary config-as-code verbs carry them:
124
+
125
+ ```bash
126
+ # Export → edit → dry-run → confirm, exactly as for any other tenant setting.
127
+ plexus-cli --env staging config export --tenant test --out /tmp/test.json
128
+ # …edit `notificationLetters` / `tenant.notificationChrome` in /tmp/test.json…
129
+ plexus-cli --env staging config apply --tenant test --from snapshot:/tmp/test.json
130
+ plexus-cli --env staging config apply --tenant test --from snapshot:/tmp/test.json --confirm
131
+
132
+ # One block, without an export round trip. The --key IS the identity triple
133
+ # (templateId:locale:scopeKey) — a letter is worded per language and per scope.
134
+ plexus-cli --env staging config set --tenant test \
135
+ --target notification-letters --key "applicant.tasksOpen:de:tenant" \
136
+ --field slots.intro --value "…" --confirm
137
+ ```
138
+
139
+ Two things follow from "a deviation is a delta":
140
+
141
+ - A letter **absent** from `notificationLetters` is a letter this school does not
142
+ deviate from, so applying a config without it takes that letter back to the
143
+ platform text. Removing an entry is how you undo a rewording in config.
144
+ - `config set` **edits an existing entry**; it does not create one. The first
145
+ deviation for a letter comes from `notifications letters set` or from an
146
+ edited export.
147
+
148
+ ## Related
149
+
150
+ - [Debug a mail delivery](./debug-mail-delivery.md) — when the mail did not
151
+ arrive at all (the operator's log, not the school's wording).
152
+ - [Update a tenant's config](../config/update-tenant-config.md) — the export →
153
+ edit → dry-run → apply loop these letters travel in.
154
+ - `plexus-cli notifications letters show --help` (and `set`/`reset`) — the letter /
155
+ block / override commands; a letter showing `— (not editable)` is one a school may not reword.
@@ -0,0 +1,50 @@
1
+ # Trace why someone could not sign in
2
+
3
+ **When:** „I asked for a sign-in link and got nothing", „the link said already
4
+ used", „my new password does not work" — on any environment, after the fact.
5
+
6
+ ## Prerequisites
7
+
8
+ A platform token with `_platform.users.manage` (sessions name IP addresses) and
9
+ `_platform.notifications.monitor` (the delivery list).
10
+
11
+ ## Steps
12
+
13
+ ```bash
14
+ # Everything the platform still knows about the address, oldest first.
15
+ plexus-cli --env prod users login-trail ada@example.com --since 24h
16
+
17
+ # Only the login page's letters to that address, with the relay's answer.
18
+ plexus-cli --env prod notifications list --template account.loginLink --to ada@example.com --since 24h
19
+ ```
20
+
21
+ `login-trail` prints one time-ordered list:
22
+
23
+ | Line | Means |
24
+ | --- | --- |
25
+ | `abgelehnt: <reason>` | the login page sent this KNOWN account no link (`platformAdmin`, `unprovenAddress`, `noSchoolWithOption`) |
26
+ | `Brief account.loginLink: sent — Relay: 250 … queued as <id>` | our relay accepted the mail; the queue id finds it in the relay's log |
27
+ | `Sitzung von <ip> — <browser>` | a sign-in happened. A session seconds after the letter from a foreign network is a **mail scanner** (Exchange Safe Links), not the person |
28
+ | `Passwort gesetzt (zuletzt)` | the credential was last changed then — compare with the failed attempts |
29
+
30
+ ## Reading it (an invented example)
31
+
32
+ - letter `sent` 09:12:05 → session from `203.0.113.7` at 09:12:29, a network the school
33
+ does not use → the person got „link already used": the scanner spent it. A link now opens a page first and only
34
+ its button spends the token, so a scanner no longer uses it up.
35
+ - password set 09:20:41, session from the school's network 09:20:42, failed
36
+ attempts before: the earlier resets went to **another** account (check the
37
+ `Passwort gesetzt` line of the account the person actually uses).
38
+
39
+ ## Gotchas
40
+
41
+ - Not recorded at all (the command lists it under „Nicht erfasst"): rate-limited
42
+ requests, requests for an address without an account (both only in the
43
+ function log, minutes), anything older than 30 days, ended sessions, and what
44
+ happened after our relay (inbox / spam / quarantine / bounce).
45
+ - `sent` in every list means „our relay said 250", never „in the inbox". The
46
+ console's word for it is „Versendet".
47
+
48
+ ## Related
49
+
50
+ - [Debug a mail delivery](./debug-mail-delivery.md)