@smartkomm/plexus-cli 0.0.0-stage → 0.1.0
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.
- package/README.md +826 -2
- package/dist/cli.mjs +483 -0
- package/dist/skill/.managed-by-plexus-cli +1 -0
- package/dist/skill/SKILL.md +185 -0
- package/dist/skill/references/accounts/email-changes.md +46 -0
- package/dist/skill/references/applications/inspect-process-store.md +61 -0
- package/dist/skill/references/applications/start-a-test-application.md +66 -0
- package/dist/skill/references/audit/answer-access-request.md +115 -0
- package/dist/skill/references/config/attach-a-template.md +117 -0
- package/dist/skill/references/config/init-tenant.md +121 -0
- package/dist/skill/references/config/onboard-tenant.md +71 -0
- package/dist/skill/references/config/rename-program-stage.md +99 -0
- package/dist/skill/references/config/set-tenant-branding.md +150 -0
- package/dist/skill/references/config/set-tenant-terms.md +50 -0
- package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
- package/dist/skill/references/config/update-tenant-config.md +170 -0
- package/dist/skill/references/config/wipe-tenant-config.md +107 -0
- package/dist/skill/references/export/create-named-export.md +47 -0
- package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
- package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
- package/dist/skill/references/finance/change-contract-grant.md +36 -0
- package/dist/skill/references/finance/record-submission-date.md +33 -0
- package/dist/skill/references/interessenten/list-interessenten.md +54 -0
- package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
- package/dist/skill/references/notifications/mail-level.md +90 -0
- package/dist/skill/references/notifications/reword-a-letter.md +155 -0
- package/dist/skill/references/notifications/trace-a-login.md +50 -0
- package/dist/skill/references/offer/add-workflow-node.md +268 -0
- package/dist/skill/references/offer/addresses.md +129 -0
- package/dist/skill/references/offer/appointment-task.md +79 -0
- package/dist/skill/references/offer/assignment-rule.md +195 -0
- package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
- package/dist/skill/references/offer/author-form-scenarios.md +97 -0
- package/dist/skill/references/offer/author-manifest-meta.md +94 -0
- package/dist/skill/references/offer/author-smartform.md +102 -0
- package/dist/skill/references/offer/bind-node-input.md +76 -0
- package/dist/skill/references/offer/completeness-check.md +137 -0
- package/dist/skill/references/offer/countersign-node.md +91 -0
- package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
- package/dist/skill/references/offer/declare-node-reads.md +99 -0
- package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
- package/dist/skill/references/offer/edit-smartform.md +121 -0
- package/dist/skill/references/offer/edit-workflow-node.md +126 -0
- package/dist/skill/references/offer/engage-push-node.md +147 -0
- package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
- package/dist/skill/references/offer/node-tasks.md +162 -0
- package/dist/skill/references/offer/optional-assessment.md +65 -0
- package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
- package/dist/skill/references/offer/set-offer-capacities.md +78 -0
- package/dist/skill/references/offer/staff-input-if-node.md +112 -0
- package/dist/skill/references/offer/task-appointment-result.md +69 -0
- package/dist/skill/references/offer/task-family-request.md +69 -0
- package/dist/skill/references/offer/task-result-files.md +52 -0
- package/dist/skill/references/offer/template-document-node.md +113 -0
- package/dist/skill/references/offer/workflow-default-channels.md +69 -0
- package/dist/skill/references/resolver/manage-contract-resolver.md +159 -0
- package/dist/skill/references/retention/erase-due-applications.md +99 -0
- package/dist/skill/references/retention/erase-due-parties.md +107 -0
- package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
- package/dist/skill/references/signing/README.md +113 -0
- package/dist/skill/references/signing/connect-a-school.md +90 -0
- package/dist/skill/references/signing/rotate-the-key.md +41 -0
- package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
- package/dist/skill/references/stripe/connect-a-school.md +107 -0
- package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
- package/dist/skill/references/test-actions/drive-a-test-application.md +59 -0
- package/dist/skill/references/users/add-platform-admin.md +59 -0
- package/dist/skill/references/users/add-platform-user.md +59 -0
- package/dist/skill/references/users/add-tenant-admin.md +75 -0
- package/dist/skill/references/users/change-login-email.md +55 -0
- package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
- package/dist/skill/references/users/delete-permission-group.md +76 -0
- package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
- package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
- package/dist/skill/references/users/set-password.md +57 -0
- package/dist/skill/references/users/use-test-account.md +79 -0
- package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
- package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
- package/dist/spec.snapshot.json +145815 -0
- package/package.json +53 -4
|
@@ -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)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Add a workflow node to an offer bundle
|
|
2
|
+
|
|
3
|
+
**When to use:** an offer's admission workflow needs another step (an approval
|
|
4
|
+
gate, a waitlist holding node, a selection terminus, another form, …) and you want
|
|
5
|
+
the graph + progress board kept in sync automatically — instead of hand-editing
|
|
6
|
+
`workflow.json` (graph + `progressWiring.mappings`) and `manifest.json`
|
|
7
|
+
(`progressLayout` states + columns) in lockstep.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
- **Env / scope:** none for authoring — `offer workflow node add|rm|list` and
|
|
11
|
+
`offer workflow show` are **offline** (read/write the on-disk bundle, no
|
|
12
|
+
deployment). Only the final `offer apply --publish` is online and needs
|
|
13
|
+
`--env <env>` + a token with offer config rights (`plexus-cli --env <env> whoami`).
|
|
14
|
+
- **A bundle on disk:** scaffold one with `offer init`, or `offer export` an
|
|
15
|
+
existing offer into a directory.
|
|
16
|
+
- **Tooling:** an installed, current `plexus-cli` (`plexus-cli offer workflow node add --help`).
|
|
17
|
+
- **Both languages:** a node's label is ONE `{ de, en }` field — `--label` AND
|
|
18
|
+
`--label-en` are required (as `--reason` + `--reason-en` on a collectPayment
|
|
19
|
+
node and `<id>:<Deutsch>:<English>` per `--decision-outcomes` entry).
|
|
20
|
+
- **Supported types:** `form`, `decision`, `review`, `setFlag`, `approval`, `waitlist`,
|
|
21
|
+
`roleAssign`, `join`, `collectPayment`, `completenessCheck`, `countersign`, `engagePush`, `if`, `end`
|
|
22
|
+
(the list `offer workflow node add --help` prints). `decision` is the
|
|
23
|
+
configurable-decision builder (ATS primitives — assign-decision, composed
|
|
24
|
+
tasks, artifacts, decision-as-task); `approval` is a **deprecated alias** for
|
|
25
|
+
`decision` (maps `--decision-outcomes` → outcomes). `setFlag` flips an external
|
|
26
|
+
flag on the form instance it names (`--flag-key` + `--target '$node.<formNode>.admission'`).
|
|
27
|
+
`countersign` („Unterschrift (intern)") is the
|
|
28
|
+
school's countersignature of a checked contract — see
|
|
29
|
+
[Countersign the contract](./countersign-node.md).
|
|
30
|
+
`if` („Wenn") routes on one DSL expression over `$env` (`--condition
|
|
31
|
+
'$env.staffInput.<key>'`, outcomes `true` / `false` — route both with
|
|
32
|
+
`--outcomes`) — see [Branch on a staff input](./staff-input-if-node.md).
|
|
33
|
+
`completenessCheck` waits for outstanding documents (`--require`,
|
|
34
|
+
`--deadline-days`|`--deadline-until`, `--notification requested=…`, optional
|
|
35
|
+
`--guest-role` + `--person`) — see
|
|
36
|
+
[Wait for outstanding documents](./completeness-check.md). The folded
|
|
37
|
+
`governedDocument` (offer letter / contract) cannot be added by the CLI yet.
|
|
38
|
+
A `form` node binds a SmartForm via `--form` / `--new-form` (see
|
|
39
|
+
[Attach a form to a workflow](./attach-form-to-workflow.md)).
|
|
40
|
+
- **Letters with the node:** `--notification <event>=<templateId>` (repeatable)
|
|
41
|
+
binds a letter in the same call — required when the type declares the event
|
|
42
|
+
required (completenessCheck `requested`, countersign `countersigned`), since
|
|
43
|
+
`add` validates before writing.
|
|
44
|
+
- **Addresses with the node**: every flag that reads another node's data
|
|
45
|
+
takes a full address naming its producer, in single quotes — `--reads` /
|
|
46
|
+
`--require` (`'$node.<formNode>.admission.<field>'`), `--person`
|
|
47
|
+
(`'$node.<formNode>.admission.beneficiary'`), `--target` / `--adopt-form`
|
|
48
|
+
(`'$node.<formNode>.admission'`), `--contract`, engagePush `--form` / `--payment`.
|
|
49
|
+
A bare key is refused with the addresses visible at the new node; `add` binds
|
|
50
|
+
NOTHING implicitly. Bundles exported before addresses were introduced (with `wires`) are refused —
|
|
51
|
+
export again. See [Name what a node reads](./addresses.md).
|
|
52
|
+
|
|
53
|
+
### Payment gate (`collectPayment`)
|
|
54
|
+
`--article <key>` and `--settlement online|separate` are **required** (no
|
|
55
|
+
default); both are written as the node's parameters (`inputs.article`,
|
|
56
|
+
`inputs.settlement` — the editor lists them under „Parameter"). The article is
|
|
57
|
+
a LINK: the key (`billing_item_definitions.configKey`) of a `one_time`
|
|
58
|
+
article assigned to the offer's program (Finanzen → Artikel, directly or through
|
|
59
|
+
an offered package). Amount, currency and the Stripe line name are read from the
|
|
60
|
+
article at runtime — the node stores no amount, and publish refuses `amount` /
|
|
61
|
+
`currency` on it (`[collectPayment/retiredField]`) as well as an unknown,
|
|
62
|
+
non-one-time or out-of-program article. A price change in Finanzen applies to
|
|
63
|
+
the next payment without a republish. `online`: the family gets a „Zahlung leisten" request row and pays
|
|
64
|
+
through Stripe Checkout; the process responsible can also close it
|
|
65
|
+
„Ohne Online-Zahlung abschließen" with a required reason (e.g. on an
|
|
66
|
+
installation without Stripe). `separate`: the school bills
|
|
67
|
+
the fee outside Plexus — nothing is asked of the family (no row, no letter; the
|
|
68
|
+
portal says „Die Anmeldegebühr wird separat abgerechnet."), and the process
|
|
69
|
+
responsible closes the step in the cockpit with **„Zahlung überspringen"**
|
|
70
|
+
(optional comment, feed entry „Zahlung übersprungen", nothing booked). The
|
|
71
|
+
routing edge is `paid` either way. Switch an existing node with
|
|
72
|
+
`offer workflow node set --id <node> --settlement separate` (or
|
|
73
|
+
`--article <key>` to charge another article); a `requested`
|
|
74
|
+
letter bound on a `separate` node is refused (`[notification/unknownEvent]`).
|
|
75
|
+
Applications already parked on the node stay on the version they are pinned
|
|
76
|
+
to — moving them onto the new version after `apply --publish` needs a platform
|
|
77
|
+
operator.
|
|
78
|
+
|
|
79
|
+
### Terminal, fan-in, and role nodes
|
|
80
|
+
- **`end --outcome <outcome> --data-decision <commit|discard>`** authors a
|
|
81
|
+
terminal node's application outcome (`approved` | `rejected` | `withdrawn` |
|
|
82
|
+
`waitlist` | `expired`; default `approved`) and — required, no default —
|
|
83
|
+
whether the application's data is kept (`commit`) or discarded (`discard`)
|
|
84
|
+
at this end (the outcome is only a label and never decides it). It
|
|
85
|
+
writes the `terminalStatus` / `applicationOutcome` / `dataDecision` /
|
|
86
|
+
`terminationKind` literal inputs the end executor reads (fixing the old gap
|
|
87
|
+
where every CLI-authored end recorded `approved`), and contributes **one**
|
|
88
|
+
outcome-aware terminal column (e.g. a rejected end adds a red *Abgesagt*
|
|
89
|
+
column, not a misleading green *Eingeschrieben*). A second end is inserted
|
|
90
|
+
isolated — wire predecessors to it with `join --from` or `node set --outcomes`.
|
|
91
|
+
Override the ledger label with `--terminal-status`, the kind with
|
|
92
|
+
`--termination-kind`.
|
|
93
|
+
- **`join --from <nodeId>:<outcome>,…`** authors an explicit XOR fan-in
|
|
94
|
+
convergence. Each `--from` pair reroutes that source node's outcome onto the
|
|
95
|
+
join; the join's own onward edge defaults to the end node (override with
|
|
96
|
+
`--outcomes done:<target>`). Multi-input into a join is the one legal
|
|
97
|
+
many-to-one shape (the publish gate rejects multi-input on non-join nodes).
|
|
98
|
+
- **`roleAssign --role <key>`** grants a person-lifecycle role. `--role` is
|
|
99
|
+
validated **offline** against the manifest's declared `roleInstances` (fail
|
|
100
|
+
loud if absent — declare it first with `offer role add`). `--mode`
|
|
101
|
+
is `add` (default) | `remove` | `replace`; `--person '<address>'` is required —
|
|
102
|
+
the party that gets the role, a party leaf of a form
|
|
103
|
+
(`--person '$node.form.admission.beneficiary'`). Never defaulted.
|
|
104
|
+
|
|
105
|
+
### Configurable decision node (`decision`)
|
|
106
|
+
The `decision` node is the **one configurable decision node** (it replaced the old
|
|
107
|
+
`approval` node; `--type approval` is refused as an unknown type). By default it
|
|
108
|
+
declares the type outcomes `approved` / `rejected` / `waitlisted`. To author a
|
|
109
|
+
**custom** outcome set (with your own ids + labels), pass `--decision-outcomes
|
|
110
|
+
<id>:<Deutsch>:<English>,…` (an outcome's label is `{ de, en }` — both languages
|
|
111
|
+
are required) and route each with `--outcomes`:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Inquiry decision: direkt aufnehmen / zum Interview / absage.
|
|
115
|
+
plexus-cli offer workflow node add --dir ./demo \
|
|
116
|
+
--type decision --id decisionInquiry --label "Erstentscheidung" --label-en "Initial decision" --after form \
|
|
117
|
+
--decision-outcomes 'direkt:"Direkt aufnehmen":"Admit directly",interview:"Zum Interview":"To the interview",absage:"Absage":"Rejection"' \
|
|
118
|
+
--outcomes 'direkt:formApplication,interview:interview,absage:joinAbsage' \
|
|
119
|
+
--assign-decision zulassung --decision-as-task 'Erstentscheidung treffen'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each entry becomes a `NodeOutcome {id,label}` on `node.outcomes`
|
|
123
|
+
(first = `isDefault`); the backend executor + publish gate + cockpit UI all validate
|
|
124
|
+
and render decisions against whatever set the node declares. `--decision-outcomes` is
|
|
125
|
+
decision-only (fails loud on any other type). Outcomes whose id maps to a known
|
|
126
|
+
semantic (`direkt`/`accepted`→granted, `absage`/`rejected`→rejected,
|
|
127
|
+
`warteliste`/`waitlisted`→waitlisted) drive the roster chip + annotation mirror; any
|
|
128
|
+
other id (e.g. `interview`) is a real routing decision with no typed annotation.
|
|
129
|
+
Route each declared outcome to a target that exists — the publish gate checks coverage.
|
|
130
|
+
|
|
131
|
+
#### ATS lifecycle primitives on a decision node
|
|
132
|
+
A decision node carries the full offer-lifecycle authoring surface:
|
|
133
|
+
|
|
134
|
+
- **`--assign-decision <groupKey>`** — the permission group whose members hold the
|
|
135
|
+
synthesized decision Work-Item (= may pick the routing outcome, in ONE act).
|
|
136
|
+
Pairs with `--decision-as-task` (each requires the other — fails loud). Absent
|
|
137
|
+
= super-user-only decide (the instance's responsible).
|
|
138
|
+
- **`--comment-required <id>[,<id>]`** — authored outcome ids that require a decider comment.
|
|
139
|
+
- **`--task …`** (repeatable) — a composed task the node spawns on enter,
|
|
140
|
+
instantiating a code-owned kind (the kind carries the behaviour: actions,
|
|
141
|
+
close protocol, respawn, gate default):
|
|
142
|
+
- `key:task:title:group[:dueInDays]` — an internal check task (trivial `complete`).
|
|
143
|
+
- `key:approval:title:group[:revisionGroup]` — an approval; gates the node by
|
|
144
|
+
default. With **`--task-on-reject spawnRevision`** a rejecting verdict
|
|
145
|
+
spawns a revision back to `revisionGroup` (then REQUIRED — fails loud).
|
|
146
|
+
(`binds` is DELETED — the offer arc is node-internal; use the
|
|
147
|
+
`offer` node type for the prepare/approve arc.)
|
|
148
|
+
- **`--artifact key:type:visibility:label`** (repeatable) — a versioned artifact SLOT
|
|
149
|
+
(`visibility` = `internal` | `external`). Each slot exposes a `<key>Uploaded` node-export.
|
|
150
|
+
- **Assignment rule** (GRAPH-level, no flag) — who becomes responsible and who owns
|
|
151
|
+
each team is `graph.assignmentRules` in the same `workflow.json`; `node add/set` do
|
|
152
|
+
not touch it. A node that gives work to a team the rule has no slot for makes
|
|
153
|
+
`offer validate` / publish refuse with `[assignment/slotMissing]` — add a
|
|
154
|
+
`team:<key>` list in the bundle or in the workflow editor („Team-Verantwortliche").
|
|
155
|
+
Recipe: `playbooks/offer/assignment-rule.md`.
|
|
156
|
+
- **`--decision-as-task title[:dueInDays]`** — surface the decision as a
|
|
157
|
+
first-class queue Work-Item. Requires `--assign-decision` (and vice versa).
|
|
158
|
+
Combining with gating approval tasks is fine: the HARD node gate blocks a
|
|
159
|
+
non-rejecting advance server-side while gate-effective rows are open.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Offer create-phase (mirrors the grundschule createOffer node): external artifact,
|
|
163
|
+
# a prepare task, a finance approval gate with a ping-pong revision on reject, and
|
|
164
|
+
# the Schulleitung send-offer decision row (hard-gated on the finance approval).
|
|
165
|
+
plexus-cli offer workflow node add --dir ./demo \
|
|
166
|
+
--type decision --id createOffer --label "Angebot erstellen" --label-en "Prepare offer" --after form \
|
|
167
|
+
--decision-outcomes 'offerReady:"Angebot freigegeben",absage:"Angebot abgebrochen"' \
|
|
168
|
+
--assign-decision schulleitung --decision-as-task 'Angebot versenden:3' \
|
|
169
|
+
--artifact 'offer:offer:external:Angebotsschreiben' \
|
|
170
|
+
--task 'prepareOffer:checkTask:Angebotsschreiben vorbereiten:schulleitung:5' \
|
|
171
|
+
--task 'offerApproval:approval:Angebot freigeben:finanzen:offer:offerUploaded:schulleitung' \
|
|
172
|
+
--task-on-reject spawnRevision
|
|
173
|
+
|
|
174
|
+
# Task-driven selection decision (grundschule approvalSchulleitung), gate-less:
|
|
175
|
+
plexus-cli offer workflow node add --dir ./demo \
|
|
176
|
+
--type decision --id freigabe --label "Freigabe" --label-en "Approval" --after form \
|
|
177
|
+
--decision-outcomes 'approved:Freigegeben,rejected:Abgelehnt,waitlisted:Warteliste' \
|
|
178
|
+
--assign-decision schulleitung \
|
|
179
|
+
--comment-required rejected \
|
|
180
|
+
--decision-as-task 'Über Freigabe entscheiden:5'
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
All of the above is validated by the REAL publish gate on `offer validate`; the CLI adds
|
|
184
|
+
fail-loud cross-checks at the flag boundary (undeclared bound artifact, `spawnRevision`
|
|
185
|
+
without `revisionGroup`, `decision-as-task` ⇔ `assign-decision` pairing).
|
|
186
|
+
|
|
187
|
+
## Steps
|
|
188
|
+
```bash
|
|
189
|
+
# 1. Scaffold a minimal linear offer (start → form → end).
|
|
190
|
+
plexus-cli offer init --slug demo --title "Demo Schule" --title-en "Demo School" --tenant test --responsible sekretariat --out ./demo
|
|
191
|
+
|
|
192
|
+
# 2. Inspect the graph.
|
|
193
|
+
plexus-cli offer workflow node list --dir ./demo
|
|
194
|
+
plexus-cli offer workflow show --dir ./demo
|
|
195
|
+
|
|
196
|
+
# 3. Insert a decision gate between the form and the end node.
|
|
197
|
+
# --outcomes routes each declared outcome; any omitted outcome defaults to
|
|
198
|
+
# where --after currently points (here end), splicing into the chain.
|
|
199
|
+
plexus-cli offer workflow node add --dir ./demo \
|
|
200
|
+
--type decision --id approval --label "Freigabe" --label-en "Approval" --after form \
|
|
201
|
+
--assign-decision freigeber --decision-as-task 'Freigabe entscheiden' \
|
|
202
|
+
--outcomes approved:end,rejected:end
|
|
203
|
+
|
|
204
|
+
# 4. Validate offline against the REAL publish gate (add already ran it; this is
|
|
205
|
+
# the explicit re-check / CI gate).
|
|
206
|
+
plexus-cli offer validate --dir ./demo
|
|
207
|
+
|
|
208
|
+
# 5. Apply + publish (online). Needs --env + a token; cuts new versions on diff.
|
|
209
|
+
plexus-cli --env <env> --tenant test offer apply --dir ./demo --publish
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- **`--id`** is optional — defaults to the type, numbered from 2 when taken
|
|
213
|
+
(`approval`, `approval2`, …; `makeNodeId`). An id you pass must be a dot-free
|
|
214
|
+
identifier (`^[a-z][A-Za-z0-9]*$`, e.g. `decisionInquiry`) — `offer validate`
|
|
215
|
+
refuses anything else as `[graph/nodeIdFormat]`.
|
|
216
|
+
- **`--after <nodeId>`** inserts after that node and rewires its onward edge(s)
|
|
217
|
+
to the new node. Omit it to insert immediately before the end node.
|
|
218
|
+
- **`--type form`** binds a SmartForm — `--form <slug>` (existing) or
|
|
219
|
+
`--new-form --form <slug>` (scaffold + bind). See the dedicated playbook.
|
|
220
|
+
- **Remove** a node (rewires predecessors onto its single successor, strips its
|
|
221
|
+
progress entries, re-validates; refused while an address elsewhere names it):
|
|
222
|
+
```bash
|
|
223
|
+
plexus-cli offer workflow node rm --dir ./demo --id approval
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Verify
|
|
227
|
+
```bash
|
|
228
|
+
plexus-cli offer workflow node list --dir ./demo # the node appears, routing is wired
|
|
229
|
+
plexus-cli offer validate --dir ./demo # exit 0 = publish gate clean
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For the approval example, `offer validate` exits 0 with **no manual manifest
|
|
233
|
+
edit**: `offer workflow node add` materialised the `approval.released` process
|
|
234
|
+
state, its board column, and the `(approval, released) → process/approval.released`
|
|
235
|
+
wiring mapping. A decision that reads nothing of the form names no address
|
|
236
|
+
(`offer workflow node reads --dir ./demo --node approval` lists nothing). After `offer workflow node rm`, the graph returns to the linear
|
|
237
|
+
`form → end` shape and validation passes again.
|
|
238
|
+
|
|
239
|
+
## Idempotency / gotchas
|
|
240
|
+
- **`add` validates before writing.** Any error-severity issue from the real
|
|
241
|
+
publish gate (`runWorkflowValidators` etc.) blocks the write and exits 1 — the
|
|
242
|
+
bundle is left untouched. A failed `add` never leaves a half-spliced bundle.
|
|
243
|
+
- **Splice defaults can over-route.** Every declared outcome you DON'T name in
|
|
244
|
+
`--outcomes` defaults to the anchor's current onward target. Inserting a
|
|
245
|
+
multi-outcome node (endauswahl, approval) after a node that points at the end
|
|
246
|
+
node routes *all* its outcomes to the end node — pass `--outcomes` to fan them
|
|
247
|
+
out (e.g. `warteliste:waitlist`).
|
|
248
|
+
- **Nothing is bound implicitly.** A node reads a form only at the address it
|
|
249
|
+
names (`$node.<formNode>.admission.<field>`), and the producer must run on every
|
|
250
|
+
path before it (`[address/notVisible]` otherwise). A `form` node's `smartForm` is
|
|
251
|
+
written as a `{ kind: "literal", value: <slug> }`; with
|
|
252
|
+
`--adopt-form '$node.<formNode>.admission'` it continues that instance instead
|
|
253
|
+
(`adoptForm` holds the address; the adopting node produces nothing — later
|
|
254
|
+
nodes still read the creator's address). Explicit-by-default, matching `offer init`.
|
|
255
|
+
- **`rm` refuses** to remove the start (`eventTrigger`) or `end` node, a node with
|
|
256
|
+
multiple distinct successors (ambiguous splice), and a node any address elsewhere
|
|
257
|
+
still names (its form instance, document, contract, payment, or a value one of
|
|
258
|
+
its tasks writes) — the refusal lists every naming place (config path +
|
|
259
|
+
address); edit those first. Orphans surface at re-validate and block the write.
|
|
260
|
+
- **Progress labels/tones** come from one built-in default table, shared with the
|
|
261
|
+
`offer init` scaffold. An event with no default falls back to its name +
|
|
262
|
+
neutral tone and prints a warning.
|
|
263
|
+
|
|
264
|
+
## Related
|
|
265
|
+
- [Attach a form to a workflow](./attach-form-to-workflow.md) — the `--type form` flow.
|
|
266
|
+
- [Author a SmartForm as code](./author-smartform.md) — build the form itself.
|
|
267
|
+
- `plexus-cli offer --help` — the full `offer …` command reference; `offer validate`
|
|
268
|
+
runs the same publish-gate validators as `offer workflow node add`.
|