|
| 1 | +## Banners |
| 2 | + |
| 3 | +This is the publisher reference for backend banner commands. The stack stores |
| 4 | +one `io.cozy.banners` document per category and instance. See |
| 5 | +[ADR 054](https://github.com/linagora/twake-workplace-private/blob/main/documentation/docs/adrs/adr-054.md) |
| 6 | +for the platform design. |
| 7 | + |
| 8 | +### Configuration |
| 9 | + |
| 10 | +Enable banners and allow the publisher's categories in each recipient context: |
| 11 | + |
| 12 | +```yaml |
| 13 | +contexts: |
| 14 | + b2b_twake_default: |
| 15 | + banner: |
| 16 | + enabled: true |
| 17 | + command_categories: |
| 18 | + - billing |
| 19 | + - trial |
| 20 | + cta_hosts: |
| 21 | + - manager.example.org |
| 22 | +``` |
| 23 | +
|
| 24 | +Set `command_categories` to `["*"]` to allow every category except `quota`. |
| 25 | +`cta_hosts` has no wildcard. |
| 26 | + |
| 27 | +Broker credentials, permissions and bindings control who can publish. Each |
| 28 | +category must have one owner and one addressing mode: the stack keeps one |
| 29 | +revision per instance and category, shared by `orgId` and `workplaceFqdn` |
| 30 | +commands, so never address a category both ways. `quota` is reserved for the |
| 31 | +stack's rules. |
| 32 | + |
| 33 | +Instances that disable banners are skipped. Instances whose context does not |
| 34 | +list the category or a CTA host are skipped with a warning log. Other eligible |
| 35 | +recipients still receive the command. Skipping an instance leaves its existing |
| 36 | +documents and recorded revision unchanged. An error on one instance |
| 37 | +does not stop processing the others. The stack returns all failures after |
| 38 | +attempting every recipient, so delivery can be retried. |
| 39 | + |
| 40 | +### Commands |
| 41 | + |
| 42 | +Publish JSON on the `platform` exchange, consumed by `stack.banner.commands`. |
| 43 | +The routing key selects the operation: |
| 44 | + |
| 45 | +- `banner.materialize`: create or replace the banner in a category. |
| 46 | +- `banner.clear`: expire the banner in a category while retaining its revision; nonempty presentation fields |
| 47 | + are rejected. |
| 48 | + |
| 49 | +See [RabbitMQ configuration](rabbitmq.md#configuration) for queue declarations |
| 50 | +and [shared fixtures](../model/banner/testdata) for complete examples. |
| 51 | + |
| 52 | +`banner.materialize`: |
| 53 | + |
| 54 | +```json |
| 55 | +{ |
| 56 | + "workplaceFqdn": "alice.twake.app", |
| 57 | + "eventId": "banner-command-42", |
| 58 | + "revision": 42, |
| 59 | + "timestamp": 1788944400, |
| 60 | + "category": "billing", |
| 61 | + "bannerId": "billing.grace.cycle-a.attempt-2", |
| 62 | + "severity": "warning", |
| 63 | + "surface": "banner", |
| 64 | + "dismissible": true, |
| 65 | + "text": { "en": "We could not charge your card.", "fr": "Nous n'avons pas pu débiter votre carte." }, |
| 66 | + "cta": { |
| 67 | + "label": { "en": "Update payment method", "fr": "Mettre à jour le moyen de paiement" }, |
| 68 | + "url": "https://manager.example.org/linagora/twake_prod/premium" |
| 69 | + } |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +`banner.clear`: |
| 74 | + |
| 75 | +```json |
| 76 | +{ |
| 77 | + "workplaceFqdn": "alice.twake.app", |
| 78 | + "eventId": "banner-command-43", |
| 79 | + "revision": 43, |
| 80 | + "timestamp": 1788944400, |
| 81 | + "category": "billing" |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +| Field | Required | Contract | |
| 86 | +| --- | --- | --- | |
| 87 | +| `category` | always | Matches `^[a-z][a-z0-9-]{0,31}$`; `quota` is rejected. | |
| 88 | +| `workplaceFqdn` / `orgId` | exactly one | A single instance host name / a B2B organization ID matching instance `org_id`, whose members receive the command; `orgId` is at most 256 bytes with no surrounding whitespace. | |
| 89 | +| `revision` | always | Positive counter, increasing per category. | |
| 90 | +| `timestamp` | always | Decision time in positive epoch seconds, within the RFC3339 range. Does not order commands. | |
| 91 | +| `eventId` | no | Correlation ID, at most 256 bytes. | |
| 92 | +| `bannerId` | materialize | Matches `^[a-z0-9.-]{1,64}$`. Keep it for the same occurrence to preserve dismissal; change it for a new occurrence. | |
| 93 | +| `severity` | materialize | `info`, `warning` or `error`. | |
| 94 | +| `surface` | materialize | `banner` or `modal`. | |
| 95 | +| `text` | materialize | Locale map with nonempty `en`; at most 1024 bytes per locale. | |
| 96 | +| `title` | no | Locale map with nonempty `en` when supplied; at most 256 bytes per locale. | |
| 97 | +| `cta`, `secondaryCta` | no | Each has a locale-map `label` (nonempty `en`, at most 128 bytes per locale) and an absolute `https` `url` (at most 2048 bytes) whose host is in `banner.cta_hosts`. A secondary CTA requires a primary one. | |
| 98 | +| `dismissible` | no | Defaults to false. A modal without a CTA is made dismissible. | |
| 99 | +| `priority` | no | 0–1000; defaults to 0. Quota banners use 50 and 100. | |
| 100 | +| `startsAt`, `endsAt` | no | RFC3339. If both are supplied, `startsAt` must precede `endsAt`. An explicit start replaces the stored start; omission preserves it for the same occurrence when compatible with the end, otherwise defaults to the command's decision time. | |
| 101 | + |
| 102 | +Each locale map accepts at most 32 locales with keys of 1–35 bytes. The JSON |
| 103 | +body is limited to 256 KiB, including whitespace and unknown fields. |
| 104 | +`_id`, `_rev`, `dismissedAt` and `cozyMetadata` are not command fields and are |
| 105 | +ignored if supplied. |
| 106 | + |
| 107 | +### Localization |
| 108 | + |
| 109 | +The publisher supplies all wording. The stack selects the instance's locale |
| 110 | +only if it is complete for every supplied text and label; otherwise the whole |
| 111 | +banner falls back to `en`. The stored `lang` identifies the selected language. |
| 112 | +Any complete publisher-supplied locale is supported, independently of the |
| 113 | +stack's translation catalogs. |
| 114 | + |
| 115 | +On an instance language change, existing banners are re-localized from retained |
| 116 | +commands in the banner documents without republishing. Cleared or deleted |
| 117 | +banners and older records without retained wording are left unchanged. |
| 118 | + |
| 119 | +### Revisions and recovery |
| 120 | + |
| 121 | +Commanded banners store `revision`, `eventId`, and the full localized command |
| 122 | +in `accepted` alongside their presentation. A clear retains the category's |
| 123 | +document with `cleared: true`, an expired `endsAt`, and no retained wording; |
| 124 | +clients must filter out banners whose validity window has ended. A newer |
| 125 | +materialize replaces it normally. Updating the command revision also updates |
| 126 | +the document revision, even when its visible wording is unchanged. |
| 127 | + |
| 128 | +These fields use the same app permissions as the banner. Apps recording a |
| 129 | +dismissal should preserve the other fields and use the current CouchDB `_rev`; |
| 130 | +editing or deleting the ordering state can allow stale commands to be replayed. |
| 131 | + |
| 132 | +- Revisions at or below the last accepted revision for an instance and category |
| 133 | + are ignored, even after a clear. Only a changed decision needs a new revision; |
| 134 | + the publisher must ensure newer revisions carry newer state. |
| 135 | +- Retry with the original revision, event ID and payload. Replays complete |
| 136 | + partial organization deliveries and reach newly provisioned members while |
| 137 | + leaving recipients that already accepted the revision unchanged. |
| 138 | +- Enabling banners does not bootstrap them: the publisher must republish. |
| 139 | +- Invalid commands and missing workplaces fail delivery. The broker requeues |
| 140 | + failures without a delay until its configured delivery limit is exhausted; |
| 141 | + configure dead lettering as described in [RabbitMQ](rabbitmq.md#dead-letter-exchange-dlx-and-dead-letter-queue-dlq). |
| 142 | + Fix the cause and explicitly replay dead-lettered commands with their original |
| 143 | + operation routing key (`banner.materialize` or `banner.clear`). |
| 144 | +- The stack sends no application acknowledgement. A broker confirm means the |
| 145 | + broker accepted the message, not that a banner was stored or displayed. |
0 commit comments