@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.
Files changed (80) hide show
  1. package/README.md +826 -2
  2. package/dist/cli.mjs +483 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +185 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  7. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  8. package/dist/skill/references/audit/answer-access-request.md +115 -0
  9. package/dist/skill/references/config/attach-a-template.md +117 -0
  10. package/dist/skill/references/config/init-tenant.md +121 -0
  11. package/dist/skill/references/config/onboard-tenant.md +71 -0
  12. package/dist/skill/references/config/rename-program-stage.md +99 -0
  13. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  14. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  15. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  16. package/dist/skill/references/config/update-tenant-config.md +170 -0
  17. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  18. package/dist/skill/references/export/create-named-export.md +47 -0
  19. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  20. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  21. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  22. package/dist/skill/references/finance/record-submission-date.md +33 -0
  23. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  24. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  25. package/dist/skill/references/notifications/mail-level.md +90 -0
  26. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  27. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  28. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  29. package/dist/skill/references/offer/addresses.md +129 -0
  30. package/dist/skill/references/offer/appointment-task.md +79 -0
  31. package/dist/skill/references/offer/assignment-rule.md +195 -0
  32. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  33. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  34. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  35. package/dist/skill/references/offer/author-smartform.md +102 -0
  36. package/dist/skill/references/offer/bind-node-input.md +76 -0
  37. package/dist/skill/references/offer/completeness-check.md +137 -0
  38. package/dist/skill/references/offer/countersign-node.md +91 -0
  39. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  40. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  41. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  42. package/dist/skill/references/offer/edit-smartform.md +121 -0
  43. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  44. package/dist/skill/references/offer/engage-push-node.md +147 -0
  45. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  46. package/dist/skill/references/offer/node-tasks.md +162 -0
  47. package/dist/skill/references/offer/optional-assessment.md +65 -0
  48. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  49. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  50. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  51. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  52. package/dist/skill/references/offer/task-family-request.md +69 -0
  53. package/dist/skill/references/offer/task-result-files.md +52 -0
  54. package/dist/skill/references/offer/template-document-node.md +113 -0
  55. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  56. package/dist/skill/references/resolver/manage-contract-resolver.md +159 -0
  57. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  58. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  59. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  60. package/dist/skill/references/signing/README.md +113 -0
  61. package/dist/skill/references/signing/connect-a-school.md +90 -0
  62. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  63. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  64. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  65. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  66. package/dist/skill/references/test-actions/drive-a-test-application.md +59 -0
  67. package/dist/skill/references/users/add-platform-admin.md +59 -0
  68. package/dist/skill/references/users/add-platform-user.md +59 -0
  69. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  70. package/dist/skill/references/users/change-login-email.md +55 -0
  71. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  72. package/dist/skill/references/users/delete-permission-group.md +76 -0
  73. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  74. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  75. package/dist/skill/references/users/set-password.md +57 -0
  76. package/dist/skill/references/users/use-test-account.md +79 -0
  77. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  78. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  79. package/dist/spec.snapshot.json +145815 -0
  80. package/package.json +53 -4
