@open-xchange/demo-content
v1.0.2
Published
Demo content for OX App Suite: fill demo contexts with mail, calendars, tasks, contacts and files from a dataset.
Readme
@open-xchange/demo-content
Demo content for OX App Suite. A dataset describes the people of a demo and
what they have: mail, appointments, tasks, contacts, Drive files, portal
widgets, signatures, mail filters and settings. demo-content checks a
dataset, turns its generated parts into items you can edit, and fills it
into a running App Suite by logging in as each user over the HTTP API.
It is the fill behind the provisioning UI's Demo content panel, and works on its own against any App Suite you have logins for. It needs Node.js 24 or later; nothing has to be installed:
npx @open-xchange/demo-content validate ./my-datasetdemo-content guide prints this file.
Commands
A <dataset> argument is a dataset directory when it contains a slash or
exists on disk, otherwise the name of a dataset shipped with the
provisioning UI (default, for instance). Give a directory in the current
directory as ./name.
A shipped dataset is fetched from the provisioning UI through your
cluster's API server, with your own kubeconfig: the UI serves its datasets
on a port only the API server's service proxy reaches, and the provisioning
editors may use it. --datasets <namespace>/<service> names another
installation's UI (the default is the main one, provisioning-operator/provisioning-operator-ui);
--datasets local reads the datasets next to the package, in a checkout of
the repository. When the UI runs a newer major version of this package, the
CLI says which version to update to.
datasets
npx @open-xchange/demo-content datasetsLists the shipped datasets, name and description.
validate
npx @open-xchange/demo-content validate ./my-datasetChecks dataset.yaml and every file it references, and lists every problem
at once.
expand
npx @open-xchange/demo-content expand ./my-dataset --seed 42 # to stdout
npx @open-xchange/demo-content expand ./my-dataset --seed 42 --out ./my-dataset-42Runs the dataset's generators with the seed and writes the result as
authored items, so a random start becomes a file to edit: reroll the seed
until the start looks right, expand once, then change, delete and add items
by hand. The same seed always gives the same items. --out writes a
complete dataset directory, the dataset's files copied along, and never
overwrites the source.
fill, with a users file
cat > users.yaml <<'YAML'
- { login: david@1234, password: … }
- { login: anna@1234, password: … }
YAML
npx @open-xchange/demo-content fill ./my-dataset \
--api-url https://appsuite.example.com --users users.yaml --anchor 2026-10-08Logs in as each user and creates the dataset's items. Users are taken in
slot order: the first user gets the first persona's content, users after the
last persona get none. Logins are as typed on the login page. --api-url is
the App Suite host; the API prefix (/api or /appsuite/api) is found by
asking, and a URL ending in either skips that.
A generating dataset without --seed picks one and prints it with the
result, so the same fill can be repeated. Without --anchor the dates count
from now. --anchor takes:
- a day,
2026-10-08: every relative date counts from that day; - an instant,
2026-10-08T10:30:00+02:00, an RFC 2822 date or Unix seconds: the dates count from that day, and the instant is also the fill's moment, so the same dataset, seed and instant give the same dates and times every time. Each user gets the instant's day in their own timezone. Without an offset the instant is read in this machine's timezone, so give one.
The fill record keeps an instant as ISO 8601 UTC, and a reset reuses it. For screenshots or a recording, freeze the browser's clock to the same instant. Failed items are listed and make the exit code 1; a failed login fails the whole fill before anything is created. Filling twice creates everything twice: wipe the mail and start from a fresh context.
fill, into a Context of the provisioning operator
npx @open-xchange/demo-content fill ./my-dataset --context appsuite-main/sales-demoWith --context <namespace>/<name>, the users, their passwords and the
context id come from that Context in the cluster, read with your own
kubeconfig, and only once the operator reports it Ready. Reading the
passwords needs what the UI's viewers have; recording the fill and finding
the host need what its editors have. The host is the first one of the namespace's VirtualService
that routes to the HTTP API Service; --api-url overrides it.
KUBECONFIG and ~/.kube/config are read as kubectl reads them;
--kubeconfig <file> names another, and --kubeconfig - reads one from
stdin, for a kubeconfig made on the fly that should not land in a file. A
kubeconfig that logs in through an exec plugin (kubectl oidc-login) needs
that plugin where the CLI runs.
A Context with neither users nor usersFrom gets the dataset's people as
its users first, named and addressed as the UI names them, with the demo's
mail tag. The fill waits until the operator has created them (up to ten
minutes) and then fills them. Their addresses are in the Context's
spec.mxDomain; without one, give the operator's mail domain with
--mx-domain <domain>.
The fill is recorded on the Context as the UI records it, in the annotation
provisioning.open-xchange.com/content-fill, so the UI shows it. The record
says where the dataset came from: source: { type: shipped, version } for a
shipped dataset, source: { type: directory, sha256 } for a directory, the
sum over dataset.yaml and every file it references. A Context filled
before needs --again, since a second fill adds everything again.
The UI's reset can fill a shipped dataset again on its own, but not a directory, which only exists where you ran the CLI. Reset such a demo without filling it, then fill it again:
npx @open-xchange/demo-content fill ./my-dataset --context appsuite-main/sales-demowipe
npx @open-xchange/demo-content wipe --api-url https://appsuite.example.com --users users.yamlEmpties each user's mailbox, for a clean refill: every mail and every non-standard mail folder goes; calendar, contacts, tasks and Drive stay. It prints what it removed per user. Mail outlives a deleted context, since mailboxes are keyed by address, and nothing here checks whether another context uses the same addresses: that is your call. There is no undo.
personas
npx @open-xchange/demo-content personas kestrel-trading ./my-datasetPrints the named datasets' personas as Context users, a Markdown page with
a users: block per dataset: what a demo made from a dataset's people
holds, with placeholders for the mail tag and domain. Without a name it
prints every shipped dataset, which needs a checkout of the repository.
guide
Prints this file.
Writing a dataset
A dataset is a directory holding dataset.yaml and the mails, files and
avatars it references. name is lowercase letters, digits, - and _. The
quickest start is a copy of a shipped one:
npx @open-xchange/demo-content expand default --seed 1 --out ./my-datasetname: default
locale: en_US # generators' locale: de_DE, en_US, en_GB, fr_FR
personas: # their order is the slot order
- { key: david, givenName: David, surname: Smith, avatar: avatars/david.png }
- { key: anna, givenName: Anna, surname: Bush }
mails: # keyed by the recipient persona
david:
- { from: anna, eml: eml/welcome.eml, daysAgo: 1, time: '11:13' }
- { from: { name: News, email: [email protected] }, subject: Hi, text: Body, daysAgo: 0, time: '07:30' }
appointments:
david:
- { title: Standup, weekday: 1, time: '09:00', durationMinutes: 15, series: weekdays, attendees: [anna] }
generate: # background volume, reproducible from a seed
mails: { perPersona: 40, withinDays: 30 }
appointments: { perPersona: 5, withinWeeks: 2 }Sections
Sections keyed by persona: folders, categories, mails, appointments,
tasks, contacts, files, snippets, mailFilters, portal (lists) and
settings (a map of namespace//path keys; settings.all applies to every
persona the fill logs in as, and a persona's own entry wins, so no persona
may be called all). People are always named by persona key. Unknown fields
are reported, not ignored.
- Personas. A persona's
company,department,position, home address,avatarandbirthdaygo on its user's profile when it is filled; itslocaleandtimezonego on the user when the demo is made from the dataset's people. - Folders. A mail folder goes to the top of the account, where "My
folders" lists it; an address book with
public: trueis shared with everyone in the context, the persona administering it. A mail, appointment, task or contact can name one of its owner'sfoldersof the matching module withfolder:; a file'sfolder:is a Drive path the fill creates. - Mails. A mail can go
toanother persona instead of its owner, sit infolder: SentorDrafts(withdraft: true), beflagged, carry acolor(red, blue, green, gray, purple, lightgreen, orange, pink, lightblue, yellow; it also flags the mail) and be snoozed untilsnooze: { weekday, week, time }(08:00 unless a time is given). A mail dated later than the moment of the fill (now, or the anchor's instant) moves to the day before, since the server would stamp it with the delivery time. - Categories. A category is a
name, a#rrggbbcolorand an optionalicon(bi/cart.svg); a mail'scategoriesnames the owner's or the four every user has (Important, Business, Private, Meeting). A persona with categories gets the categories feature switched on, which needs middleware 8.19 or later to show. - Appointments.
attendeesare keys or{ persona, status }(needs-action, accepted, declined, tentative), andreminderMinutessets the alarm. - Tasks.
participantsdelegate a task to other personas, each a key or{ persona, status, comment }: with astatus(accepted, declined, tentative) the fill logs in as that person and answers, then deletes the mail that tells the owner about the answer. A participant without a user in the demo is left out. A task withdone: trueis finished. - Contacts. A contact is someone outside the demo, in the persona's
address book: a
givenName,surnameorcompany, up to threeemails, phones, a business address, anavatarand abirthday. A contact list is{ list, members }: members are addresses, and one that belongs to a contact listed before it points at that card. - Snippets. A
signatureor atemplatewith anameand HTMLcontent; a signature may be thedefaultfornewmail or forreply, and sitsaboveorbelowthe quote. - Mail filters. A
headertest forvaluethat moves the mail into one of the owner's mail folders or gives it acolor;{ vacation: { from, until, subject, text } }is the automatic reply, its days given as{ weekday, week }.
Dates
Dates are relative, so a demo looks current whenever it is filled:
daysAgo, weekday plus an optional week offset, dueInDays, and a
persona's birthday: { inDays, age }. They count from the fill's anchor
(--anchor, a day or an instant, else now) in the user's own timezone. {{date weekday=4
week=1}} in an eml file, a mail's text or a snippet becomes that day,
written out ("Thursday, October 15").
Files
Paths are relative to the dataset directory and must stay inside it.
A story.md next to dataset.yaml is optional: plain Markdown for whoever
presents the demo, saying who the company and the people are and what to
show in each app. The provisioning UI shows it next to the dataset. HTML in
it is shown as text, and links only go to http(s) and mailto.
Trying it
validate while writing, expand to see what a seed generates, then a fill
of a fresh demo: fill --context for a Context of the provisioning
operator, or fill --users for any App Suite. To try a changed draft on the
same demo, reset it without filling (or wipe its mail) and fill again.
License
AGPL-3.0-or-later. See LICENSE.
