@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,117 @@
1
+ # Attach a blank template (Vorlage) to a form field
2
+
3
+ **When to use:** a school wants to offer a blank document on an upload field — a
4
+ reference form, a GDPR consent, a health record — that the family downloads,
5
+ fills in or signs, and uploads back at the same field. Or a template changed and
6
+ every form that offers it should show the new version.
7
+
8
+ The file lives in the tenant's **file library** (`tenant_files`, UI: Daten →
9
+ Dateien) under a stable **key**. A form field names the key only; label, file
10
+ name and type are read from the library. Replacing the file (same key) updates
11
+ every form at once — no republish.
12
+
13
+ ## Prerequisites
14
+
15
+ - `plexus-cli` installed; a platform-admin token with `_platform.config.manage`
16
+ for the target env (listing needs `_platform.config.read`).
17
+ - The `files` commands and the `fileKey` attachment shape must be **live** on
18
+ the target env before an offer that uses them is published (a platform
19
+ operator rolls a release out to staging, then prod). An offer published before
20
+ that fails with `[config.attachment-source]` or `[form.unknown-attachment-file]`.
21
+ - Allowed types: PDF, DOCX, DOC, ODT, XLSX, JPG, PNG; max 20 MB.
22
+
23
+ ## Steps
24
+
25
+ 1. **Put the file into the library** (create — a new key needs `--label` AND
26
+ `--label-en`: the label is shown in the reader's language):
27
+
28
+ ```bash
29
+ plexus-cli --env staging --tenant test files put referenzformular ./Referenzformular.pdf \
30
+ --label "Vorlage: Referenzformular" --label-en "Template: reference form"
31
+ ```
32
+
33
+ `put` on an existing key **replaces** the file (keeps the key and the label
34
+ unless `--label` / `--label-en` is given). `--label-en` alone translates the
35
+ stored German; a German change over a real English is refused without
36
+ `--label-en`. Identical bytes and label are a no-op.
37
+
38
+ 2. **Attach it to the field** in the offer bundle — a fresh export of the live
39
+ offer in a scratch directory of your own (production is the source of truth
40
+ for a real tenant's offer, so always start from a fresh export; see
41
+ [edit-live-tenant-offer](../offer/edit-live-tenant-offer.md)):
42
+
43
+ ```bash
44
+ WORK=$(mktemp -d) # a scratch directory
45
+ plexus-cli --env prod --tenant <slug> offer export --slug <offer-slug> --out $WORK/<offer-slug>
46
+ plexus-cli offer form field attach --dir $WORK/<offer-slug> \
47
+ --key kind.referenz --file-key referenzformular
48
+ # an inline link in the label instead of a download chip:
49
+ # --render inlineLink --label "Referenzformular"
50
+ ```
51
+
52
+ 3. **Validate and publish** the bundle as usual (`offer validate`, `offer plan`,
53
+ `offer apply --publish`). Publish checks the key against the target tenant's
54
+ library.
55
+
56
+ ## Declarative path (config-as-code)
57
+
58
+ The library is part of the tenant config (`files`):
59
+
60
+ ```jsonc
61
+ "files": [
62
+ {
63
+ "key": "referenzformular",
64
+ "label": { "de": "Vorlage: Referenzformular" },
65
+ "filename": "Referenzformular.pdf",
66
+ "mimeType": "application/pdf",
67
+ "sha256": "…", // recomputed from `path` by the CLI
68
+ "sizeBytes": 48213, // recomputed from `path` by the CLI
69
+ "path": "files/referenzformular/Referenzformular.pdf"
70
+ }
71
+ ]
72
+ ```
73
+
74
+ - `config export` downloads every file to `files/<key>/<filename>` beside
75
+ `config.json` and writes `path`. Swap a file on disk and `config apply --from
76
+ snapshot:…` uploads it (the file on disk decides the hash).
77
+ - `apply` uploads **only** files whose hash the target does not hold; a dry run
78
+ uploads nothing and lists them as `~ file files/<key>`.
79
+ - A file absent from `files` is reported as an **orphan** (`- orphan
80
+ foundation/tenant_files`) and removed only with `--allow-destructive` — then
81
+ still refused with `[file/inUse]` while any form version (archived included)
82
+ names it. A file uploaded on the Daten page is therefore never lost to an
83
+ older snapshot.
84
+ - A dry run also names changed files whose bytes the source cannot supply
85
+ (`NO bytes in this source`) — the commit would fail `[file/contentMissing]`.
86
+ - `--from env:<src>` downloads changed files from the source env and uploads them
87
+ to the target.
88
+
89
+ ## Verify
90
+
91
+ ```bash
92
+ plexus-cli --env staging --tenant <slug> files list # key, file, sha256, usedBy
93
+ ```
94
+
95
+ Open a draft application on the portal: the field shows a „Vorlage …" download
96
+ chip (or, for a document field, a „Vorlage" button inside the upload box). A
97
+ key the library no longer holds renders as **„Vorlage fehlt (<key>)"** — never
98
+ as a missing chip.
99
+
100
+ ## Gotchas
101
+
102
+ - **Remove:** `plexus-cli files remove <key>` is refused while a form uses the
103
+ key; the error names `offer rN (status) / form / field` for each use. Detach
104
+ first (`offer form field detach --key … --file-key …`), republish, and note that
105
+ **archived** offer versions still count — applications pin them.
106
+ - Replacing or removing a file deletes its previous bytes in the same step (a
107
+ blank template has no audit value; the family's signed upload is the record).
108
+ Exception: a file the migration adopted from an old form attachment shares its
109
+ bytes with a document template (`ownsStorage: false`) — those go only once no
110
+ document template, resolver pin or template run of the tenant names them.
111
+ - The raw `storageId` attachment shape is retired; a stale bundle carrying one
112
+ fails publish with `[config.attachment-source]` naming the fix.
113
+
114
+ ## Related
115
+
116
+ - UI: Daten → Dateien (upload, replace, delete).
117
+ - [Update a tenant's config](./update-tenant-config.md).
@@ -0,0 +1,121 @@
1
+ # Scaffold a brand-new tenant config from nothing
2
+
3
+ **When to use:** stand up a brand-new customer (tenant) from *nothing* — no snapshot,
4
+ no `.dev` export, no other environment to promote from. `config init` writes the
5
+ smallest valid config to disk; you then `config apply` it. If a config source already
6
+ exists (a `.dev` export / a tenant in another env), use
7
+ [Onboard / establish a tenant](./onboard-tenant.md) instead — that path skips the
8
+ scaffold.
9
+
10
+ ## Prerequisites
11
+ - **Env:** target with `--env <local|staging|prod>`. Tenant creation is implicit in
12
+ `config apply` (there is no separate "create tenant" verb). `config init` itself is
13
+ **offline** — no deployment, no auth.
14
+ - **Auth (for the apply, not the init):** a platform-admin token (`config apply` is
15
+ gated on `_platform.config.manage`). Confirm with `plexus-cli --env <env> whoami`.
16
+ The local admin-key fallback has no platform identity and is rejected — drive
17
+ `--env local` with a token too. A tenant admin's token cannot do this: a
18
+ tenant-scoped apply refuses a tenant that does not exist yet
19
+ ([CLI access for a tenant admin](../users/cli-access-for-tenant-admin.md)).
20
+ - **Tooling:** an installed, current `plexus-cli` (`plexus-cli --help`).
21
+ - **Prod:** every writing run against prod needs `--allow-prod` (`config apply`,
22
+ `users provision`); without it the CLI refuses.
23
+
24
+ ## Decide before you apply
25
+
26
+ Settle these with the school first — each one is a later playbook, but the answers
27
+ belong to onboarding:
28
+
29
+ | Decision | Default of the scaffold | Where it is set |
30
+ |---|---|---|
31
+ | Slug + display name | — (`--tenant`, `--name`) | step 1; the slug is the identity, never rename it |
32
+ | Modules the school bought | **all** grantable module permissions | trim `tenant.permissions` in the scaffold before step 4 |
33
+ | „Bewerbung" or „Anmeldung" | Bewerbung (no `terms` in the scaffold) | [Set a school's terms](./set-tenant-terms.md) |
34
+ | Which mail families get | `all` („Alle E-Mails“) | [Mail level](../notifications/mail-level.md) |
35
+ | Portal branding (colors, logo, fonts) | neutral | [Set tenant branding](./set-tenant-branding.md) |
36
+ | Payments / e-signature | none | [Stripe](../stripe/connect-a-school.md) · [Signing](../signing/connect-a-school.md) |
37
+
38
+ **Look at an existing tenant as a reference.** Export one you can reach into a private
39
+ scratch dir and compare:
40
+ `plexus-cli --env prod config export --tenant <existing slug> --out <scratch>/ref.json`.
41
+
42
+ ## Steps
43
+ ```bash
44
+ # 1. Scaffold a minimal, valid tenant config (offline — no deployment, no auth).
45
+ plexus-cli config init --tenant <slug> --name "<Display Name>" \
46
+ --out config/.dev/<slug>/config.json # --out defaults to this path
47
+
48
+ # 2. (Optional) edit config/.dev/<slug>/config.json — add custom fields, roles,
49
+ # extra units/legal entities, a `source` block if this tenant imports data, etc.
50
+ # The scaffold has NO `source` block: a tenant that never imports needs none.
51
+
52
+ # 3. Dry-run the apply (shows the create/update plan; writes nothing).
53
+ plexus-cli --env <env> config apply --tenant <slug> \
54
+ --from snapshot:config/.dev/<slug>/config.json
55
+
56
+ # 4. Commit it.
57
+ plexus-cli --env <env> config apply --tenant <slug> \
58
+ --from snapshot:config/.dev/<slug>/config.json --confirm
59
+
60
+ # 5. Provision the generic admin login (separate auth plane — see related playbook).
61
+ # Convention: admin@<short>.plexus.swop.schule, name "<Short> Administration",
62
+ # so a platform operator always has one login per school that belongs to no person.
63
+ plexus-cli --env <env> --tenant <slug> users provision admin@<short>.plexus.swop.schule \
64
+ --name "<Short> Administration" --group tenant_admin --output creds.csv --quiet
65
+ ```
66
+
67
+ Write `config/.dev/…` and `creds.csv` to a private scratch dir — never put a real
68
+ tenant's config or a password into a shared or public place. Hand the credentials over
69
+ in the three-line format of
70
+ [Add a tenant admin](../users/add-tenant-admin.md#hand-the-credentials-over--always-this-format).
71
+ Staff of the school get their own accounts the same way (their address, their name);
72
+ the generic admin is not shared with them.
73
+
74
+ **Created for you, not by you:** the apply also provisions the tenant's test applicant
75
+ `test@example.com` („Test Bewerber"), which then shows up in `users list` with the new
76
+ slug. That is intended — see [Use a tenant's test account](../users/use-test-account.md).
77
+
78
+ The scaffold emits the smallest config `apply` accepts: the tenant block (all
79
+ grantable module permissions + `defaultApplicantGroupKey`), one school operator
80
+ (legal entity), **one pedagogy unit** (the school offers link to), the `tenant_admin`
81
+ + `applicant` permission groups, and one display group (required — the system
82
+ identity fields attach to it). No users (provision them in step 5) and no `source`.
83
+ It is built by the same code that builds the platform's demo tenant, so the
84
+ scaffolded permission sets can never drift from it.
85
+
86
+ ## Verify
87
+ ```bash
88
+ # The tenant + its foundation now exist:
89
+ plexus-cli --env <env> --tenant <slug> config targets # foundation rows > 0
90
+ plexus-cli --env <env> --tenant <slug> config export # round-trips the unit, groups
91
+ plexus-cli --env <env> tenants # <slug> is listed
92
+ ```
93
+
94
+ Then, in this order: the decisions above (terms, mail level, branding, payments) →
95
+ offers on the pedagogy unit: `offer init --unit <unit key> --title … --title-en … --responsible <staff configKey>` →
96
+ `offer apply --publish` (see `plexus-cli offer --help` and
97
+ [Publish an offer bundle](../offer/publish-offer-bundle.md)).
98
+
99
+ ## Idempotency / gotchas
100
+ - **`config init` is offline and overwrites `--out`** — re-running regenerates the
101
+ same skeleton from `--tenant`/`--name`; it does not touch any deployment.
102
+ - **`config apply` is idempotent** — re-running reconciles to the config (no
103
+ duplicate tenant). Dry-run first; only `--confirm` writes.
104
+ - **`source` is optional.** Omit it for a non-importing tenant; add it only when
105
+ wiring a master-data import. Import code fails loud at import time if a source is
106
+ needed but absent — there is no fake-source fallback.
107
+ - **Teardown:** only `config wipe --target all --confirm --allow-destructive` deletes
108
+ the tenant **identity row** — after purging its applications and draining every
109
+ target, files included (only the logo stays: sent mail links it). `--target foundation` (even with `--cascade`) clears
110
+ child tables + dependent data but leaves the tenant row and sibling config targets —
111
+ by design (`tenants` is excluded from every wipe target). The CLI prints a note when
112
+ you wipe a non-`all` target so this is never a surprise.
113
+ - Staff accounts are a **separate plane** (Better Auth) — the config never creates
114
+ logins; provision them with `users provision` (step 5).
115
+
116
+ ## Related
117
+ - [Onboard / establish a tenant](./onboard-tenant.md) — establish from an existing
118
+ `.dev` export / env (the non-scaffold path).
119
+ - [Update a tenant's config](./update-tenant-config.md) · [Wipe a tenant's config](./wipe-tenant-config.md)
120
+ - [Add a tenant admin](../users/add-tenant-admin.md) — the credentialed login (step 5).
121
+ - Command reference: `plexus-cli config <verb> --help`.
@@ -0,0 +1,71 @@
1
+ # Onboard / establish a tenant
2
+
3
+ **When to use:** stand up a real tenant's config-as-code in an environment for the first time
4
+ (or refresh it). A real tenant's config lives in the database (DB-authoritative) and is
5
+ established from a snapshot or another environment — never from built-in seed data.
6
+
7
+ ## Prerequisites
8
+
9
+ - `plexus-cli` installed; platform-admin token for the target env (`_platform.config.manage`).
10
+ - A config **source** for the tenant, one of:
11
+ - a `config export` from another env (default path `config/.dev/<slug>/config.json`),
12
+ - an environment that already has the tenant (promote with `--from env:<srcEnv>`).
13
+ A real tenant's config is kept nowhere but its deployment — prod is its source of truth, so a
14
+ refresh of staging comes FROM prod, never the other way round.
15
+ - The `--tenant` value must be the **slug embedded in the snapshot** — apply rejects a mismatch.
16
+
17
+ ## Steps
18
+
19
+ ```bash
20
+ # Establish from a snapshot (dry-run first, then commit).
21
+ plexus-cli config apply --env <env> --tenant <slug> \
22
+ --from snapshot:config/.dev/<slug>/config.json
23
+ plexus-cli config apply --env <env> --tenant <slug> \
24
+ --from snapshot:config/.dev/<slug>/config.json --confirm
25
+
26
+ # OR copy a tenant that already exists on prod to staging (re-plans against the target's live state).
27
+ plexus-cli config apply --env staging --tenant <slug> --from env:prod --confirm
28
+ ```
29
+
30
+ **If the source carries `foundation.users`, the committing apply hands you passwords.**
31
+ Every declared user without a login account gets one minted right here, printed once
32
+ (`+ account <email> password: …`) — the server keeps only the hash. Redirect them instead
33
+ of running this in a shared terminal:
34
+
35
+ ```bash
36
+ plexus-cli config apply --env <env> --tenant <slug> \
37
+ --from snapshot:<path> --confirm --output creds.csv --quiet
38
+ ```
39
+
40
+ The dry-run above mints nothing (`--output` is inert and creates no file); it lists the
41
+ same users as `! no account <email>`, so you know beforehand whether the commit will emit
42
+ secrets.
43
+
44
+ After the config exists, iterate it live with [Update a tenant's config](./update-tenant-config.md),
45
+ run the real import, and publish offers via `plexus-cli offer apply`.
46
+
47
+ ## Verify
48
+
49
+ ```bash
50
+ plexus-cli config targets --env <env> --tenant <slug> # registry + live per-tenant row counts
51
+ plexus-cli config export --env <env> --tenant <slug> # full reconstructed config
52
+ ```
53
+
54
+ ## Idempotency / gotchas
55
+
56
+ - `apply` is **idempotent and additive on config tables** — it never wipes the data plane
57
+ (parties/values/applications). Re-running an establish is safe.
58
+ - **Deploys do not establish real tenants.** A staging/prod deploy applies only the
59
+ **template tenant** (`test`) — establishing a real tenant is this manual step.
60
+ - The **import mapping is DB-authoritative**: `custom_fields.sourceColumns` ships in the config
61
+ and is read by the import worker. The import **fails loud** if a tenant has fields but none
62
+ carry `sourceColumns` — i.e. the worker was deployed before the tenant was (re)applied:
63
+ re-run this apply.
64
+ - A structural schema change is a **migration** that a platform operator runs before the
65
+ apply — not part of onboarding, and not something the CLI does.
66
+
67
+ ## Related
68
+
69
+ - [Update a tenant's config](./update-tenant-config.md) · [Wipe a tenant's config](./wipe-tenant-config.md)
70
+ - [Scaffold a brand-new tenant config](./init-tenant.md) — when no source exists yet.
71
+ - Command semantics: `plexus-cli config apply --help`.
@@ -0,0 +1,99 @@
1
+ # Rename, add, reorder or retire a program stage (Klassenstufe)
2
+
3
+ **Use it to** change an offer's Klassenstufen — rename "Kindergarten Jahr 1" to
4
+ "Nursery 1", add a level, change the order, or retire one — without leaving a
5
+ second, stale level behind and without cutting applications loose.
6
+
7
+ ## The model in one paragraph
8
+
9
+ A stage is identified by the **key** in `bundle.yaml → program.stages[].key`, never
10
+ by its name. The stored key is `<program.key>-stufe-<key>`. Applications, capacity
11
+ cells and waitlists point at the stage row by id, so a rename that keeps the key
12
+ keeps all of them. `offer apply` reconciles by key: same key → update, new key →
13
+ insert, removed key → archive (refused while an active application sits on it),
14
+ re-listed archived key → restore. Array order = display order.
15
+
16
+ ## Declaratively (bundle — preferred for anything you want reproducible)
17
+
18
+ ```bash
19
+ # 1. Start from what is live — prod is the source of truth for a real tenant, so
20
+ # work on a fresh export in a scratch directory (export writes the bare keys back)
21
+ WORK=$(mktemp -d)
22
+ plexus-cli --env prod --tenant <slug> offer export --slug <offer-slug> --out $WORK/<offer-slug>
23
+ ```
24
+
25
+ ```yaml
26
+ # 2. Edit program.stages — KEEP the key of every level you rename. A name is
27
+ # German AND English; changing only the English is a rename too.
28
+ program:
29
+ stages:
30
+ - key: kindergarten-jahr-1 # existing level, new name
31
+ name: { de: Nursery 1, en: Nursery 1 }
32
+ - key: kindergarten-jahr-2
33
+ name: { de: Nursery 2, en: Nursery 2 }
34
+ - key: early-years-1 # new level
35
+ name: { de: Early Years 1, en: Early Years 1 }
36
+ ```
37
+
38
+ ```bash
39
+ # 3. Offline check, then look at the stage rows of the plan
40
+ plexus-cli offer validate --dir $WORK/<offer-slug>
41
+ plexus-cli --env prod offer plan --dir $WORK/<offer-slug>
42
+ # ~ stage:kindergarten-jahr-1 ~stage kindergarten-jahr-1 ("Kindergarten Jahr 1" → "Nursery 1", en "Kindergarten year 1" → "Nursery 1")
43
+ # + stage:early-years-1 +stage early-years-1 (Early Years 1)
44
+
45
+ # 4. Apply
46
+ plexus-cli --env prod offer apply --allow-prod --dir $WORK/<offer-slug>
47
+ ```
48
+
49
+ ## Imperatively (one-off, no bundle)
50
+
51
+ ```bash
52
+ # --level takes the level's KEY; a German rename always needs --name-en (the
53
+ # English is never copied from the German, and a name without English is refused).
54
+ # Against prod: --env prod … --allow-prod
55
+ plexus-cli --env <env> offer level rename --slug <offer-slug> --level kindergarten-jahr-1 --name "Nursery 1" --name-en "Nursery 1"
56
+ plexus-cli --env <env> offer level add --slug <offer-slug> --name "Early Years 1" --name-en "Early Years 1" --key early-years-1
57
+ plexus-cli --env <env> offer level remove --slug <offer-slug> --level early-years-1
58
+ plexus-cli --env <env> offer level restore --slug <offer-slug> --level early-years-1
59
+ ```
60
+
61
+ After imperative changes, **export the bundle again before the next `apply`**:
62
+ otherwise the old bundle still lists the old state, and applying it undoes your
63
+ changes (renames them back, archives the added levels).
64
+
65
+ ## Verify
66
+
67
+ ```bash
68
+ plexus-cli --env prod offer plan --dir $WORK/<offer-slug> # no stage rows = in sync
69
+ plexus-cli --env prod offer show --slug <offer-slug> # levels in order
70
+ ```
71
+
72
+ ## Gotchas
73
+
74
+ - **Changing a key is not a rename.** A new key inserts a new level and archives the old
75
+ one. If the old one has active applications, the apply is refused — which is what you
76
+ want: those applications would stay on a level nobody sees.
77
+ - **`[stage/hasActiveApplications]`** — the level you dropped (or `level remove`d) still
78
+ has active applications. `offer plan` shows it in advance: `-stage … — REFUSED: n
79
+ active application(s)`. Rename it instead, or close those applications first. No
80
+ stage was changed: the reconcile is one transaction. (The manifest draft and the
81
+ unit/program link apply wrote before it stay — unpublished; don't publish by hand.)
82
+ - **A level moves to another offer** (e.g. Grade 12 moves from Primary to Secondary). Keys are per
83
+ program, so the target offer gets a *new* row and the source offer's row is archived.
84
+ Nothing re-points an application across programs. While applications are active on
85
+ the source row, keep the level listed in the source bundle and close its cell
86
+ (`offer capacity set --slug … --run … --level … --close`), then drop it in a later apply.
87
+ - **`[stage/hasOpenLeads]`** is a warning, not a refusal: open Interessenten keep
88
+ pointing at the archived level and cannot be converted until they are moved to an
89
+ active level.
90
+ - **`[stage/duplicateKey]` from apply but not from validate** — the stored key is taken
91
+ by a stage of *another* program in the tenant (keys are unique per tenant). Choose a
92
+ different key.
93
+ - **A `-stage` row showing a key like `kindergarten:vorschule:lz3k9`** — a legacy stage
94
+ created in the master-data admin before stage keys existed, keyed outside the `<program>-stufe-`
95
+ scheme. No bundle key can address it, so `apply` archives it (through the guard).
96
+ `offer export` refuses such a row with `[stage/foreignKey]`. No CLI verb re-keys a
97
+ stage today — raise it with the platform operator instead of hand-editing around it.
98
+ - A bundle **without** `program.stages` leaves the levels alone; `stages: []` archives
99
+ all of them (guarded).
@@ -0,0 +1,150 @@
1
+ # Set a tenant's external-portal branding
2
+
3
+ **When to use:** give a tenant's public `/e/**` Bewerbungsportal its own white-label
4
+ look — brand colors, corner radius, logo size, header style, and font families, plus an
5
+ uploaded logo and uploaded font files — on staging and/or prod, without a reseed.
6
+
7
+ ## Prerequisites
8
+
9
+ - `plexus-cli` installed and current (`plexus-cli --help`).
10
+ - A token configured for each target env (`plexus-cli --env <env> config show` prints
11
+ the resolved connection): a platform-admin token holding `_platform.config.manage` (the
12
+ same right that gates `config apply`), or — for your OWN tenant only — a tenant
13
+ admin's token holding `administration.config.manage` (see
14
+ [CLI access for a tenant admin](../users/cli-access-for-tenant-admin.md)). Such a
15
+ token changes branding **only through `config apply`** (branding is one of the few
16
+ `tenant.*` settings it may change); every `plexus-cli branding …` command needs the
17
+ platform right and refuses it. A local admin key without a user identity does **not**
18
+ satisfy this gate.
19
+ - The `plexus-cli branding …` commands must be **live on the target env** first (they
20
+ call backend functions of the same release). A platform operator rolls a release out
21
+ to staging, then prod; only then run the CLI.
22
+ - The tenant **slug** (e.g. `test`). Always **stage on staging first**, verify in the
23
+ browser, then promote to prod.
24
+
25
+ ## The two halves of branding
26
+
27
+ | Part | Where it lives | How to set it |
28
+ | --- | --- | --- |
29
+ | **Theme tokens** — colors, radius, logoSize, bannerDensity, filledHeader, websiteUrl, font **families** | `tenant_branding` row columns; config-portable (exported in `tenant.branding`) | `plexus-cli branding set-theme` (surgical) **or** `config apply` (whole-config) |
30
+ | **Logo** | `_storage` id column (a single `primary` slot); carried by config **by hash** (`tenant.brandingAssets.logo`) | `plexus-cli branding set-logo` (one-off upload) **or** `config apply` (bytes from the snapshot / source env) |
31
+ | **Font files** (WOFF2, one per slot `heading` / `body`) | `_storage` id columns; carried by config **by hash** (`tenant.brandingAssets.fontHeading` / `fontBody`) | `plexus-cli branding set-font --slot heading\|body --file <woff2>` **or** `config apply` |
32
+
33
+ A `storageId` is deployment-local, so config never carries it. It carries each uploaded
34
+ file like a library file: `{ sha256, sizeBytes, mimeType }` under `tenant.brandingAssets`, and
35
+ `config export` downloads the bytes beside `config.json` as `branding/<slot>.<ext>`
36
+ (`path` in the entry). `config apply --from snapshot:…` or `--from env:<src>` uploads
37
+ only the files whose hash the target does not hold; the dry run prints
38
+ `~ brand branding/logo content differs — would upload`. A missing slot — or a snapshot
39
+ without the block — leaves the target's file untouched. A replaced **logo** file is never
40
+ deleted (sent mail links it); a replaced **font** file is.
41
+ Fonts are served from the app's own Convex origin — **never** point a browser at an
42
+ external font host (a private-network host is blocked by the browser; the Google-Fonts
43
+ CDN is a GDPR problem).
44
+
45
+ ## The theme shape
46
+
47
+ Every field is optional — omit a key to fall back to the platform default:
48
+
49
+ ```jsonc
50
+ {
51
+ "colors": {
52
+ "primary": "#1E4BCD", // brand primary → --color-primary (buttons, rings, filled header)
53
+ "accent": "#0AAA5F", // secondary accent → --color-accent (links, hovers, dots)
54
+ "background": "#FFFFFF", // portal page bg → --color-bg
55
+ "foreground": "#111111" // body text → --color-fg
56
+ },
57
+ "radius": "sharp", // "sharp" | "rounded" | "soft"
58
+ "logoSize": "max", // banner logo height: "sm" | "md" | "lg" | "max" (default lg)
59
+ "bannerDensity": "normal", // "compact" | "normal" | "spacious" (default normal)
60
+ "filledHeader": false, // true → top nav bar filled with the PRIMARY colour (on-primary text)
61
+ "websiteUrl": "https://www.example-schule.de",
62
+ "fonts": {
63
+ "heading": { "family": "Space Grotesk" }, // family only — the file is uploaded (set-font)
64
+ "body": { "family": "Lora" }
65
+ }
66
+ }
67
+ ```
68
+
69
+ - `colors.*` are `#RRGGBB` hex strings; `radius`/`logoSize`/`bannerDensity` are the enums above.
70
+ - A font entry is `{ family }`. With an uploaded file for that slot the portal loads it as
71
+ `@font-face` under that family; without one the family must already be available
72
+ (system/bundled). A font `url` is **refused** (`fonts.<slot>.url is no longer
73
+ supported`) — upload the file instead.
74
+ - Bad values (malformed hex, bad enum, a font `url`) fail loud server-side — the write
75
+ aborts, no partial state.
76
+
77
+ ## Steps — the surgical CLI path (recommended)
78
+
79
+ ```bash
80
+ # 1. Inspect current branding (branding + resolved logo URL).
81
+ plexus-cli --env staging --tenant <slug> branding show
82
+
83
+ # 2. Set the theme tokens from a JSON file (replaces the theme wholesale; leaves the logo).
84
+ plexus-cli --env staging --tenant <slug> branding set-theme --file theme.json
85
+
86
+ # 3. Upload the logo — from a local file …
87
+ plexus-cli --env staging --tenant <slug> branding set-logo --file logo.png
88
+ # … or straight from a URL (fetched + uploaded by the CLI):
89
+ plexus-cli --env staging --tenant <slug> branding set-logo \
90
+ --url https://cdn.example.com/logo-black.png
91
+
92
+ # 4. Upload the font files — the slot's family must be set first (step 2), the file is
93
+ # applied under it. WOFF2 only (signature-checked), ≤ 2 MB; a replaced file is deleted.
94
+ plexus-cli --env staging --tenant <slug> branding set-font --slot heading --file heading.woff2
95
+ plexus-cli --env staging --tenant <slug> branding set-font --slot body --file body.woff2
96
+ # (`branding clear-font --slot <slot>` removes a file; the family stays.)
97
+
98
+ # 5. Verify, then promote to prod (re-run 1–4 with --env prod — never assume prod == staging).
99
+ plexus-cli --env prod --tenant <slug> branding show
100
+ ```
101
+
102
+ - `set-theme` replaces the whole theme (a submitted-absent field clears the stored one),
103
+ mirroring the admin form — so the file must carry the **complete**
104
+ theme you want.
105
+ - The logo cap: images only, ≤ 3 MB. Pick the variant that reads on the header — a **dark
106
+ logo** for the default light bar, a **white/inverse logo** when `filledHeader: true`
107
+ (the bar is the dark primary colour). `branding clear-logo` removes it.
108
+
109
+ ## Alternatives
110
+
111
+ - **Whole-config rollout (`config apply`).** The theme round-trips through the config
112
+ snapshot's `tenant.branding` block and the files through `tenant.brandingAssets`, so
113
+ `config export → edit → apply` (or `apply --from env:staging` onto prod) carries the
114
+ complete brand, bytes included — but it re-plans the *entire* tenant config, so prefer
115
+ it when branding rides along with other config changes or moves between environments.
116
+ To swap a file in a snapshot, replace `branding/<slot>.<ext>` and keep its `path`: the
117
+ CLI re-hashes the file on disk. For a one-off change, `set-theme` / `set-logo` /
118
+ `set-font` are surgical.
119
+ - **Admin UI.** A tenant admin (holding `administration.branding.manage`) edits the same
120
+ row live under **Administration → Branding**. Use it for quick per-tenant tweaks; use the CLI
121
+ for reproducible, cross-env rollout by a platform admin.
122
+
123
+ ## Gotchas
124
+
125
+ - **Release gate:** a brand-new `plexus-cli branding` verb (or a new theme field) must be
126
+ live on the env before the CLI call resolves — otherwise the call fails with an unknown
127
+ function. Ask the platform operator whether the release is out.
128
+ - `config export` names the logo and the font files by hash and writes their bytes to
129
+ `branding/`; `branding show` resolves their URLs (`logoUploads`, `fontUploads`). A
130
+ committing apply refuses a changed hash it has no bytes for
131
+ (`[branding/contentMissing]`), a font file that is not WOFF2 (`[branding/fontRefused]`)
132
+ and a font slot without an own family (`[branding/fontFamilyMissing]`).
133
+ - `set-font` refuses (`INVALID`, German message) a slot without a saved
134
+ family, a family named like the back office's own fonts (`Inter Variable`, `Azeret Mono
135
+ Variable` — an `@font-face` is document-wide), anything over 2 MB and anything that is
136
+ not WOFF2 — convert TTF/OTF/WOFF first (e.g. `woff2_compress`).
137
+ - Removing a family (config / `set-theme`) leaves its file attached but unused; the admin
138
+ page flags it. Clear the file with `branding clear-font` when you drop the family.
139
+ - A deployment that once held external font URLs dropped them on upgrade (the platform
140
+ operator has the list of dropped URLs); re-upload those files with `set-font`.
141
+ - Verify in the browser: DevTools → Network → the `.woff2` request from the Convex origin
142
+ answers 200 with `Access-Control-Allow-Origin: <the portal origin>` (fonts load
143
+ cross-origin; Convex storage reflects the requesting origin).
144
+ - This is a live-DB operation; nothing else records it. A `config export` snapshot is
145
+ your only copy — keep it out of any shared or public place.
146
+
147
+ ## Related
148
+
149
+ - [Update a tenant's config](./update-tenant-config.md) · [Onboard / establish a tenant](./onboard-tenant.md)
150
+ - Command semantics: `plexus-cli branding <verb> --help`, `plexus-cli config apply --help`.
@@ -0,0 +1,50 @@
1
+ # Set a school's terms („Anmeldung" statt „Bewerbung")
2
+
3
+ **When to use:** a school calls the process by another name — it runs an
4
+ „Anmeldeverfahren", not a „Bewerbungsverfahren". One setting switches the whole product for that school: the UI,
5
+ task titles, feed and party notes, applicant-name fallbacks, mail blocks and buttons,
6
+ and every refusal the browser shows. URLs (`/e/units/<unit>/bewerben`) stay as they are.
7
+
8
+ ## What it is
9
+
10
+ - `tenant.terms` in the tenant config — today one term: `application`, a **closed
11
+ choice**: `"Bewerbung"` (platform default) or `"Anmeldung"`. The browser and the
12
+ backend read the same rules, so both always say the same word.
13
+ - Stored on `tenants.terms` **only when it deviates**. Absent = the platform's word.
14
+ - An unknown term or choice is refused by `config set` / `config apply`
15
+ (`[terms/unknownTerm]`, `[terms/unknownChoice]`) — nothing silently falls back.
16
+ - The person follows the choice: „Bewerber:in" → „Anmelder:in", „Bewerberportal" →
17
+ „Anmeldeportal".
18
+ - Not rewritten: a school's own texts (letter deviations, offer/board labels, team
19
+ names — change those via `offer`/`config` as usual).
20
+
21
+ ## Set it (one command, dry-run first)
22
+
23
+ ```bash
24
+ # dry run — prints the one-row plan
25
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
26
+ --key <slug> --field terms --value '{"application":"Anmeldung"}'
27
+ # commit
28
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
29
+ --key <slug> --field terms --value '{"application":"Anmeldung"}' --confirm
30
+ ```
31
+
32
+ Equivalent through a scratch export (a real tenant's config lives only in its
33
+ deployment — keep the export private): `config export --tenant <slug> --out <scratch>/config.json` → set
34
+ `"tenant": { …, "terms": { "application": "Anmeldung" } }` → `config apply --tenant
35
+ <slug> --from snapshot:<scratch>/config.json` (dry run) → `--confirm`.
36
+
37
+ Back to the platform word: `--value '{}'` (or remove the block and apply).
38
+
39
+ ## Verify
40
+
41
+ - `plexus-cli --env <env> config export --tenant <slug>` shows `tenant.terms`.
42
+ - Portal `https://<host>/<slug>/e` shows „Jetzt anmelden", nav „Anmelden", the
43
+ sign-in button „Einloggen"; the first server-rendered frame already says it (no flash).
44
+ - Staff board: „Anmeldungen", „Anmeldung ablehnen", …
45
+
46
+ ## Gotchas
47
+
48
+ - Texts the server **stored** before the switch (feed notes, party notes, task titles
49
+ already spawned) keep the word they were written with — history is not rewritten.
50
+ - The portal's English UI is unaffected (terms are German words).