@@ -0,0 +1,170 @@
1
+ # Update a tenant's config
2
+
3
+ **When to use:** change a real tenant's live config-as-code — add or remove a search
4
+ filter, search target, field, list, display group, role binding, result icon, etc. — on
5
+ staging and/or prod, without a reseed and without breaking unrelated config.
6
+
7
+ ## Prerequisites
8
+
9
+ - `plexus-cli` installed and current (`plexus-cli --help`).
10
+ - A token configured for each target env: a platform-admin token
11
+ (`_platform.config.manage` to commit), or — for your OWN tenant only — a tenant admin's
12
+ token holding `administration.config.manage` (see
13
+ [CLI access for a tenant admin](../users/cli-access-for-tenant-admin.md)). The tenant
14
+ token cannot change most `tenant.*` settings (modules, `mailLevel`, …), the import
15
+ source, or add new accounts; that section lists every refusal. A local admin key
16
+ without a user identity does **not** satisfy the gate.
17
+ - The tenant **slug** (e.g. `test`). `plexus-cli config show` prints the resolved connection.
18
+ - Always **stage on staging first**, verify, then promote to prod.
19
+
20
+ ## Steps
21
+
22
+ The mechanism is **export → edit the snapshot → dry-run → confirm**. `export` snapshots the
23
+ live config *exactly* (round-trip is identity), so re-applying with one thing changed yields a
24
+ diff of only that one change.
25
+
26
+ ```bash
27
+ # 1. Snapshot the live config (writes config/.dev/<slug>/config.json below the current
28
+ # directory; keep it private — it holds the tenant's whole config and user roster).
29
+ plexus-cli config export --env staging --tenant <slug>
30
+
31
+ # 2. Edit config/.dev/<slug>/config.json — make the ONE change (e.g. remove the
32
+ # searchFilters entry with "key":"nachname"). Leave everything else untouched.
33
+
34
+ # 3. Dry-run (no --confirm → transaction rolls back, prints the plan).
35
+ plexus-cli config apply --env staging --tenant <slug> \
36
+ --from snapshot:config/.dev/<slug>/config.json
37
+
38
+ # 4. Commit once the dry-run shows only your intended change.
39
+ plexus-cli config apply --env staging --tenant <slug> \
40
+ --from snapshot:config/.dev/<slug>/config.json --confirm
41
+ ```
42
+
43
+ Then repeat 1-4 with `--env prod` (re-export from prod first — never assume prod == staging).
44
+
45
+ `config set` (surgical single-field edit) is the alternative for changing **one field of one
46
+ existing row**, but it **cannot delete a row** — removing an array element (a filter, a target,
47
+ an option) must go through export → edit → apply.
48
+
49
+ ## Verify
50
+
51
+ ```bash
52
+ # Re-export and confirm the change landed; nothing else moved.
53
+ plexus-cli config export --env staging --tenant <slug>
54
+ ```
55
+
56
+ Read the dry-run plan carefully — it is the safety gate:
57
+
58
+ - `+ create (all) N` / `~ update (all) N` / `- delete (all) N` / `· unchanged (all) N` aggregate counts.
59
+ - `~ update <target> <key>` — **one row per patched config row, by key** (e.g.
60
+ `~ update lists salutation`, `~ update admission.fields geburtsurkunde`). An idempotent
61
+ row is `· unchanged`, never an update — admission custom fields and derived attributes
62
+ included, in the dry run too. A deployment older than this row list prints one
63
+ `~ update (unnamed)` line instead of guessing.
64
+ - `- delete <target> <key>` — **one row per pruned config row, by key** (e.g.
65
+ `- delete searchFilters nachname`). This is exactly what you removed; confirm it matches.
66
+ - `- orphan <target>/<table> <key>` — rows the reconciler will NOT auto-delete (see gotchas).
67
+ - `! no account <email>` — a `foundation.users` entry with no login account. On `--confirm`
68
+ each of these is **minted a password, printed once** (`+ account <email> password: …`), so
69
+ read this line before committing in a shared terminal — add `--output creds.csv --quiet` to
70
+ steer the plaintexts into a file instead. A steady-state tenant usually shows none: an
71
+ existing account is linked, never re-minted.
72
+
73
+ - `~ slug units/<key> <from> → <to>` — an existing unit's URL moves (see
74
+ [Rename a unit's URL](#rename-a-units-url-slug) below). Expect none unless you changed a
75
+ `slug`.
76
+
77
+ **Control-run technique** (isolate baseline drift): a fresh *unedited* export re-applied as a
78
+ dry-run should show no `~ update` row; if it does, the named rows are the ones the export
79
+ does not round-trip. To prove your edit added only what you intended, dry-run the unedited export too and
80
+ diff the counts:
81
+
82
+ ```bash
83
+ plexus-cli config export --env staging --tenant <slug> --out /tmp/control.json
84
+ plexus-cli config apply --env staging --tenant <slug> --from snapshot:/tmp/control.json # baseline
85
+ ```
86
+
87
+ The difference between the control plan and your edited plan is your change.
88
+
89
+ ## Rename a unit's URL (slug)
90
+
91
+ A unit's **identity** is its config `key` (stored as `units.configKey`); its **slug** is only
92
+ the URL projection — `/{tenant}/e/units/<slug>` (portal landing + `/bewerben`) and the staff
93
+ `?unit=<slug>` scope. Renaming the unit's `name` never moves the slug; setting `slug` does.
94
+
95
+ - `slug` absent in config → an existing unit keeps its slug, a new unit derives one from its
96
+ name. `config export` always emits the live `slug`, so an exported snapshot pins it.
97
+ - `slug` present → create sets it, an existing unit is patched to it.
98
+ - Shape: lowercase `a-z0-9` words joined by single dashes (exactly what the name derivation
99
+ produces); `tenant` is reserved. Two units in one config may not share a slug (validator),
100
+ and a slug any other unit of the tenant holds — including a row no longer in config — fails
101
+ the apply with `[unit/slugTaken]`. Two units may swap slugs in one apply. To take the slug
102
+ of a unit no longer in config, list that unit again under its key with a new `slug` in
103
+ the same apply — it moves off like any config unit, no data lost. Only if the old unit
104
+ should go anyway: remove it first (`apply --confirm --allow-destructive`, deletes its
105
+ data), then set the slug in a second apply — orphans are deleted only after the
106
+ reconcile has planned the slugs.
107
+ - A new unit in the same apply never derives a slug another unit just vacated. A unit created
108
+ in a *later* apply (or via the UI) may — from then on old links open that unit.
109
+
110
+ Example — the school unit `school` → `school-primary` (**do not run on prod without the
111
+ go of whoever is responsible for production**; this is the recipe, not a pending change):
112
+
113
+ ```bash
114
+ # Unit key from the export (it is the config key, not the slug — may differ).
115
+ plexus-cli config export --env staging --tenant <slug>
116
+ # Dry-run the one-field change; the plan must show exactly one slug row:
117
+ # ~ slug units/<unitKey> school → school-primary (old URL /e/units/school no longer reaches this unit)
118
+ plexus-cli config set --env staging --tenant <slug> --target units --key <unitKey> \
119
+ --field slug --value school-primary
120
+ # Commit.
121
+ plexus-cli config set --env staging --tenant <slug> --target units --key <unitKey> \
122
+ --field slug --value school-primary --confirm
123
+ ```
124
+
125
+ **The old URL dies with no redirect.** Before committing, update everything outside the DB
126
+ that links to it: the school's own website "Bewerben" button, QR codes on printed flyers
127
+ (regenerate from the offer's QR dialog), links in mails already sent (a magic link mailed
128
+ before the change points at the old slug and 404s — send a fresh one).
129
+
130
+ Also held by value, and **not** rewritten by a slug change: a custom field whose automatic
131
+ string has a `unit`/`slug` segment stores the generated text once, so existing records keep
132
+ the old slug (no committed config uses that segment today).
133
+
134
+ **Cross-env promotes carry slugs.** `export` always emits `slug`, so `apply --from
135
+ env:staging` onto prod moves any prod unit whose slug differs from staging's (e.g. a deduped
136
+ `x-2` vs `x`). The dry run lists each as a `~ slug` row — read them before `--confirm`.
137
+
138
+ ## Idempotency / gotchas
139
+ - The writing run (`--confirm`) needs `--env` on the command line, and a production target
140
+ `--allow-prod` — decided on the resolved deployment URL, so an alias pointing at prod is prod.
141
+ The dry-run is unaffected.
142
+
143
+ - **Removing an array element does NOT need `--allow-destructive`.** The reconciler
144
+ prunes any config row absent from your snapshot on a plain
145
+ `apply --confirm`. This covers **search targets, search filters, filter options, result icons,
146
+ role-field bindings, target requirements, template placeholders** — they show as `- delete`
147
+ rows. It is **not** a migration.
148
+ - **`--allow-destructive` is only for `orphans`.** Exactly five upsert-only tables —
149
+ **display-groups, lists, custom_fields, roles, permission_groups** — are *not* auto-pruned;
150
+ removing one surfaces it as an `orphan` (labelled *would destroy*) and is deleted only with
151
+ `--confirm --allow-destructive`. This split is deliberate: dropping a field/role/group can
152
+ orphan dependent data, so it requires the explicit flag.
153
+ - **Changing the *shape* of existing rows (rename a key, change a field type) is a migration,
154
+ not an apply** — it needs a platform operator, who ships and runs the migration. `apply`
155
+ only moves config *values* into the shape that already exists.
156
+ - **Removing a filter never touches the underlying data.** E.g. dropping the `nachname` search
157
+ *filter* leaves `last_name` (a system field) and the free-text search bar (`searchTargets`)
158
+ intact — only the dedicated filter input disappears.
159
+ - **An export carries `foundation.users`**, so the export → edit → apply loop can mint
160
+ credentials for anyone in that roster who still lacks an account — see the `! no account`
161
+ line in the dry-run plan above, and `--output`/`--quiet` in `plexus-cli config apply --help`.
162
+ - This is a live-DB operation; nothing else records it. The `config/.dev/<slug>/config.json`
163
+ snapshot is your only copy — keep it out of any shared or public place.
164
+ - Cross-env shortcut: `--from env:staging` promotes staging's live config directly to prod
165
+ without a snapshot file (re-plans against prod's live state).
166
+
167
+ ## Related
168
+
169
+ - [Onboard / establish a tenant](./onboard-tenant.md) · [Wipe a tenant's config](./wipe-tenant-config.md)
170
+ - Command semantics: `plexus-cli config <verb> --help`.
@@ -0,0 +1,107 @@
1
+ # Wipe a tenant's config
2
+
3
+ **When to use:** clear a tenant's config target (and optionally its dependent data) — e.g. to
4
+ rebuild a config section from scratch, or to tear down a test tenant. Reprovision afterwards with
5
+ `config apply`.
6
+
7
+ ## Prerequisites
8
+
9
+ - `plexus-cli` installed; platform-admin token for the target env (`_platform.config.manage`).
10
+ - Know the **target id** from the registry: `plexus-cli config targets --env <env> --tenant <slug>`.
11
+ - Wipe is destructive — **rehearse on staging**, never improvise on prod.
12
+
13
+ ## Steps
14
+
15
+ ```bash
16
+ # Clear-only by default; --confirm required to actually wipe.
17
+ plexus-cli config wipe --env <env> --tenant <slug> --target <id> --confirm
18
+
19
+ # Heavy tier (the `masterdata` data target, the `foundation` config target, or --target all)
20
+ # additionally requires --allow-destructive and echoes the affected row count.
21
+ plexus-cli config wipe --env <env> --tenant <slug> --target all --confirm --allow-destructive
22
+
23
+ # Config target with dependent live data → blocked unless --cascade (which also clears the data).
24
+ plexus-cli config wipe --env <env> --tenant <slug> --target masterdata-fields --cascade --confirm
25
+ ```
26
+
27
+ ## Verify
28
+
29
+ ```bash
30
+ plexus-cli config targets --env <env> --tenant <slug> # the wiped target's count should be 0
31
+ # Reprovision exactly those rows:
32
+ plexus-cli config apply --env <env> --tenant <slug> --from snapshot:<path> --confirm
33
+ ```
34
+
35
+ ## Idempotency / gotchas
36
+ - The writing run (`--confirm`) needs `--env` on the command line, and a production target
37
+ `--allow-prod` — decided on the resolved deployment URL, so an alias pointing at prod is prod.
38
+ The dry-run is unaffected.
39
+
40
+ - **Dry run first.** Without `--confirm`, `config wipe` prints per table what it would clear
41
+ (`applications.contracts would clear 3`) and any block it would hit, and deletes nothing.
42
+ Each table is counted in its own execution, so it works where `config targets` hits the read cap.
43
+ - **Class memberships (`enrollments`) and leads (`program_interests`) are NOT application data.**
44
+ Memberships come from master data (manual or import) and live in `contract-setup` with their
45
+ classes; leads (Interessenten) are recorded before any application and live in `masterdata`.
46
+ `--target applications` leaves both alone; the dry run lists them under their own target.
47
+ - **Contracts without an application block the applications wipe.** The finance runtime tables
48
+ are cleared tenant-wide, so a contract created on a party (not by an admission) would go with
49
+ them. The wipe (and its dry run) refuses with `Blocked: N of M contracts … belong to no
50
+ application` and deletes nothing.
51
+ - **Two tiers.** Normal target = `--confirm`. **Heavy** (`masterdata` data target, `foundation`
52
+ config target, or `--target all`) additionally needs `--allow-destructive`.
53
+ - **`--target all` is the teardown.** It deletes every file the tenant's forms name (also the
54
+ uploads the ledger cannot attribute to one form — every form goes anyway), purges every
55
+ application, drains every target's
56
+ tables (manual tasks and contracts without an application included — the applications
57
+ target's blocks do not apply), then the remaining tenant tables, the tenant's open upload
58
+ tickets and the tenant row. Nothing of the tenant is left in storage but the **logo**
59
+ (letters already sent link it).
60
+ - **Files go with their rows.** Every wipe deletes the stored files only its rows held: fonts,
61
+ template files (once no template, library file, resolver pin or template run still names
62
+ them), import and export files, party files, comment attachments, signature scans. Only the
63
+ **logo** stays.
64
+ - **Master data waits for the applications.** `--target masterdata` is blocked
65
+ (`Blocked: … still has applications`) while the tenant has any application: a form may name
66
+ the same file as a party value, which cannot be proven otherwise, and once the value is gone
67
+ no wipe could find the file again. Pass `--cascade` (the applications go first) or wipe
68
+ `--target applications` before.
69
+ - **Interlock.** Wiping a CONFIG target while dependent DATA exists (e.g. `masterdata-fields`
70
+ while `values` rows reference those fields) is **blocked** with a bilingual message listing the
71
+ dependent counts — unless `--cascade`, which then also clears the data.
72
+ - **Reprovision is exact.** After a scoped config wipe, `config apply` recreates exactly those
73
+ rows (`wipe search` → `apply` round-trips). The gates sit in the backend
74
+ (fail loud); the CLI adds the UX warnings.
75
+ - **`--target applications` hard-deletes every application of the tenant and everything it
76
+ owns** — work items (+ comments, assignees, parties), team-owner bindings, feed (events,
77
+ comments, chat channels, read marks), waitlist entries, notifications about it (+ deliveries,
78
+ previews), its access-audit rows, the party-timeline activities that point at it, party-role
79
+ buckets, its form instances (`smart_form_instances`) and file artifacts, its process store, the workflow cursor, and the finance rows that flow from admissions (contracts, lines, snapshots,
80
+ signatures, payments, invoices, postings) — **and their files**: docRef uploads (single and
81
+ multi-file) whose upload-ledger row names the form, generated/uploaded file artifacts, chat
82
+ attachments, contract snapshot PDFs, signed documents. Draft party shells the forms created
83
+ go too (the soft-delete GC rule); **real master data stays**, as do **manual tasks** (no
84
+ application) and the **school setup**.
85
+ - **School setup is its own heavy target, `contract-setup`:** contract templates,
86
+ billing definitions, grants, generators and their article dependencies, ledger accounts, payment methods, classes
87
+ (`learning_groups`), portal info events and consultation slots. It is maintained in the app,
88
+ not by config-as-code — **no `config apply` brings it back**. Needs `--allow-destructive`.
89
+ - **How it runs:** a few applications per mutation through one cascade, then the target's
90
+ tables page by page — never one tenant-sized transaction.
91
+ - **What it cannot prove, so it leaves:** a file referenced by a docRef value WITHOUT an
92
+ upload-ledger row for that form (dev-seed placeholders, uploads older than the ledger) — it
93
+ might be shared. Only `--target all` takes those. Pending (never persisted) uploads are
94
+ reclaimed by the nightly orphan sweep within 7 days.
95
+ - **Verify an applications wipe:** `config targets` shows 0 for every `applications` table
96
+ (`work_items` counts only application tasks). Tables only the cascade reaches
97
+ (`notifications*`, `access_audit`, `activities`, `storage_uploads`) are not in that listing —
98
+ check them with `plexus-cli call` if needed.
99
+ - **Reprovisioning `foundation` mints new passwords.** The wipe took the tenant's users with
100
+ it, so the re-apply creates their login accounts again and prints each generated password
101
+ once. Run that re-apply with `--output creds.csv --quiet` if the terminal is shared — see
102
+ `plexus-cli config apply --help`.
103
+
104
+ ## Related
105
+
106
+ - [Update a tenant's config](./update-tenant-config.md) · [Onboard / establish a tenant](./onboard-tenant.md)
107
+ - Command semantics: `plexus-cli config wipe --help`.
@@ -0,0 +1,47 @@
1
+ # Create a named person export
2
+
3
+ **When to use:** A tenant needs a reusable XLSX export with selected person fields and the shared search filters.
4
+
5
+ ## Prerequisites
6
+
7
+ - An operator token with export access to the target tenant in each environment.
8
+ - Review the readable field IDs in `exports:metadata` separately for staging and production; IDs differ between deployments.
9
+
10
+ ## Steps
11
+
12
+ 1. List existing templates before creating a new name:
13
+
14
+ ```bash
15
+ plexus-cli call dataExportTemplates:list --env staging client=<tenant> --json
16
+ plexus-cli call exports:metadata --env staging client=<tenant> --json
17
+ ```
18
+
19
+ 2. Choose explicit `dataExportTemplateColumnInput` paths from the metadata. Prefer a small set of ordinary fields and core role/unit columns. Leave `filters=[]` to let a user choose filters at export time.
20
+
21
+ 3. Create on staging first, passing the column array as one JSON argument:
22
+
23
+ ```bash
24
+ plexus-cli call dataExportTemplates:create --kind mutation --env staging client=<tenant> name='<name>' completenessMode=all 'columns=[{"path":{"kind":"core","key":"party.roles"},"headerLabel":"Rollen","pathLabel":"Rollen"}]' --json
25
+ ```
26
+
27
+ 4. After verification, repeat against `--env prod` with production field IDs. Core paths do not need environment-specific IDs.
28
+
29
+ ## Verify
30
+
31
+ ```bash
32
+ plexus-cli call dataExportTemplates:get --env staging client=<tenant> templateId=<id> --json
33
+ plexus-cli call exports:previewSample --env staging client=<tenant> page=0 'columns=[{"path":{"kind":"core","key":"party.roles"},"headerLabel":"Rollen","pathLabel":"Rollen"}]' --json
34
+ ```
35
+
36
+ Confirm the name, `formatDefault: "xlsx"`, filters, and column paths. Select the named configuration in the browser to check that the preview and XLSX action render. Repeat the read checks on production.
37
+
38
+ ## Idempotency / gotchas
39
+
40
+ - `create` always inserts a new template. Check `list` first; update an existing template when it has the intended identity.
41
+ - `previewSample` pages through underlying parties, so a rare filter can return zero rows on one page even if later pages contain matches. A full XLSX job scans the whole tenant.
42
+ - The template is independent of tenant config snapshots; `config export/apply` does not carry it between environments.
43
+
44
+ ## Related
45
+
46
+ - `plexus-cli call --help` and `plexus-cli describe <function>` for the arguments of each function
47
+ - [Update tenant config](../config/update-tenant-config.md)
@@ -0,0 +1,69 @@
1
+ # Create + triage feedback from the CLI
2
+
3
+ **When to use:** you want to file platform-level feedback without the browser
4
+ (e.g. an agent capturing an issue), assign it to a triage admin, pull the list
5
+ of items assigned to you, and turn an item into a GitHub issue — all from `plexus-cli`.
6
+
7
+ ## Prerequisites
8
+ - `--env local|staging|prod` configured with a platform-operator token
9
+ (creating/listing need `_platform.feedback.read`; assigning needs
10
+ `_platform.feedback.manage`).
11
+ - The assignee must be feedback-assignable — see
12
+ [Make a platform admin assignable to feedback](../users/grant-feedback-assignable.md).
13
+
14
+ ## Steps
15
+ ```bash
16
+ # 1. File platform-level feedback (no screenshot — attachments are browser-only).
17
+ plexus-cli --env <env> feedback create \
18
+ --summary "Board: Spalte 'Klärung' lädt langsam" \
19
+ --tag <feedbackTagId> \
20
+ --situation "Beim Öffnen der Klärung-Spalte mit vielen Karten" \
21
+ --reason "Triage wird ausgebremst"
22
+ # → prints the new feedbackId
23
+
24
+ # 2. Assign it to a triage admin (userId, email, or @me for yourself).
25
+ plexus-cli --env <env> feedback assign <feedbackId> ada@example.com
26
+
27
+ # 3. List everything assigned to me.
28
+ plexus-cli --env <env> feedback list --all-tenants --assignee @me
29
+
30
+ # 4. File an item as a GitHub issue. If it has a screenshot, save it, host it
31
+ # yourself as an EXPIRING link, and pass the URL (the CLI uploads nothing).
32
+ plexus-cli --env <env> feedback screenshot <feedbackId> --out ./feedback.webp
33
+ # → upload ./feedback.webp with your environment's upload tool, presigned,
34
+ # e.g. 7 days; screenshots can show customer data — never a permanent public URL
35
+ plexus-cli --env <env> feedback to-issue <feedbackId> --type bug \
36
+ --screenshot-url '<presigned-url>' # or: --no-screenshot
37
+ ```
38
+
39
+ ## Verify
40
+ ```bash
41
+ # The item shows the assignee and appears under the @me filter:
42
+ plexus-cli --env <env> feedback list --all-tenants --assignee @me --json
43
+ # Eligible assignees (must include your target):
44
+ plexus-cli --env <env> feedback admins
45
+ ```
46
+
47
+ ## Idempotency / gotchas
48
+ - **`feedback create` is platform-level only** — the item is owned by the reserved
49
+ `_platform` "Plattform" tenant, not a client tenant. Use the in-app feedback
50
+ button to file *tenant-scoped* feedback.
51
+ - **No screenshot/attachments on `feedback create`** — they require a browser upload;
52
+ both are optional backend-side, so omitting them is a valid submission.
53
+ - **`feedback to-issue` never drops a screenshot silently:** an item with one is refused
54
+ until you pass `--screenshot-url <https-url>` or `--no-screenshot`; a URL for an item
55
+ without a screenshot is refused too. `--dry-run` applies the same rule.
56
+ - **The issue embeds the link you pass.** A presigned link expires, and the image in the
57
+ issue goes with it — that is the point for customer data; the item itself keeps the
58
+ original (`feedback screenshot` fetches it again any time).
59
+ - **`feedback screenshot`** refuses an existing `--out` file (`--force` overwrites) and an
60
+ item without a screenshot; it prints the content type (staging stored WebP when checked).
61
+ - Tags replace the old fixed category field. Use repeatable `--tag <feedbackTagId>`
62
+ on create, or `feedback tag <feedbackId> <feedbackTagId>` afterwards.
63
+ - `--assignee @me` (and `me`) resolves to the calling operator via `whoAmI`.
64
+ `feedback assign` fails loud if the target lacks `_platform.feedback.assign`.
65
+ - `--assignee` on `feedback list` requires `--all-tenants` (assignee is platform-only).
66
+
67
+ ## Related
68
+ - [Make a platform admin assignable to feedback](../users/grant-feedback-assignable.md)
69
+ - [Manage feedback tags + bulk-tag items](./manage-feedback-tags.md)
@@ -0,0 +1,57 @@
1
+ # Manage feedback tags (incl. visibility) and bulk-tag items
2
+
3
+ **When to use:** curate the global feedback tag catalog (create / rename / recolor /
4
+ set public-vs-platform visibility / delete), or attach a tag across many feedback
5
+ items at once.
6
+
7
+ ## Prerequisites
8
+ - `--env local|staging|prod` selected (the environment as configured for your CLI, see `plexus-cli --help`).
9
+ - Catalog edits need platform `_platform.feedback.tags`; attach/detach needs board
10
+ access to the item (the CLI authenticates as a platform admin, so both are covered).
11
+ - `jq` for the bulk loop.
12
+
13
+ ## Steps
14
+ ```bash
15
+ # Inspect the catalog (id, name, color, scope). Shows platform-only tags too.
16
+ plexus-cli --env prod feedback tags list
17
+
18
+ # Create — public (default) or platform-only (triage tags like "Unklar"/"Fehlt").
19
+ plexus-cli --env prod feedback tags create --allow-prod "Rechte" --color 5319e7
20
+ plexus-cli --env prod feedback tags create --allow-prod "Unklar" --color d4c5f9 --visibility platform
21
+
22
+ # Rename / recolor / re-scope / delete (delete cascades the item links).
23
+ plexus-cli --env prod feedback tags rename --allow-prod <tagId> "Konfiguration"
24
+ plexus-cli --env prod feedback tags recolor --allow-prod <tagId> fbca04
25
+ plexus-cli --env prod feedback tags visibility --allow-prod <tagId> platform # or: public
26
+ plexus-cli --env prod feedback tags delete --allow-prod <tagId>
27
+
28
+ # Attach / detach a tag on one item.
29
+ plexus-cli --env prod feedback tag --allow-prod <feedbackId> <tagId>
30
+ plexus-cli --env prod feedback untag --allow-prod <feedbackId> <tagId>
31
+ ```
32
+
33
+ ### Bulk-tag: attach one tag to every matching item
34
+ ```bash
35
+ TAG=<tagId>
36
+ plexus-cli --env prod feedback list --all-tenants --text "import" --json \
37
+ | jq -r '.[].id' \
38
+ | while read -r id; do plexus-cli --env prod feedback tag --allow-prod "$id" "$TAG"; done
39
+ ```
40
+
41
+ ## Verify
42
+ ```bash
43
+ plexus-cli --env prod feedback tags list # scope column reflects the change
44
+ plexus-cli --env prod feedback list --all-tenants --tag <tagId> --json | jq length
45
+ ```
46
+
47
+ ## Idempotency / gotchas
48
+ - `feedback tag`/`untag` are idempotent (re-running is a no-op).
49
+ - A non-platform user can attach only **public** tags; the CLI is platform-authed so
50
+ it can attach platform-only ones too.
51
+ - `feedback list` filters in-memory over rank-paginated pages, so a heavily filtered
52
+ query may need `--limit` raised to reach older items (iterate to exhaustion).
53
+ - `tags delete` is destructive: it detaches the tag from every item first.
54
+
55
+ ## Related
56
+ - [Create + triage feedback](./create-and-triage-feedback.md) (incl. `feedback to-issue`) ·
57
+ `feedback link/unlink/links` to cross-reference two feedback items · the board UI's "Tags verwalten" dialog does the same edits.
@@ -0,0 +1,36 @@
1
+ # Change a contract grant
2
+
3
+ Use a line grant for a concession on one contract line. Use an aggregate grant for
4
+ an agreed reduction across eligible contract lines with one combined floor. Plans
5
+ only assemble articles. Prices selected through pricing attributes stay in the tree.
6
+
7
+ ```bash
8
+ plexus-cli --env staging --tenant test contract grant add \
9
+ --line <lineId> --template <templateKey> --from 2026-11-01 \
10
+ --evidence '{"decision":"Finance approved"}'
11
+
12
+ plexus-cli --env staging --tenant test contract grant add-aggregate \
13
+ --contract <contractId> --template <templateKey> --amount 380 --floor 150 \
14
+ --period month --from 2026-11-01 \
15
+ --evidence '{"decision":"Income assessment approved"}'
16
+
17
+ plexus-cli --env staging --tenant test contract grant end <grantId> --to 2027-07-31
18
+
19
+ plexus-cli --env staging --tenant test contract grant replace <lineGrantId> \
20
+ --template <newTemplateKey> --from 2026-12-01 \
21
+ --evidence '{"decision":"Updated concession"}'
22
+ ```
23
+
24
+ `add` also accepts negotiated line terms: `--name`, `--reason`, and exactly one of
25
+ `--percent` or `--amount`. `replace` replaces line grants. To change an aggregate
26
+ agreement, end it and apply the new explicit terms from the following period.
27
+
28
+ Finance enters income reductions as absolute amounts. The engine caps the granted
29
+ reduction so the eligible total cannot fall below the agreed floor. Annual amounts
30
+ and floors are entered explicitly; monthly terms are never multiplied by twelve.
31
+ Only eligible lines with the matching price period participate. Supporting evidence
32
+ is required. Backdated changes to already booked periods are refused.
33
+
34
+ Template edits do not change existing contract grants. Their target lines, rule,
35
+ floor, period and evidence were agreed when applied. Contract snapshots preserve
36
+ signed evidence; booked charges preserve their pricing explanation and accounting.
@@ -0,0 +1,33 @@
1
+ # Record a missing historical SEPA submission date
2
+
3
+ **When to use:** The dashboard reports a legacy debit batch without a recorded
4
+ submission date. Use the original transmission receipt, not the execution date.
5
+
6
+ ## Prerequisites
7
+
8
+ - Tenant-wide finance permission and the original bank transmission evidence.
9
+ - Inspect the target environment before writing. Staging/prod changes require
10
+ the operator's explicit approval; this recipe is not authorization.
11
+
12
+ ## Steps
13
+
14
+ ```sh
15
+ plexus-cli --env local --tenant test config show
16
+ plexus-cli --env local --tenant test call paymentBatches:recordSubmissionDate --kind mutation client=test batchId=<batch-id> submittedDate=2026-08-31
17
+ ```
18
+
19
+ Use the actual receipt's date and intended tenant/environment. Ordinary new
20
+ submissions already record this date and need no repair.
21
+
22
+ ## Verify
23
+
24
+ ```sh
25
+ plexus-cli --env local --tenant test call financeDashboard:overview client=test
26
+ ```
27
+
28
+ The return count and denominator appear in the original submission month.
29
+
30
+ ## Idempotency / gotchas
31
+
32
+ An identical date can be replayed. A conflicting date, another tenant's batch,
33
+ a draft batch or a date after execution is refused. Nothing books money.
@@ -0,0 +1,54 @@
1
+ # List a tenant's Interessenten (open leads)
2
+
3
+ Inspect the CRM funnel's `interesse` stage from the CLI — every OPEN lead
4
+ (program interest) for a tenant, newest first, with the person, Angebot, Stufe
5
+ and Lauf. Read-only; backs ops/debug ("why isn't this lead showing /
6
+ converting?").
7
+
8
+ There is no stored lead status: a lead is CONVERTED iff an application
9
+ references it (`applications.leadId`) — converted leads appear as Bewerbungen,
10
+ not here — and withdrawn/declined leads are deleted outright.
11
+
12
+ ## Command
13
+
14
+ ```bash
15
+ # All open leads for the active tenant (table)
16
+ plexus-cli interessenten list --env local --tenant <slug>
17
+
18
+ # Rich JSON
19
+ plexus-cli interessenten list --env local --tenant <slug> --json
20
+ ```
21
+
22
+ - `--tenant <slug>`: overrides the config default. The local dev seed's tenant
23
+ slug is **`test`** (the committed `test` template); `grundschule-havelland` is
24
+ a *unit*, not the tenant.
25
+ - `--json`: rich shape (`{ lead, personDisplayName, programName, runName,
26
+ stageName }`); default is a compact table.
27
+
28
+ ## Example output (table)
29
+
30
+ ```
31
+ name angebot stufe lauf
32
+ Lia Busch Grundschule Klassenstufe 6 Schuljahr 2026/27
33
+ Noah Kaiser Grundschule Klassenstufe 5 Schuljahr 2026/27
34
+ ```
35
+
36
+ ## How it works
37
+
38
+ Wraps the gated query `api.leads.listForManagement(client)`, which requires the
39
+ **`bewerbungsmanagement.applications.read`** permission (unlike the
40
+ membership-stub `leads.listForTenant`). Leads are real Stammdaten persons with
41
+ the tenant's configured Interessent (lead) role — the Bewerber role is attached
42
+ only at conversion — so a lead here is the same person you'll find in the
43
+ Personen catalog. The free-text note lives on the person as the `notiz` custom
44
+ field, not on the lead row.
45
+
46
+ ## Verify / gotchas
47
+
48
+ - **`Unknown tenant "<slug>"`** → wrong `--tenant`. Use the tenant slug (`test`
49
+ locally), not a unit slug.
50
+ - **`Missing required permission: bewerbungsmanagement.applications.read`** →
51
+ the CLI token's user lacks admission read in that tenant. Grant it (or use a
52
+ platform-admin token).
53
+ - Empty list on a fresh deployment → seed hasn't run, or every lead was
54
+ converted (now listed as Bewerbungen) or withdrawn (deleted).
@@ -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`.