69c63dfff3ee824df173144894650453fa243d49
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69c63dfff3 |
Read the wedding table the way the spreadsheet meant it
Its lines were carried over literally in V4, and the source is offset: the prices sit on its "cupcakes"
line and "4 dz cc or sc" on its "12x17 bars" line. Taken at face value that produced five lines, two of
which had no quantity in any column and rendered as a row of dashes.
Read as a baker would — a 12x17 pan IS the sheet pan, and "cc or sc" is what the dozens are counting —
it is three lines that each say something:
Bride & groom cake (8 in) B&G cake B&G cake B&G cake
Sheet cakes or 12x17 bars 2 pans 3 pans 4 pans
Cupcakes or sugar cookies 4 dz 5 dz 6 dz
A NEW migration rather than an edit to V4, because V4 has already been applied and Flyway validates
checksums. And every statement in it is guarded on the exact text V4 wrote, so if the bakery has already
reworded a line in the admin, this finds nothing to change and leaves their wording alone. Verified that
way and not just in the abstract: renamed one line through the admin, then let V5 run over that database
— the renamed line and its cells survived untouched while the untouched line was corrected and the two
empty ones removed.
This is still an interpretation of somebody else's shorthand. It is three boxes in
/admin/catering/tables/3 to change if Morissa reads it differently.
The office table's "Mini cinnamon rolls" line is the same shape — named, never quantified, and the note
under the table ("minimum of 6 items per baked good") is all the source says about it. Left alone: it
would need a number nobody has given us.
|
||
|
|
ad8ad724a3 |
Make the catering page look like it belongs to the shop
It was correct and plain: a heading, three white cards, a bare table in each. Now it reads like the rest of the site, using only what the site already has. - A short photographic band, same treatment as the homepage hero — blurred cake photo, sage wash, an eyebrow line above the heading. Short: the content is three lines, and 34svh of dark left the title floating in a void. - Prices are the loudest thing in each table, set in the display serif above a small-caps size label, over a ruled header band. Zebra rows below, the line names in the left column. - The branch from the wordmark divides the intro from the tables — it is a mask over currentColor, so it takes the colour of the text around it and costs one span. - Small print and the enquiry link share one tinted card footer. As separate full-width bands they made every card read as four stacked stripes. The link is quiet: three filled buttons down a page shout, and the real call to action belongs once, in a closing sage band with the terms and the phone number. - The blank-cell dash was so faint that a line with no quantities looked like a rendering fault. Checked by rendering it, at 1280 and at 390: hero, tables, the phone's one-card-per-size stack, the closing band. All 197 classes the page uses resolve in the compiled stylesheet, so nothing is styled by accident. Two notes for anyone doing the same: headless Chromium screenshots this site mid-fade unless you pass --virtual-time-budget (the body fades in), and a very tall window makes min-h-screen stretch the flex column, which looks like a gap above the footer and is not one. |
||
|
|
faf7bd9f18 |
One form per row in the admin, and buttons that don't trip its validation
The catalogue screen had a form per control: 467 forms, 464 CSRF tokens, 317 KB of HTML for a page that gets opened on a phone in a bakery. It is now one form per row with several submit buttons — the same `name="do"` pattern the catering table editor already used — which is 89 forms and 206 KB, and less markup to read. Only `save` looks at the text boxes, so moving a row cannot save a half-typed name. The buttons that ignore them carry `formnovalidate`, because the name box is `required` and a browser would otherwise refuse to submit "move down" while that box was empty — a validation error about something the button has nothing to do with. Verified on the running container: 89 forms, move and photo-remove still land, and the seeded catalogue comes back the same after a database reset. |
||
|
|
5045ccc7c8 |
The admin is Thymeleaf too: no JavaScript framework left in the repo
The last React went with this. /admin and /admin/catering are pages of forms; every write is a POST and a redirect back, so the back button and reload do what they look like they do, a double-tap cannot repeat an upload, and there is no client-side state to lose — a reload is always the truth. The /api/admin/** endpoints went too: they existed for the React screen, and their logic now lives in Catalogue (extracted from the two deleted JSON controllers) and CateringMenu, which the pages call. THE TABLE EDITOR IS THE INTERESTING PART, because a catering table cannot be edited a field at a time — a column heading, its price and the entries beneath it only mean anything together. One form holds the whole table and every button submits it; `name="do"` says which was pressed and its value carries the position (`remove-column:2`). "Add a column" therefore arrives with every cell the editor has typed, adds the column to what arrived plus an empty entry on every line, and re-renders. Nothing typed is lost, and only Save writes — so a half-built table with a blank heading never reaches the live page. A failed save comes back the same way, with the work still in the form and the reason above it; a redirect would throw the work away and leave them guessing which cell the message was about. Spring binds `lines[2].values[1]` into the right cell, which flat repeated parameters could not promise. Reordering moved to the server, where it always belonged: the browser used to compute the new order and send the whole list back, and now "move this up" arrives as an action. Same for arranging photos — one endpoint takes the key and -1/1/0 (earlier, later, remove), because those three buttons are the same edit. frontend/ became styles/: node, Tailwind and nothing else. It exists because Tailwind needs a compiler and the alternative is a hand-written stylesheet; there is no bundler and no framework. The admin's controls are @utility classes (v4 will only let you @apply a registered utility, and only a utility can take the `file:` variant the photo pickers use) — the same buttons the React screen had, from the same class strings it composed. Also fixed .gitignore, which still named frontend/: with styles/ unlisted, `git add -A` staged 1,626 files of node_modules. Verified against a running container, not only in tests: pressing "+ Column" returns the draft with an unsaved cell intact, a new column and a matching new entry on the line, "Not saved yet" — and the live page unchanged; Save then writes both columns with the price parsed from "48". Renaming and moving an item land on the products page. Deleting a category that is in use is refused with the sentence naming it. Removing the only photo of an item is refused, and that button is already disabled in the page. 51 tests (10 new): the form binding, the flash on success and on refusal, a structural button writing nothing, and the whole admin surface closed to anonymous visitors. PlatformContractTest's routing assertion now says what is true — an unknown path 404s, and so does /admin when no identity provider is configured, because AdminController only exists under OIDC. |
||
|
|
70af2922f5 |
The site renders itself: Thymeleaf pages, and a catering page among them
The public site was a React SPA. It is now server-rendered Thymeleaf, and the goodie box and catering tables added in the previous commit have a page of their own. The look is unchanged: the templates carry the same Tailwind classes the components did, and every one of the 241 classes the five pages use resolves in the compiled stylesheet. WHAT WENT AWAY. PageMetaController — 148 lines whose only job was to splice per-page <title> and OG tags into one shell with regular expressions, with a test that read the real index.html so that reformatting it failed the build instead of silently breaking the rewriting. A page rendered on the server writes its own head. Also react-router (no client-side routes left), motion, vite-plugin-svgr, and the SPA fallback (platform.web.spa.enabled=false): with the site server-rendered, forwarding a mistyped URL to /index.html would answer with a blank admin shell and a 200 instead of the site's own 404 page. WHAT GOT BETTER ON THE WAY, none of it visible. The category filter is a ?category= link, so every filtered view is a URL you can send someone and a crawler can reach all forty items instead of the twelve the default filter showed. The contact form is a form post: the enquiry is recorded before delivery is attempted, and a refused relay re-renders the page with what the visitor typed still in the boxes. The mobile menu is a <details> — the React version needed four effects to close on navigation, close on Escape, stop the page behind it scrolling, and unmount (a panel parked off-screen still extends the scrollable area, which is how you used to be able to scroll sideways and find the menu); a new document cannot inherit an open menu. THE PUBLIC SITE SHIPS 5 KB OF JAVASCRIPT, and works without it. The product cards are scroll-snap strips, so the photos swipe on a phone and scroll with a trackpad unaided; gallery.js adds the arrows and the dots, and creates them itself rather than having the template render controls that would sit there dead. Tailwind still needs its compiler, so npm remains a BUILD tool: the CLI compiles the templates into static/css/site.css at process-classes (so `spring-boot:run` gets it too), and frontend/ now builds only that stylesheet and the admin. The brand tokens are one file both stylesheets import — the alternative was the shop front and the screen that edits it drifting a shade apart. The stylesheet URL carries ?v=<sha>, because one hand-written CSS file has no content hash and a deploy has to be able to tell a browser that what it cached is stale. The admin is still React and is untouched, apart from losing the router it no longer needs. It is an editor, not content. PlatformContractTest stopped inheriting platform-starter-test's contract and restates it. The shared version asserts that an unknown path forwards to the SPA shell, which is no longer true here, and its test methods are package-private so it cannot be overridden. The platform should decide that assertion from platform.web.spa.enabled — noted in the file. 9 new tests (46 total): every page's real title and og:url, the catalogue and the catering tables in the HTML rather than fetched afterwards, server-side filtering, the 404, and that a crafted ?about= link cannot put words of its own choosing in front of a customer. |
||
|
|
24d26c2bc9 |
Catering tables: the spreadsheet becomes data the bakery can edit
The goodie box and catering prices arrived as a spreadsheet — Office, Parties and Weddings, each a
few columns of sizes and prices with lines of baked goods underneath. This puts it behind
/api/catering and makes every part of it editable at /admin, because the prices move and the
spreadsheet's own last line says the tables are "mostly just an idea for people".
A package is one table, its tiers are the columns, its rows are the lines, and a line holds one
value per column. That alignment is why this is an aggregate rather than three tables edited
separately: drop the middle column on its own and every remaining entry shifts one place left, so
the Large box advertises the Medium box's contents at the Large price and nothing looks broken.
CateringPackage#arrange takes a whole table, renumbers positions from the order it arrived in, and
refuses an arrangement whose lines and columns disagree.
Money owns prices — what "24", "$24" or "24.50" means and how it prints — so the browser never
formats money and never multiplies it by 100 in floating point. Cents in the column, "$24" in the
response. An empty price is "ask us", not zero.
Seeded from the bakery's own wording. Shorthand is expanded ("4 dz cc or sc") and typos fixed, since
customers read these lines; in the wedding table the labels and the values are offset in the source
spreadsheet, so they are carried over literally and can be renamed in the admin. The lines that are
named but never quantified keep their blank cells: dropping the blanks would shorten the line and
shift everything after it.
The public response leaves out a table with no columns or no lines — adding a table and filling it
in are two separate acts, and the gap between them shouldn't put a bare heading on the live page.
No public page renders any of this yet; this is the backend and the editor for it.
Admin endpoints are @ConditionalOnProperty on SECURITY_MODE=OIDC like the rest, so a deployment with
no identity provider has no price writes. 18 new tests: the seeded spreadsheet, the alignment
invariant, money in both directions, and the HTTP surface the screen actually calls (including that
/packages/order isn't read as a table id, and that a refusal arrives as a ProblemDetail sentence).
|
||
|
|
91219efbaa |
Reconcile: your admin wins, keeping main's non-admin work
You built a self-service catalogue admin on feature/admin-and-ui-wins while I built a
competing one that had already merged and deployed. Both forked from
|
||
|
|
d2c62f35ed |
Admin for the menu and enquiries, plus gallery fixes
Admin - /api/admin: products CRUD, the enquiry inbox, and presigned photo upload straight to the bucket so images never pass through the app. Gated by platform.security.authenticated-paths = /api/admin/**, so any signed-in Authentik user is staff — the alternative is a role model a two-person bakery would never maintain. - /api/me is deliberately PUBLIC. The SPA asks on every page load, and requiring a login would bounce every anonymous visitor to Authentik just to read the menu. - /admin screens: product list with edit and remove, an editor with drag-free photo reordering and upload, and an enquiry inbox that flags anything the relay refused. Gallery - swipe on touch devices, which the react-awesome-slider it replaced had and this did not, plus arrow keys and position dots — with swipe there is otherwise nothing to say a card holds more than one photo. Vertical drags are ignored so page scrolling still works. - @BatchSize on the photo collection: the products page loaded the whole catalogue and Hibernate issued a query per product for its images, forty-odd round trips for a page that needs two. Three things the tests caught, none of which are obvious: - Adding the storage starter broke every existing test. It activates on a default endpoint, so an S3 client is built even in tests and dies on blank keys. - MockMvc's webAppContextSetup leaves the security filter chain OUT, so the first version of the security test passed 200s and proved the opposite of what it claimed. It needs .apply(springSecurity()). - Turning on the security starter turns on CSRF — for the PUBLIC contact form too, which then 403s. The SPA now reads the XSRF-TOKEN cookie and sends X-XSRF-TOKEN, and there is a test asserting the form is rejected without it. |
||
|
|
f471462d05 |
Rewrite on the Bennett platform: Spring Boot + Vite/React SPA
Replaces the Next.js app. Same site, same look; the parts that were decisions rather than markup now live in Java. - catalogue, curated order, category filter and image URLs move from a TypeScript array into Postgres behind /api/products and /api/categories - contact form uses the shared platform-starter-contact: validate, RECORD, send, then fan out to n8n. Recording first means a relay outage costs a notification, not an enquiry - PageMetaController rewrites title/description/OG per route, replacing what Next's SSR gave crawlers and link-preview scrapers - 50MB of photos leave the repo for the MinIO bucket, re-encoded to webp (14MB) with EXIF (including phone GPS) stripped - fixes a catalogue typo: 'Strawberry Pie' was category 'Pies', which no filter matched, so it was unreachable unless browsing All |