Repository navigation
Releases
v1.16.0
v1.16.0 — One shape for the code 🧭
The fourth release of iteration 17, and a minor. Most of it is inside: the API, the shared contracts and the web app are now cut the same way, by audience first and area second, and the styling lives in a UI library of its own. A shop sees three things: a product list that asks both ways, forms that no longer lose what was typed while the page loads, and an order reference in a format the deployment writes. The machine API is unchanged, and one database migration runs on its own at start-up.
Important
A config directory written for v1.15.1 does not load in this release. Two text files are renamed, many keys move to their area, some theme colours are renamed, and ordering.orderReference.format is a new required setting. Run the migration tool on your config before you deploy (see Configuration).
What's new
The product list asks both ways (FR-ADM-20)
The admin product list could ask about a gap only as missing and about a feature only as present, so "live, without documents" could not be asked. Now each of the twelve facts can be asked either way, or left alone: price, picture, description, attributes, packaging, box volume or weight, featured, set parts, variants, sold together, documents and line note.
- One control in the thumbnail column's heading opens the questions and shows how many are asked, so the rows keep their room while nothing is asked
- Each question asked shows as a chip in the list's count line, with its own way out, so the list is never narrowed by something out of view
- "Incomplete" is no longer a filter. It was the one question that meant "any of four" rather than "all of these", and each row already names what keeps it incomplete. An old link that asks for it shows the list without that narrowing
Forms take input only once they work (NFR-SEO-05)
A page rendered on the server shows its forms before the browser can handle them, and what a visitor typed in that moment was lost without a word. On a slow phone that could be several seconds of typing. Every server-rendered form (sign-in, registration, checkout, the inquiry, the account and password forms) now refuses input until it can act on it. It looks the same meanwhile. The search box is the exception, since it works without JavaScript.
The inquiry page also loads lazily now, which takes about 80 kB of JavaScript out of every first page load.
An order reference in the shop's own format (ADR 0038)
The reference was always a prefix, a date and four digits. A shop moving from another system wants its own shape, so ordering.orderReference.format now states it:
{date}exactly once: the shop day asYYMMDD- each
#is one random digit, at least four in all, placed anywhere (#-#-#-#is allowed) - fixed text in upper-case letters, digits,
-,_and.; no/, as references appear in order links; 32 characters at most
CK{date}-#### gives CK261106-4821. The migration writes your current shape out (CK becomes CK-{date}-####), so new references look exactly as before until you change it. Stored references never change.
Smaller changes
- Long names wrap in the admin lists. A long document name ran off its cell. Products, documents and users now share one title cell, and the user list shows each account's customer number under its name
- A name may hold no digits in registration, the account page, checkout, the inquiry, the staff editor and the order adjustment. A name already stored is left as it is, and the customer exchange is not held to the rule, since source systems keep placeholders such as "Kunde 123"
- The inquiry form enforces its lengths (name 200, phone 50, message 5,000) instead of failing on submit with a generic error
- A password over 200 characters is now refused as too long, not as too short
- The hidden anti-spam field's label comes from the deployment's text, so the German shop no longer carries one English sentence
- A text that repeats a placeholder has every copy filled. Before, only the first was
- The account page keeps showing your details while an address is removed, instead of flashing back to loading bars
- Sync runs are ordered by when they were recorded, not by the wall clock, which can step backwards and misordered the log
- Visual unification. Every colour is now a named role, and some screens moved to the pattern most of the app already used: quiet text one shade, status dots and resting icons slightly darker, status panels on one lighter ground with one shared notice, neutral badges on the chip colour, rich text in the theme's colours, and a visible disabled state for scroll-to-top
Inside: the code by audience and area
This is the larger part of the release, and nothing a customer sees changes because of it (ADR 0069 to ADR 0073):
- One layout everywhere. The API,
libs/sharedand the web app are cut as<audience>/<area>/, withcore/<area>/for what two audiences use andplatform/for what knows no area. Lint holds the edges, and docs/architecture.md now carries a generated graph of which area imports which - A UI library apart from the app (
libs/ui): stateless building blocks, icons as data, every colour a theme role - Forms on Signal Forms, with views kept apart from what loads their data
- The deployment's texts are named by their reader (storefront, admin, shared, mail, PDF), with keys grouped by area inside each
- In numbers, against v1.15.1: module cycles 3 → 1, files over 700 lines 25 → 12, the largest file 2,724 → 1,428 lines, and 117k → 104k lines in all, the new UI library included
- New CI checks: the area graph, comments (no dangling file names) and every relative link in the docs
Configuration
Migrating a deployment's files
Download the tool and this release's map from the tag, run it once to read the report, then again with --write:
mkdir -p migrate/config-migration
curl -fsSLo migrate/migrate-config.mjs \
https://raw.githubusercontent.com/vik-koch/b2b-catalog-platform/v1.16.0/tools/migrate-config.mjs
curl -fsSLo migrate/config-migration/1.16.0.json \
https://raw.githubusercontent.com/vik-koch/b2b-catalog-platform/v1.16.0/tools/config-migration/1.16.0.json
node migrate/migrate-config.mjs --release 1.16.0 --config-dir <your config dir> # report only
node migrate/migrate-config.mjs --release 1.16.0 --config-dir <your config dir> --writeWhat it does:
- Renames two text files:
texts/common.jsonbecomestexts/shared.json, andtexts/app.jsonbecomestexts/storefront.json - Moves keys to their area. In
deployment.json,pages,consent,cookieConsentEnabled,locations,mapProviderandelsewheremove undersite.customerTypes,signInStepandaddressmove underaccounts, andbillingAddressEnabled,pickup,deliveryandorderReferenceunderordering.catalog.currencybecomes the top-levelcurrency, andorderReference.timezonethe top-leveltimeZone. Inapi.json,termsmoves toordering.terms, andsync.autoApply/sync.customerAutoApplytoexchange.*. The texts move the same way. Your wording is kept - Renames theme colours:
border→edge,borderStrong→edgeStrong,primaryDeep→primaryPress,surface→surfaceSubtle,subtle→inkSubtle.mutedis dropped, and aneutralsramp becomes the roles it fed (surface,chip,surfaceStrong,inkDisabled,edge,edgeStrong,ink,surfaceSubtle). Don't rename these by hand:surfaceis now a different ground, so a config that keeps the old key sets a different colour - Writes the order reference format from your prefix, as above. A deployment without a prefix gets
ORD-{date}-#### - New wording — translate it. 18 texts, written in English: the content filter, its way choices, its chips and the six content facts (
texts/admin.json), "names have no digits" for three fields, the honeypot label and the word "required" (texts/shared.json), the too-long password (texts/storefront.json), and a screen-reader count of product suggestions in the order adjustment
New optional settings
Every colour role the stylesheet uses is now a key under branding.theme, all optional. Only primary is required. The status colours (danger, warning, success, info) can be set whole, and each pair's contrast is measured when the app starts: one below its WCAG floor stops it and names the role. The roles are described in the configuration reference.
Deployment
- One database migration,
0060_sync_runs_seq, adds a sequence column to the sync log. It runs at start-up like every other, with nothing to do by hand - No change to
compose.yml, the environment or the machine API. An ERP adapter written forv1.15.1works as it is
What's Changed
- Bump cloudflare/cloudflare from 5.28.0 to 5.28.1 in /infra/demo by @dependabot[bot] in #430
- Update/rewrite documentation. Closes #429 by @vik-koch in https://github.com/vik-koch/b2b-catalog-p...
v1.15.1
v1.15.1 — A back office for the demo ⚙️
A patch in iteration 17. The English demo shop now has a back office: a simulated source system owns its catalog, customers and orders, and drives them through the public machine API the way a real ERP adapter would. The API, storefront, admin panel and database are unchanged, and a config directory written for v1.15.0 loads as it is.
The simulated back office (ADR 0068)
apps/erp-sim is a small Node app that uses nothing but the documented machine API, with a token of its own:
- Nightly, at the shop's midnight: products sell down, are reordered when they run low, and are delivered two nights later. Once a week a new article arrives, unpublished, for the shop to complete (four in all)
- Hourly in working hours (Mon–Fri, 08–16 Irish time): each new registration gets a customer number and a price list and is approved. Each open order moves one step: accepted, ready, then handed over and paid. The customer is told each time
- No state of its own. What it sends follows from the seed bundle,
simulation.jsonand the nights since the last reset, so a restarted simulator sends exactly what it would have sent - An area the shop takes back in the admin panel is skipped and logged, never reported as a broken feed
- Reviewers find it at
/erp/on the demo's domain, behind the reviewer inbox's login, with its recent log and a button that runs the hourly cycle outside working hours
For ERP adapter authors
- Creating a product through the catalog exchange needs
nameandcategoryin the run'sfields. A price is not needed: the product waits unpublished until a list prices it. A new key in a run withcreateMissingoff counts as unchanged, not as an error. docs/catalog-machine-api.md said otherwise until now - The admin text for that row error (
sync.rowErrors.cannot-create-productintexts/admin.json) said a price was needed too. The demo shops are reworded; check your own deployment's wording
Deployment
- A real shop has nothing to do. No migration and no new settings.
compose.ymlgains anerp-simservice in thesimulationprofile, which no stack enables - The prod demo needs a new secret,
ERP_SIM_TOKEN, before this tag is deployed.infra/README.mdshows how to generate one. The first deploy resets the prod demo once, because its bundle changed - Demo stacks:
infra/seed.shnow putsreset.shin the stack's directory and runs the reset there, so it can also be run by hand on the VM (./reset.sh,--status). The reset hash now covers the stack'sERP_SIM_TOKEN. A new simulator build no longer resets a stack: a deploy restarts the simulator on the same night count - Slaney Labware's
api.json:maxChangedSharegoes from 0.5 to 1, because a nightly stock run touches most of a small catalog.maxIdClaimsgoes from 0 to 10, so the back office's claims apply without waiting for a person
What's Changed
Full Changelog: v1.15.0...v1.15.1
v1.15.0
v1.15.0 — Two shops, one image 🏪
The second release of iteration 17, and a minor: a deployment can now say whom it sells to, and put a notice above every page. Both came out of building a second demo shop. The public demo now runs two shops from the same image, each from a directory of its own: the Coffee Kontor, wholly in German, and Slaney Labware, an Irish laboratory supplier that sells to companies only. Nothing in the database changes.
Important
A config directory written for v1.14.1 does not load in this release. customerTypes is a new required setting with no default. Run the migration tool on your config before you deploy (see Configuration).
What's new
Which customers the shop sells to (FR-AUTH-13)
A deployment lists the kinds of customer it sells to: private persons, companies, or both. A kind it leaves out is offered nowhere:
- Registration asks no "registering as" question when only one kind remains, and a companies-only shop asks for the company from the start
- The staff editor offers the same kinds. An account stored as a kind the shop no longer sells to can be saved again only as one it does, and the form says why
- Checkout and order adjustments invoice an order only to a party of an accepted kind. Payment methods follow, since a company pays by bank transfer
- The customer exchange refuses a row that would make an account a kind the shop does not take (
customer-type-not-accepted), and leaves an account alone where the row says nothing about its kind
A sole trader with a registration number counts as a company. The rule is held by the API wherever an account or an order is written, not only by the forms.
A notice above every page (FR-NAV-08)
A deployment can put a short line above the header of every page, such as that the shop is a demonstration or closed over the holidays, optionally followed by a link. It cannot be dismissed. Every mail carries it above its content; the order documents do not, since they record a transaction. Leave notice out and nothing is shown.
Smaller changes
- Box volume and weight are now formatted in the deployment's locale. Before, they printed the stored
1.600whatever the language - The grey ramp can be replaced per deployment (
branding.theme.neutrals), for a brand whose neutrals are not warm - The API image no longer carries any demo content. Demo data is a seed bundle loaded by a separate seeder, which no shop's deploy runs
Configuration
Migrating a deployment's files
Download the tool and this release's map from the tag, run it once to read the report, then again with --write:
mkdir -p migrate/config-migration
curl -fsSLo migrate/migrate-config.mjs \
https://raw.githubusercontent.com/vik-koch/b2b-catalog-platform/v1.15.0/tools/migrate-config.mjs
curl -fsSLo migrate/config-migration/1.15.0.json \
https://raw.githubusercontent.com/vik-koch/b2b-catalog-platform/v1.15.0/tools/config-migration/1.15.0.json
node migrate/migrate-config.mjs --release 1.15.0 --config-dir <your config dir> # report only
node migrate/migrate-config.mjs --release 1.15.0 --config-dir <your config dir> --writeThe report has two sections:
- New settings — decide them.
customerTypesis set to["person", "company"], which is what every shop did until now. A shop that sells to companies only writes["company"]. The first kind listed is the one the forms start on - New wording — translate it. Five texts, written in English: the refusal at checkout (
texts/app.json), and four admin messages for the account list, the staff editor, the order adjustment and the customer sync's row errors (texts/admin.json)
New optional settings
Both in deployment.json, and both may simply be left out:
"notice": {
"text": "We are closed from 24 December to 2 January.",
"link": { "label": "Opening hours", "url": "https://shop.example/pages/contact" }
}The link's url must be absolute, since a mail has no page to resolve a path against.
branding.theme.neutrals replaces steps of the grey ramp (50…950) with colours of your own. Any step left out keeps the platform's.
Deployment
infra/deploy.shrequiresCONFIG_DIR. It no longer falls back to a config committed in this repo. The private deployment template already passes itinfra/deploy.shno longer hasSEED. Loading demo data moved toinfra/seed.sh, which resets a demo stack to a seed bundle and refuses a database it did not make. A real shop's deploy never calls itADMIN_FORCE_PASSWORD_CHANGEis new for thebootstrap-adminone-shot and defaults totrue, so the bootstrap admin still has to change the deployed password at first sign-in. Only a demo stack's reset turns it off- No database migration
For contributors
The committed config and seed bundle the suites run on moved from the repo root to deployments/fixture/, beside the two demo shops in deployments/. The unit specs now read the fixture whatever CONFIG_PATH your .env names, so pointing your local setup at another deployment no longer fails them. Update CONFIG_PATH, SEED_PATH and PASSWORD_BLOCKLIST_FILE in your .env (see .env.example). nx run seeder:seed now resets the local database before loading the bundle.
What's Changed
- Extract seeding to make it reusable (#412) by @vik-koch in #413
- Update pictures / icons for coffee shop demo persona (#412) by @vik-koch in #414
- German dev seeding. Closes #412 by @vik-koch in #415
- Add a new Slaney Labware demo persona for prod (#416) by @vik-koch in #417
- Make b2b/b2c customer support configurable. Closes #418 by @vik-koch in #419
- Extend prod pipeline to seed new demo persona (#416) by @vik-koch in #420
- Move seed + config folders to deployments/fixture (#412) by @vik-koch in #421
- Add site notice. Closes #422 by @vik-koch in #423
- Separate seeding from deploy script and reset data before seeding (#412) by @vik-koch in #424
- Seed only when seeding is changed (#412) by @vik-koch in #425
- Update documentation (#426) by @vik-koch in #427
Full Changelog: v1.14.1...v1.15.0
v1.14.1
v1.14.1 — One text, one key 🗂️
A patch on top of iteration 16, and the first release of iteration 17. The deployment's text had grown in three files that repeated each other: "Company ID" six times, the payment methods in four places, the order summary's wording inside the mail's. This release gives every thing one key per reader, words every count so the number needs no plural form, and moves the settings only the API reads out of the file every visitor receives. Nothing in the database or on the machine surface changes. What changes is the layout of a deployment's config directory, and a tool moves a deployment's own files to it.
Important
A config directory written for v1.14.0 does not load in this release. Files moved and keys were renamed, merged or dropped. Run the migration tool on your config before you deploy (see Configuration). It keeps your own wording and settings, and lists what it could not decide for you.
Configuration
Migrating a deployment's files
The tool needs Node and nothing else. Download it and this release's map from the tag, run it once to read the report, then again with --write:
mkdir -p migrate/config-migration
curl -fsSLo migrate/migrate-config.mjs \
https://raw.githubusercontent.com/vik-koch/b2b-catalog-platform/v1.14.1/tools/migrate-config.mjs
curl -fsSLo migrate/config-migration/1.14.1.json \
https://raw.githubusercontent.com/vik-koch/b2b-catalog-platform/v1.14.1/tools/config-migration/1.14.1.json
node migrate/migrate-config.mjs --release 1.14.1 --config-dir <your config dir> # report only
node migrate/migrate-config.mjs --release 1.14.1 --config-dir <your config dir> --writeThe report has up to six sections. Read each before deploying:
- New settings — decide them.
mapProvideris set to the demo's"OpenStreetMap". Set it to whoever serves your map embeds - New wording — translate it. Five admin strings, written in English: two dialog buttons and three order sync field names
- Merged keys whose values differed. 120 keys each replace several old ones. Where your files had different words for what is now one key, the first was kept and the others are listed. Check that the kept one reads right everywhere it now appears
- Dropped, no longer used. Among them
branding.title: the document title is now built from the name and the tagline - Counting texts to reword. A text with
{count}that is still written assingular|pluralstops the boot until it is reworded with one form, for example "Products: {count}" - Missing from your files. Anything listed here is required. Write it before deploying
CI migrates the v1.14.0 demo config with this map and validates the result, so the map covers every key the release requires.
The new layout
Before (v1.14.0) |
Now |
|---|---|
deployment.json |
deployment.json, without terms, retention, session and sync |
| — | api.json: terms, retention, session, sync, the settings only the API reads |
| — | texts/common.json: names the storefront, the admin, mail and the PDFs share, and what customers read in more than one place |
app-text.json |
texts/app.json |
admin-text.json |
texts/admin.json |
mail-text.json |
texts/mail.json, without the order summary |
| — | texts/pdf.json: the order summary and the terms' file name |
Changed keys in deployment.json:
locale— new, top level, a BCP 47 tag such as"de-DE". It replacescatalog.currency.locale(the tool moves the value) and now sets the language every page, mail and PDF declares as well as how dates, numbers and amounts are formatted.catalog.currencykeeps onlycodebranding.tagline— replacesbranding.title. It is the landing page's heading (formerlyhome.titlein the app text, where the tool takes it from) and, after the name, the title of every page without one of its own:Coffee Kontor — Wholesale specialty coffeemapProvider— new, required, the name the map notice and the cookie banner can show as{provider}
Text, in short (the tool's report lists every key for your files):
- Keys group by domain, then by screen:
orders.detail,products.editor. The admin'sorderDetailis noworders.detail, the storefront'sauth.myAccountis nowaccount - A text only a screen reader hears ends in
Aria. It need not fit on screen, so it can be a whole phrase - Cancel, Close, Remove and the other controls every screen has are one key each in
texts/common.json'sactions. Staff-only verbs (Save, Edit, Delete) are the admin text'sactions - The admin text's
commonblock is gone. Its keys now sit with what they belong to:actions,discard(the unsaved-changes dialog),upload,grid, a top-levelcatalogErrors, and a few in their own screens' blocks - An admin page is
{ title, intro? }and a confirmation dialog{ heading, message, confirm, cancel?, warning? }. Leave out an intro and none is drawn; leave out a dialog'scanceland the common one is used - The admin's own checks are worded as statements ("A name is required."), customer-facing ones keep addressing the reader
config/README.md has the layout in full, and a new list of the parts that describe one shop and a new deployment rewrites.
Environment
CONFIG_PATH names the config directory, and compose sets it to /config itself. These variables are no longer read. Remove them from your stack .env:
DEPLOYMENT_CONFIG_FILE,APP_TEXT_FILE,ADMIN_TEXT_FILE,MAIL_TEXT_FILE,CONFIG_ASSETS_DIR
PASSWORD_BLOCKLIST_FILE is unchanged. New and optional: MEDIA_BACKUP_KEEP_DAYS, the days the paired backup archives stay on the server. It defaults to BACKUP_KEEP_DAYS. With an off-host copy, two or three are enough.
Storefront
- Counts read without plural forms: "Products: 12" rather than "12 products". The demo's texts were reworded throughout
- Pages, mails and PDFs declare the deployment's language, so a screen reader picks the right voice and a browser hyphenates correctly. Until now a page always declared English
- Some wording was merged where two screens named the same thing differently
Admin panel
- Wording merged and reworded in the same way: one word for one thing, checks as statements
Documentation
- README.md — status line names
v1.14.1 - config/README.md — the new layout, the locale, what describes your shop, texts nobody sees, and migrating between releases
- ADR 0018 amended — the directory's layout, which text goes where, how keys are grouped and named, one locale, and a migration map for each release that moves keys or files
- ADR 0009 amended — counting texts carry no plural forms
- docs/roadmap.md — iteration 17 and this release
What's Changed
- Make all plural texts consistent and plural-free (#398) by @vik-koch in #399
- Configure VPS backup retention time (#383) by @vik-koch in #400
- Consolidate / sweep texts + separate common/pdf texts. Closes #401 by @vik-koch in #403
- Bump cloudflare/cloudflare from 5.27.0 to 5.28.0 in /infra/demo by @dependabot[bot] in #402
- Extract api-only configuration (#404) by @vik-koch in #405
- Add global locale key (#406) by @vik-koch in #407
- Minor text/config adjustments (#401) by @vik-koch in #408
- Split admin text common block (#401) by @vik-koch in #409
- Update documentation (#410) by @vik-koch in #411
Full Changelog: v1.14.0...v1.14.1
v1.14.0
v1.14.0 — Iteration 16: Compliance before going live ⚖️
Closes iteration 16. The fifteen iterations before it built a shop. This one makes it one that can open with real customers' data in it. The platform does not write the shop's legal texts. It owes the shop the mechanisms those texts rely on, and a plain account of what it processes, why and for how long.
Consent is offered rather than presumed. Each purpose has its own versioned text and its own switch, and every consent is kept as a record. An order accepts the terms of sale as they stood when it was placed, and its prices state their tax basis. Personal data that is destroyed leaves a record of the destruction. A finished order loses its personal details after a period the deployment sets. An admin can delete an account, or clear a guest's order, when the person asks by letter, email or phone. Around that: an optional second sign-in step through the account's phone, backups that leave the server encrypted, and one document listing every kind of personal data the platform holds.
Important
Set retention.orderDays deliberately before you deploy. A daily sweep starts a few minutes after the API boots. On its first run it removes the personal details from every order that has been finished for longer than that period. This cannot be undone. On the same run, stored customer sync runs older than a day lose the rows they carried. If you need either, take a backup first. See Configuration.
Requirements delivered (compared to v1.13.1)
Personal data
- NFR-LEGAL-09 — consent is recorded per purpose (contact, account) against the version of its text, can be withdrawn, and an admin can find a person's records by account, email address or phone number
- NFR-LEGAL-12 — every destruction of personal data writes a record in the same transaction: deleting an account, declining a registration, the end of a consent record's retention, and removing an order's details
- NFR-LEGAL-13 — one account of every kind of personal data the platform holds, including copies in logs, backups, mail and the visitor's browser: docs/personal-data.md
- NFR-LEGAL-14 — a finished order loses its personal details after a period the deployment sets, and an admin can remove a guest order's details on the guest's request
- FR-ADM-23 — an admin can delete an account on the person's request, through the same anonymization as the holder's own deletion
Selling
- NFR-LEGAL-10 — a terms page of its own. Each order records the version it was placed under, and the confirmation names that version's date and can carry the terms as a PDF
- NFR-LEGAL-11 — prices state their tax basis (tax included, tax added on the invoice, or no tax charged), and a product can have its own rate. Each order records its basis and the rate of every line
Signing in
- FR-AUTH-12 — an optional second step after the password: the person proves they hold the account's phone number, by a code sent to it or by calling a number from it. Sessions renew while they are used, and a browser can be remembered
Amended
- NFR-LEGAL-01 — every form that takes personal details (contact, registration, checkout) points to the privacy policy, as information and never as part of what a box says
- NFR-LEGAL-02 — the footer of every page can carry a seller line: legal name, registration numbers and a way to reach the seller
- NFR-LEGAL-04 — a withdrawal notice page only where a jurisdiction prescribes one. Elsewhere, the return procedure belongs in the terms
- NFR-LEGAL-08 — deleting an account also withdraws its consents and records the destruction
- FR-NAV-03 — the conditions page states the tax basis, the deployment's words about the goods, and how a return works in practice
- FR-NAV-06, FR-AUTH-01/04 — the contact form, registration and an invited holder's first password ask for consent where the deployment switches that purpose on
- FR-AUTH-06 — deleting your own account withdraws your account consent and records the destruction
- FR-AUTH-11 — declining a registration removes it with its details, instead of switching the account off. It cannot be undone and asks for confirmation
- FR-ADM-01/02/07 — a product's tax rate is set in the editor, by a file sync or by the catalog exchange
- FR-ADM-03 — every page save keeps a version, the editor lists earlier versions, and pages a reader may hold the shop to show when their text last changed
- FR-ADM-08 — the order exchange reads each order's tax basis and line rates, and can take back a customer's cancellation if it says so explicitly
- FR-ORD-02 — a cancellation the customer made can be taken back for them, but only by an instruction that says so and tells them
- FR-ORD-05 — the order summary names the terms version and the tax basis, and can carry a return notice
- FR-CART-02, FR-ACC-01, FR-NOTIF-06 — the cart, the customer's order pages and the order mails state the tax basis. The confirmation also names the terms version
- FR-UNIT-06 — the attribute table also states how many pieces a pack and a box contain
- NFR-SEC-01 — responses send HSTS, forbid framing and content sniffing, and send no referrer to other sites
- NFR-OPS-04 — backups are a matched pair of database and files, and can leave the host encrypted for storage the host cannot erase
Architecture decisions
- ADR 0065 — state the tax basis, never calculate the tax
- ADR 0066 — confirm a sign-in through the phone: a code or a call, through a sidecar the deployment supplies
- ADR 0067 — each night's backup leaves the host as an encrypted pair, for storage the host can add to but not delete from
- ADR 0016 amended — maintenance-mode 503s are kept out of alerting
- ADR 0019 amended — the session slides: it renews while used and ends after a period without use
- ADR 0020 amended — every page save keeps a version. Rich text can link phone numbers and pages on the same site
- ADR 0028 amended — database and media backups are taken as one pair
- ADR 0039 amended — checkout asks for no consent, because an order's details are processed to fulfil it
- ADR 0062 amended — a write-back may take back a customer's cancellation, but only explicitly
Database
Thirteen migrations, all applied automatically on deploy. None of them drops data.
0047_page_versions—page_versionsreplacespages. Each existing page becomes its version 1, thenpagesis dropped0048_consents,0049_consent_withdrawals— consent records and their withdrawals. The database refuses any change to a record0050_order_terms_version—orders.termsVersionId. Existing orders accepted no recorded version0051_destruction_records— the destruction record0052_product_tax_rate—products.taxRate. Empty means the deployment's default0053_order_tax—orders.taxBasisandorder_items.taxRate. Existing orders are given basisnone, because nothing recorded what they were quoted on0054_order_personal_data_removed—orders.personalDataRemovedAt0055_user_last_sign_in—users.lastSignInAt. It starts empty and fills from the next sign-in0056_sign_in_codes,0059_sign_in_calls— pending sign-in codes and call checks0057_sign_in_step_exemption— who exempted an account from the second step, and when0058_devices_trusted_since— forgets every remembered browser when an account's number changes or the account is switched off.
The machine surface
No routes or scopes changed. Every new field is optional for a sender, so adapters written against v1.13.x keep working.
- Catalog — a product row can carry
taxRate: a percent with at most two decimals.nullputs the product back on the default rate, and the file sync takes it as a column. New refusal:tax-rate-invalid - Order read — every order carries
taxBasis, and every line itstaxRate. These are statements. The platform computes no tax amount (order-machine-api.md) - Order write-back —
restoreCustomerCancellation: truereopens an order the customer called off, and needs bothnotifyandshowCustomer. Two new refusals:restore-not-announced, andpersonal-data-removedfor an order whose details are gone, which nothing moves or changes again (a payment can still be recorded) - Customer exchange — declining a registration in the panel removes it, so a run never meets one. Declining is refused anyway while the exchange owns customers
- Stored runs — a customer sync run keeps the rows it carried and changed for one day. The run and its counts stay
Configuration
deployment.json — four new blocks are required. A config written before this release will not load until they are added:
"consent": { // which purposes ask for consent (NFR-LEGAL-09)
"contact": false, // the contact form
"account": false // registration and an invited holder's first password
},
"terms": {
"attachToReceipt": true // the terms as a PDF on the order confirmation
},
"retention": { // in days
"consentRecordDays": 1095, // a consent record, mostly counted from its withdrawal (config/README.md)
"destructionRecordDays": 1095,
"orderDays": 1095 // a finished order's personal details (see the note above)
},
"tax": {
"basis": "included", // included | added | none ("none" takes no rate)
"rate": 19, // the default rate, a percent
"statedAtPrices": true // also state the basis beside each product price
}All four are decisions for the shop, not defaults to copy. config/README.md explains each, including when a jurisdiction relies on consent at all.
New optional keys:
seller—{ name, registration[], contact? }, the footer's seller l...
v1.13.1
v1.13.1 — Pictures, framed and whole 🖼️
A patch on top of iteration 15. Getting product photos into the catalog took more steps than it should, and the storefront cut off the edges of any photo that was not square. Pictures can now be pasted into the admin panel and framed before they are uploaded, and the storefront shows every picture whole. One line of text was also added under the variants.
Storefront
- Pictures are shown whole. Product galleries, listing cards, the featured row and search suggestions now fit a picture inside its frame on white instead of cropping it to fill the frame
- A note under the variants. Wherever a product's variants are listed, a short line now follows the list
Admin panel
- Paste a copied file. Every place that takes an image or a file also accepts one pasted with Ctrl+V (⌘V on a Mac), as well as one dropped or picked: product pictures, category marks, document files, and the catalog and customer upload pages
- Frame a product picture before it is uploaded. Adding a single photo to a product opens a framer. It places the photo in a white square, which can be zoomed (30–400 %) and moved with the mouse, the wheel or the arrow keys, then uploads the result as a JPEG. Upload as is skips framing. Several files at once, and GIFs, are uploaded as they are, so a set of variant photos framed alike elsewhere stays identical
- Frame again. A product picture already in the gallery can be reframed from its stored copy, and the reframed picture takes its place. The old files are left for the media prune to remove
Configuration
Text blocks (config is version-neutral, so compare these against your own files):
app-text.json— 1 key added, a string to write:catalog.variants.note— the line under every list of variants
admin-text.json— 11 keys added, all strings to write:common.pasteHintandcommon.imageTileHint— the paste hints, where{keys}is the shortcutimageFramer.*— the framer dialogproductEditor.images.reframe— the Frame again action
mail-text.json— unchanged
deployment.json and the environment are unchanged.
Documentation
- README.md — status line names
v1.13.1
What's Changed
- Simple image handling in product editor + pasting files. Closes #343 by @vik-koch in #344
- Add a note line for variants (#305) by @vik-koch in #345
Full Changelog: v1.13.0...v1.13.1
v1.13.0
v1.13.0 — Iteration 15: What a visitor sees 🛍️
Closes iteration 15. The three releases before it handed the catalog, the customer accounts and the orders over to a source system, all work for an integrator. This one is for the people who open the shop. A category looks the same everywhere it appears. The main page opens on products instead of a list of categories. Search answers with pictures, prices and categories. A product can now say that it is a set of parts, or that it comes in colours.
The admin side gets the upkeep that a catalog of several hundred products needs. The product list can be narrowed to what a product is missing, so it works as a queue of things to fill in. A product leaves the catalog in steps. A deleted product no longer counts toward any figure the panel shows.
Requirements delivered (compared to v1.12.0)
The storefront
- FR-NAV-07 — the footer links to the shop's other presences: a social account, a messenger, a marketplace shop
- FR-CAT-07 — a category is one chip wherever it is shown, with an optional square mark beside its name
- FR-CAT-09 — a featured row of five products on the main page, with admin-picked products drawn first
- FR-CAT-10 — a product sold as a set of parts, whose attributes can describe one part
- FR-CAT-11 — a product's variants are named on its pictures
- FR-CAT-12 — a variant can be marked as currently unavailable instead of being removed
- FR-CAT-13 — where a picture is too small to carry a label, a mark beside the product opens its variants
- FR-SEARCH-06 — a suggestion shows the product's picture and the viewer's price, and can add the product to the cart
- FR-SEARCH-07 — categories that match the query are suggested as their own group
- FR-SEARCH-08 — a query typed on the other keyboard layout still matches
- FR-ATTR-12 — the catalogue index has its own list of filters, which an admin chooses
- FR-ATTR-13 — subcategory chips carry the current filters forward and are greyed where those filters leave them empty
- FR-DOC-05 — a document can be a link to where it is held, such as a public register, instead of a file
The admin panel
- FR-ADM-19 — a category's count covers its whole subtree, as the storefront listing does
- FR-ADM-20 — the product list can be narrowed to what a product's content lacks and what it carries
- FR-ADM-21 — a deleted product can be deleted permanently
- FR-ADM-22 — deleted products are kept out of every count and list unless they are asked for
Amended
- FR-NAV-03 — the conditions page states the delivery zones, pickup points and payment methods that checkout offers
- FR-CAT-01/02/03 — the main page names each category's first subcategories. The catalogue index is a listing of every product. A listing says how many products the current filters leave
- FR-SEARCH-02 — search matches categories as well as products
- FR-SEARCH-04 — a listing with subcategories sorts by category by default, following the category tree
- FR-ADM-01 — removing a product is three steps: unpublish, delete, delete permanently. A published product cannot be deleted
- FR-ADM-02/10 — a run restores only products that a run deleted, and while the catalog is owned, an admin restores only products an admin deleted
- FR-ADM-04 — maintenance mode closes the shop to customers too: they can neither sign in nor reach their account
- FR-ADM-05 — the product list's states are All, Live, Unpublished and Deleted. Unpriced is now one of the content gaps
- FR-ADM-06 — an admin can open the page of a product the public cannot see, and the unpublish confirmation names any products that would lose their last counterpart
- FR-ATTR-02 — an attribute that names one part of a set is filtered under its plain key
- FR-CART-08 — a cart line can open the product's variants
- FR-DOC-01/03 — a document has a file, a link or both
Withdrawn
- FR-CAT-08 — the category description. On a second look, no page had a place for it. The column has been dropped (see Database)
Architecture decisions
- ADR 0063 — a category is shown as a chip, and its picture is retired
- ADR 0064 — a product's variants are names on its pictures, not separate articles
- ADR 0008 amended —
appTextButton, the second link style: for a control that stands on its own rather than a word inside a sentence - ADR 0023 amended — maintenance mode closes the shop to customers, not only to visitors
- ADR 0025 amended — deleted products leave the storefront's edit-mode overlay
- ADR 0027 amended — the conditions page joins the contact page as a code route with an editable body
- ADR 0031 amended — server rendering is done as the visitor, so a signed-in customer's first page already shows their own prices
- ADR 0039 amended — the conditions page reads the same delivery zones as checkout
Database
Eight migrations, all applied automatically on deploy. Two of them drop columns, and the data in those columns is not kept anywhere.
0039_category_mark—categories.mark0040_drop_category_image— dropscategories.image. The media prune sweep then deletes the old category photos on its next run. If you want to keep category photos, back up the media store before upgrading. Nothing turns an old photo into a mark: a photo cropped to chip size is unreadable0041_product_parts—products.partsandproduct_attributes.part0042_product_featured—products.featured0043_catalog_attributes— the catalogue index's own filter list. It starts empty, so/catalogoffers no filters until an admin chooses some0044_product_variants—products.variants0045_drop_category_description— dropscategories.description. No page ever displayed it, but an admin may have typed text into it. Back up the database first if that text matters0046_document_link—documents.link. A document's file columns become nullable, with a check that a document has a file, a link or both
The machine surface
No routes, scopes or payload shapes changed. One behaviour changed. The run's sweep and the admin each undo only their own deletions:
- A run no longer restores a product that an admin deleted. While the catalog is owned, an admin cannot restore a product that a run deleted
- Only a product that a run deleted can be deleted permanently while the catalog is owned, because the source would otherwise send it again as a new product
A run's deletion is recorded as a deletion with no admin attached. One edge case affects existing data: a product an admin deleted, which a run then restored and later swept away again before this release, may still be marked as the admin's deletion. If the source sends it again, it stays deleted until an admin restores it.
The storefront's search suggestions now depend on who is asking, because each product carries the viewer's own price. Do not put a shared cache in front of them.
Configuration
deployment.json — two new keys, both optional. A config written before this release keeps loading:
"search": {
"alternateLayout": "de" // the second keyboard layout visitors type on by mistake; leave out for none
},
"elsewhere": [ // the footer's links to the shop's other presences; leave out for none
{
"label": "Marktplatz", // what a screen reader announces
"url": "https://marktplatz.example/shops/coffee-kontor",
"icon": "marktplatz.svg" // a single-colour mark in the assets mount
}
]Both are values to decide. Each elsewhere icon is a file next to the logo in the assets mount. A mark that is not square states its proportions with "width" and "height". See config/README.md.
Environment — MAIL_DELIVERY is new. Leave it unset: mail is handed to a queue and the request no longer waits for the mail provider. Only the api-e2e run sets it to inline.
Text blocks (config is version-neutral, so compare these against your own files):
app-text.json— 28 keys added, 2 removed, 1 reworded. All of them are strings to write:- Added:
home.categoriesHeadingandhome.featured.*(the featured row)catalog.productCount, andcatalog.sort.categorywithcatalog.sort.fields.*catalog.set.*andcatalog.variants.*catalog.documents.linkLabelsearch.addSuggestion,search.categoriesLabel,search.showAllResultsandsearch.suggestionInCartconditions.paymentHeadingandconditions.cashDescriptioncheckout.fulfilment.noDeliveryauth.closedandauth.setPassword.closed/closedHeading(maintenance mode)
- Removed:
catalog.overviewTitleandcatalog.overviewIntro - Reworded:
catalog.documents.hint
- Added:
admin-text.json— 89 keys added, 8 removed, 7 reworded:- Added:
- the content filter:
productList.missing.*,productList.has.*,productList.gap.*and their group labels - result counts on every grid:
productList.count,orderList.count,userList.count,documentList.countandsync.historyCount - removing a product in steps:
common.catalogErrors.product-{published,deleted,not-deleted,ordered},editMode.purgeProduct/purgeConfirm,ownership.productPurgeandownership.productRestore* - unpublished counts:
common.unpublishedProductsandeditMode.unpublishStranded - deleted products:
productList.deletedMatches,productList.showDeleted,categoryList.deleteReassignDeleted,ownership.categoryHasDeletedProductsandattributeInventory.renameDeleted - the product editor:
productEditor.parts.*,productEditor.variants.*andproductEditor.featured.* - category marks:
categoryEditor.mark/markHint - the catalogue's filter list:
categoryFilters.catalog*and `categoryList.catalogFil...
- the content filter:
- Added:
v1.12.0
v1.12.0 — Iteration 14: Orders are worked in the source system 📦
Closes iteration 14, and with it the exchange. v1.10.0 handed over the catalog, v1.11.0 handed over the customer accounts, and both of those are things the shop has. An order is something the shop does — and until this release, whatever else a connected system ran, somebody here still had to open the panel and answer every order by hand.
See docs/exchange-areas.md, which sets the three side by side.
Requirements delivered (compared to v1.11.0)
The order exchange
- FR-ADM-08 — an automated source reads order requests outward and writes their processing back, so an order can be worked end to end in that system
Amended
- FR-ADM-09 — a source can read one of its own runs back
- FR-ADM-10 — order processing is the third owned area. While it is owned, every staff action on an order is refused
- FR-ADM-17 — a customer run may claim an account by the platform's own identifier
- FR-ORD-02 — an owning system reports where an order stands rather than taking a step
- FR-ORD-03 — an adjustment may arrive from an owning system
- FR-ORD-05 — the shop's own paperwork arrives as bytes
- FR-NOTIF-09 — the order exchange sends two messages: failed and recovered
- NFR-LEGAL-07 — the disclosed transfer now names the orders those accounts place as well as the accounts themselves
- NFR-SEC-09 — names the order exchange among the machine clients
Architecture decisions
- ADR 0062 — an order write-back is one version, applied as it arrives
- ADR 0056 amended (third amendment) — orders as an owned area, and the two carve-outs the customer area did not raise
- ADR 0053 amended — five token scopes now, with the rule that reading and writing are never one capability
- ADR 0060 amended — what the third area did not inherit: no work queue, no staged runs, no review buttons. The log is a record, not a queue
Database
Six migrations, applied automatically on deploy. One moves data — 0034_order_updated_at backfills rather than stamping every row with now(), because a book of orders that all changed at the same instant has no ordering at all — and it applies unattended like the rest
0033_ownership_area_orders—ordersjoins theownership_areaenum0034_order_updated_at—orders.updatedAtplusorders_updatedAt_idxon(updatedAt, id): the key the outbound read pages by0035_api_token_order_read_scope— theorder-readscope0036_order_revision_source—order_revisions.source, the opaque actor label0037_sync_area_orders—ordersjoins thesync_areaenum0038_api_token_order_sync_scope— theorder-syncscope
The machine surface
New paths, beside the catalog and customer ones:
GET /api/machine/orders— the outbound read, cursor-paged and resumable, ordered byorders.updatedAtGET /api/machine/orders/{reference}— one order by reference, for the moment a write-back is refused for naming a version the order has moved pastPOST /api/machine/sync/orders/runs— the write-backGET /api/machine/sync/orders/runs/{id}, and the same read-back for the catalogPOST /api/machine/sync/orders/failures— an adapter reporting its own breakagePOST/DELETE /api/machine/orders/{reference}/documents/{kind}— the real invoice as a file
Two scopes are new — order-read to read, order-sync to write — and the ownership gate splits the same way: reads answer whether or not orders have been handed over, writes are refused with orders-not-externally-owned until they have.
Configuration
deployment.json — one new ceiling beside the existing customer ones:
"sync": {
"customerAutoApply": {
"maxInvites": 25,
"maxDisables": 0,
"maxClaims": 0,
"maxIdClaims": 0 // accounts a run may claim by the platform's own identifier
}
}It has a default, so a deployment that wrote its config before this release keeps loading. Zero, like the address ceiling beside it: stage it and ask.
Text blocks (config is version-neutral — a deployment reviews these against its own files):
admin-text.json— async.ordersblock (counts, the four answer kinds, the nine row refusals),sync.areas.orders,ownership.areaText.orders.*and theownership.orderLockedbanner,apiTokens.scopes.{order-read,order-sync}plusscopeKind/scopeAreafor the new capability grid,panel.{ordersOwned,fullyOwned},orderDetail.revisions.openOrder, and the claim-by-id strings (claimedById,claim-id,claimByIdWarning, three row refusals). Reworded: the customers area description said orders "stay yours either way" — false nowmail-text.json—syncRun.areas.orders, withfailedandrecoveredonlyapp-text.json— unchanged
Privacy page copy (NFR-LEGAL-07). A third section is seeded: the orders an account places, the purpose, and the recipient
Admin panel
- A third ownership area, closed whole. Handing order processing over closes the move, the adjustment and its preview, the payment tick, telling the customer, and the document supply/remove
- The order list takes a note above the grid rather than a dialog per row
- The adjust screen draws no form at all while orders are owned — a banner and a way back
/admin/sync/orders— the order-sync log, readable by managers as well as admins- A version now names who wrote it — a staff author where there is one, otherwise the source label the exchange supplied
- The token form is a grid: areas down (catalog, customer accounts, orders), read and submit across
Documentation
- docs/order-machine-api.md — new, the third wire-level reference: credentials, the read, the write-back, the document routes, every refusal code with its params, the transition table as a machine may use it, and an adapter checklist
- docs/order-sync.md — new. Five journeys, generated from the same data the end-to-end suite runs
- docs/exchange-areas.md — new. The three areas in one table, plus where they genuinely differ
- docs/mail.md and the gallery gain the two order-sync previews
What's Changed
- Update requrements for iteration 14 by @vik-koch in #268
- Add sync run read-back for machines. Closes #265 by @vik-koch in #269
- Claim customer account by identifier. Closes #266 by @vik-koch in #270
- Orders external ownership. Closes #271 by @vik-koch in #272
- Implement machine orders read-back endpoint (#273) by @vik-koch in #274
- Implement machine orders upload endpoint. Closes #273 by @vik-koch in #275
- Implement order document upload with a machine endpoint. Closes #276 by @vik-koch in #277
- Prepare 1.12.0 release (#278) by @vik-koch in #279
Full Changelog: v1.11.0...v1.12.0
v1.11.0
v1.11.0 — Iteration 13: Customer accounts travel with the catalog 👥
Closes iteration 13. In v1.10.0 the source system took over the catalog, and a deployment could run its shop almost hands-off — except for the customers. Who may sign in, and which price group they are charged, were still decided by a manager in the admin panel. They no longer have to be.
A connected system can now do everything a manager can do to a customer account: invite one into being, approve or decline a registration, set its price group and company details, switch it off and back on. It can also read the accounts back out, so somebody who registers on the website is not invisible to the system that has to invoice them.
The feed is now area-scoped rather than catalog-shaped: one run lifecycle, one log, one policy mechanism and one set of notifications, with catalog and customers as values of an area — and orders waiting to become the third.
Requirements delivered (compared to v1.10.0)
See docs/requirements.md for full text — which now carries an explicit anchor per requirement and a generated Contents block, so a link to one survives a retitling.
The customer exchange
- FR-ADM-11 — an automated source does, over the machine endpoint, everything a manager can do to a customer account, so a deployment can be run without anybody opening the admin panel
- FR-ADM-12 — an admin bulk-imports customer details from a file, producing the same staged run, preview and audit record a catalog upload does, and reaching no further than the exchange does
- FR-ADM-13 — an account the exchange asks for is created invited, and the set-a-password link is sent from here to the person themselves; the exchange can neither set a password nor delete an account
- FR-ADM-14 — a customer account is matched by its private
sourceIdand never by email address or company registration id; an admin can set, correct and clear that key by hand - FR-ADM-15 — what the account holder maintains (name, phone, password, the deletion of their own account) travels outward only and is never written from outside
- FR-ADM-16 — a repeated or out-of-date instruction changes nothing and notifies nobody, in every area
- FR-ADM-17 — a run may claim an account somebody registered on the shop: where the key is unknown and the shop holds an account with that email address and no key of its own, the run adopts it instead of refusing the row or creating a second account beside it
- FR-ADM-18 — the source system reads the shop's customer accounts outward: keys where they exist, state, the details the account holder maintains, and the price list each is charged. Its own capability on the credential, separate from the one that writes, and it works whether or not an external system owns customers
- FR-AUTH-11 — a registration can be declined, which switches the account off rather than leaving it waiting, and is reversed by approving it later
- NFR-LEGAL-07 — transferring account details outward is configured rather than assumed, and the privacy page names the transfer, its purpose and the category of recipient
- NFR-LEGAL-08 — deleting an account reaches only as far as this platform: cleared here, reported outward as withdrawn, with no claim about the receiving system's own record. The source key is deliberately kept on the cleared row so a later run cannot recreate what the person asked to have removed — which means the honest word for the outcome is "closed and cleared", not "anonymous", and the privacy page now says so
Amended
- FR-ADM-07 / FR-ADM-09 / FR-ADM-10 — the feed, the log and the ownership switch are per area rather than about the catalog: a customer run is staged, reviewed, applied and audited exactly as a catalog run is, and who may read a run is whoever may do that area's work by hand (catalog: admin; customers: admin and manager)
- FR-ADM-02 — a category the exchange has never named is the shop's own: its key is optional, and a keyless category stays renameable while the catalog is externally owned
- FR-AUTH-01 / FR-AUTH-04 — while customers are externally owned, the admin panel's side of account administration is closed completely rather than field by field, and the screens stay readable throughout. Registering stays the person's own act and stays open however customers are owned — which is exactly the case FR-ADM-17's claim exists for
- FR-NOTIF-09 — the exchange's health mails are per area: a broken catalog feed and a working customer exchange are two independent facts, and the wording carries a section per area rather than a noun to substitute
- FR-WORK-02 — one queue of staged runs per area, so a customer run waiting for review is its own piece of work
Architecture decisions
- ADR 0060 — give a run an area, not a second set of machinery: one
sync_runstable, one run contract, one run page; a log screen per area at its own slug, and a new area gets a machine path of its own beside the published ones rather than anareafield in an existing body - ADR 0061 — the exchange issues no credential and never deletes: no password field anywhere in the customer row,
disabledas the strongest statement about access, a withdrawn account keeping its key and refusing the row that names it, and a staff account never being a customer under any setting - ADR 0056 amended — customers are the first area whose owned-field list is "all of them, and the buttons too", so the panel offers a master switch over the areas that is derived from them rather than a fourth stored flag; and a category the exchange never named is outside both the exchange and the switch
- ADR 0057 amended — feed state is read per area, the words are per area while the machinery is not, and the fourth message ("a run brought records needing attention") deliberately has no customer counterpart: an invited account has already been mailed its own link and leaves nothing on a desk
Database
Four migrations, applied automatically on deploy. One of them moves data — 0030_category_source_id_optional — but it only clears keys that bound nothing, so this release applies unattended.
0029_sync_area_and_customer_source_id— async_areaenum and anareacolumn onsync_runs(defaulting tocatalog, so every existing run keeps its meaning),users.sourceIdwith a unique constraint, andcustomersas a value of the ownership area0030_category_source_id_optional—categories.sourceIdbecomes nullable, and the mintedmanual:<uuid>keys that admin-created categories carried are cleared. No export contained them, but the ownership rule read them as identity, so a category an admin had just made could never be renamed while the catalog was owned0031_api_token_customer_sync_scope— thecustomer-synctoken scope0032_api_token_customer_read_scope— thecustomer-readtoken scope, separate from the one that writes (FR-ADM-18)
The machine surface
New paths, beside the published catalog ones rather than replacing them:
POST /api/machine/sync/customers/runs— submit customer rows; same staged/applied lifecycle, same policy check, same audit record as the catalog feedPOST /api/machine/sync/customers/failures— an adapter reporting its own breakageGET /api/machine/customers/accounts— the outward read, cursor-paged and resumable across runs
A submission body may now be up to 10 MB on every machine sync path (SYNC_MAX_BODY_BYTES), not just the catalog one; the 50,000-row cap (SYNC_MAX_ROWS) is shared by both areas. Writing needs the customer-sync scope, reading customer-read; a write is refused with customers-not-externally-owned unless the area has actually been handed over, while the read is ungated by ownership.
Configuration
deployment.json — one new block, sync.customerAutoApply, the ceilings a customer run is measured against before it may apply itself:
"sync": {
"customerAutoApply": {
"maxInvites": 25, // accounts one run may invite into being
"maxDisables": 0, // accounts it may switch off — zero until written down
"maxClaims": 0 // accounts it may claim by address — zero until written down
}
}Every ceiling has a default, so a deployment that wrote its config before this release keeps loading. The two that take something away from a person — switching an account off, and claiming an account somebody registered here — default to zero, which means "stage it and ask".
Text blocks (config is version-neutral — a deployment reviews these against its own files):
admin-text.json— async.customersblock (the upload, its presets and options, the change kinds, the counts, and one sentence per row refusal),sync.areas.{catalog,customers}naming each area's log, a much largerownershipblock (areaTextper area, the master switch underownership.all, and the locked notes a closed screen carries),userDetailfor the new read-only customer page,userEditor.sourceId, the two new token scopes andpanel.customersOwned. Removed:ownership.areas.catalog,ownership.areaDescription.catalog,ownership.statusOwnedEffect,ownership.statusOwnEffect, andownership.{take,hand}{Title,Confirm,Warning}— all of them replaced by their per-area equivalents underownership.areaText.mail-text.json—syncRun.kinds.*moves tosyncRun.areas.catalog.*, andsyncRun.areas.customers.*is new: three messages (failed, recovered, waiting) against the catalog's four. Shared labels stay shared.app-text.json— unchanged. The one customer-visible change in this release is privacy-page copy, which ships as page content rather than as text keys.
Privacy page copy (NFR-LEGAL-07/08). Two new sections are seeded: the outward transfer of account details, and what deleting an account does and does not reach. They ship only to fresh deployments — page ...
v1.10.0
v1.10.0 — Iteration 12: Automated catalog sync from the source system 🔄
Closes iteration 12. Until now the catalog moved by hand: an admin exported a file from the source system, uploaded it, read the diff and applied it. The shop's own system can now feed the catalog directly. A run applies itself only while its effect stays inside a policy the deployment writes down.
An admin says who owns the catalog. While an external system owns it, the fields it maintains are read-only in the admin panel and the manual upload is refused; taking the catalog back is one switch in the admin panel and needs no deploy.
Two pricing changes ride along, both of which the real export forced: a stored price is now the price of one piece, and a price is a row in a price list rather than a column on a product.
Requirements delivered (compared to v1.9.0)
See docs/requirements.md for full text.
The automated exchange
- FR-ADM-07 — an automated source submits the same catalog import over an authenticated machine endpoint
- FR-ADM-09 — the admin panel shows the log of sync activity: what ran, what it changed, what it skipped and what failed
- FR-ADM-10 — an admin declares that an external system owns an area of the data
- FR-NOTIF-09 — the admin is emailed when the exchange stops working, when it works again, when a run is waiting for a decision, and when a run brought records needing attention
- NFR-SEC-09 — machine tokens are stored hashed, scoped to what they may do, revocable from the admin panel, and rate-limited apart from the interactive endpoints
- NFR-OPS-06 — what a deploy costs in downtime, how to tell it failed, and how to roll back, written down in infra/README.md
- NFR-OPS-07 — an interrupted sync leaves the platform serving its last consistent state: a run applies in one transaction, and a run that died before it became one is recorded as failed rather than vanishing
Amended
- FR-ADM-02 — a run states which fields it writes (all of them, or prices only, or stock only), so an export that does not carry a field cannot blank it
- FR-ADM-04 — maintenance mode no longer closes the door on the machine endpoint
- FR-ADM-06 — a product the default price list does not price cannot be published at all, and clearing a published product's price takes it off the storefront
- FR-AUTH-05 — exactly one price list carries the default badge: guests, staff and customers without a tier are charged it, and a list that does not price a product falls back to it
- FR-UNIT-04 / FR-UNIT-10 — a stored price is the price of one piece and every other unit's price is that figure multiplied out, so nothing a customer or a member of staff sees is rounded
- FR-WORK-02 — two new queues: sync runs staged for review, and products no price list prices
- FR-CAT-01 — a category is a grouping of products, so one with nothing publicly visible beneath it is not shown, not linked in the navigation and not offered to a crawler
- FR-ADM-09 — an automated source may attach a note to a run it did produce — a file it could only read part of, an export that looked stale — kept verbatim beside the run and shown whatever became of it
Architecture decisions
- ADR 0053 — authenticate automated clients with scoped, hashed machine tokens
- ADR 0054 — put the source-system exchange behind an inbound adapter: the platform's contract is public and the format-specific adapter is not
- ADR 0055 — let an automated run apply itself only within a declared policy
- ADR 0056 — make external ownership an operator switch with an area, not two toggles
- ADR 0057 — tell the shop about an exchange on a change of state, not on a run
- ADR 0058 — price a product per piece, not per a stored basis
- ADR 0059 — a price belongs to a (product, price list) pair
- ADR 0026 amended — the headless entry point, and who may commit a run
- ADR 0035 / 0038 / 0042 amended — the price basis is gone; an order line stores a piece price; the unit lens reads one
- ADR 0036 amended — what the publication gate is now for
Database
Six migrations, applied automatically on deploy. Two of them move data — 0026_price_per_piece and 0027_price_lists_as_rows — so this release is worth a backup beforehand, even though both apply unattended.
0023_api_tokens— theapi_tokenstable: name, scopes, prefix, hash, who created it and when it was last used. A token is revoked, never deleted, so the runs it made keep naming it.0024_headless_sync_runs— a run may now come from a token rather than a person, may be staged with a reason (policyorrequested), and may end asno-change,supersededordiscarded. The staged diff is kept with the run.0025_data_ownership—externallyOwnedAreason the settings row, and asetting_changestable recording every flip of maintenance mode and of ownership, with who flipped it.0026_price_per_piece— divides catalog prices down to the price of one piece and drops the basis columns. Because the division rounds, every product whose basis was larger than one piece is unpublished by the migration and waits in the publication queue for somebody to look at its price. Order lines are history and are not touched.0027_price_lists_as_rows— the base price stops being a column onproductsand becomes a price list like any other, holding adefaultbadge that exactly one list carries.0028_sync_run_notice— a nullablenoticecolumn onsync_runs, for the note an automated source may attach to a run.
Configuration
deployment.json — one new block, sync.autoApply, which is what a run is measured against before it may apply itself:
"sync": {
"autoApply": {
"maxCreates": 100, // products one run may add
"maxSoftDeletes": 0, // products it may hide — zero until written down
"maxChangedShare": 0.5, // share of the live catalog it may touch
"maxCategoriesCreated": 5, // groupings it may add
"maxCategoriesEmptied": 0 // groupings it may leave empty
}
}Every ceiling has a default, so a deployment that wrote its config before this release keeps loading; the two that take part of the storefront apart — hiding products and emptying categories — default to zero, which means "stage it and ask a person".
Text blocks (config is version-neutral — a deployment reviews these against its own files):
admin-text.json— newapiTokenList(issuing, copying and revoking a token),ownership(handing the catalog over and taking it back, and the sentence a locked field carries),operations(the admin's operations page and its history of switches), and a much largersyncblock: the run log, one run's own page, the staged reasons, the field names a run writes, the failure and note sections. AlsotierListgaining the default badge and the unpriced-products links, andpanel.workSyncRuns/panel.workUnpricedProducts. Removed:orderDetail.basis,productEditor.packaging.priceBasis/.pricePerPiece/.basisMustDivide,orderAdjust.lines.priceSuffix/.unitsSuffix,orderAdjust.errors.line-not-priceable,tierList.defaultLabel,sync.historyTitle,panel.site.mail-text.json— a newsyncRunblock: one heading, subject and body per kind (failed, recovered, waiting, created), plus the labels the run summary uses.app-text.json— unchanged. Nothing about this iteration is visible to a customer.
Documentation
- docs/catalog-sync.md — new. What the feed does, as journeys generated from the same data the e2e suite runs, plus what a half-finished run leaves behind and what the feed deliberately cannot do.
- infra/README.md — a deploy's downtime, how to see that it failed, and how to roll back by hand.
- CI fails if the generated journeys go stale.
What's Changed
- Machine token integration. Closes #227 by @vik-koch in #228
- Automated catalog sync and revised sync UI. Closes #229 by @vik-koch in #230
- Ownership toggle and refactored operations admin page. Closes #231 by @vik-koch in #232
- Notifications for automated catalog syncs. Closes #233 by @vik-koch in #234
- Remove priceBasisPieces. Closes #235 by @vik-koch in #236
- Refactor default price handling and improve price tiers usability. Closes #237 by @vik-koch in #238
- Prepare 1.10.0 release. Closes #239 by @vik-koch in #240
- Fix texts in order adjustments non-localizable (#240) by @vik-koch in #241
- Add auto-sync warning text and improve element spacing. Closes (#239) by @vik-koch in #242
Full Changelog: v1.9.0...v1.10.0