Add HL3D branding, Etsy Live/Draft paging and Projects lifecycle
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s

Replace the old touchmark/PNG identity with the supplied lockup and
monogram, segmented role-aware navigation, Live/Draft listing filters
with generation-based cache invalidation, and admin Projects covering
immutable file revisions, listing live pointers, and exact-decimal
filament/packaging/shipping costing. Additive schema only; no Etsy
publication. Isolated clone tests: 301 tests, 1162 assertions.
This commit is contained in:
2026-11-08 12:50:56 +00:00
parent c8c5822166
commit d4928789c4
83 changed files with 5808 additions and 84 deletions
+22
View File
@@ -63,6 +63,28 @@ For local tests in the existing serviceengineering toolchain image, explicitly s
Etsy Open API v3 has no conversation inbox (`conversations_*` scopes do not exist). Receipt notes are exposed; sending a buyer-inbox reply is not.
## Projects (admin)
Projects are the product aggregate: a reversible internal lifecycle (Design, Ready, Live, Retired — distinct from Etsy's own listing state), approved immutable revisions with private STL/3MF/G-code file bundles, Etsy listing mappings verified by reading the listing from Etsy (never an arbitrary shop/listing ID), and append-only cost snapshots and releases.
- Uploads are limited to `.stl`, `.3mf` and `.gcode`, 100 MiB per file, 250 MiB combined per request, and 10 files per request, and are structurally validated (STL triangle/size cross-check, bounded 3MF zip/XML with no path traversal, symlink entries, external references or DOCTYPE/ENTITY declarations, plain-text G-code). Nothing uploaded here is ever parsed as a slicer job, executed, or served from a public URL — files live on the `project_files` disk (`storage/app/project-files`, not web-served) under a random generated key, streamed rather than buffered in memory, and only stream back through the authenticated, policy-checked `projects.files.download` route. Any failure partway through a multi-file upload (validation or otherwise) deletes every byte already written for that request.
- Releasing a revision to a listing mapping requires a cost snapshot for that same revision with every category costed (filament, packaging and shipping — even as an explicit `0`, but not missing) and pins an exact revision + cost snapshot; uploading a newer revision never moves that pointer on its own, and both release and revert create a new append-only `project_releases` row under an optimistic lock (`project_listing_links.lock_version`) rather than overwriting history. A detached listing mapping can no longer be released or reverted to.
- Cost snapshots use fixed-point integer arithmetic (`App\Support\FixedPoint` / `App\Services\ProjectCosting`), not floats: the bcmath/gmp PHP extensions are not installed in this project's runtime, so exact-decimal math is done by scaling decimal strings into bounded native integers with explicit overflow and storable-precision checks. Estimate and actual snapshots are both append-only and never overwrite each other; a missing cost category is stored as `null` ("not costed"), distinct from an intentional `0.0000`. Each category accepts exactly one quantity unit (filament `g`, packaging `unit`, shipping `parcel`) so a mistyped unit is rejected rather than silently mis-costed. Snapshot currency must match the project (no implied FX). Shipping quantities are parcels; `units_per_parcel` allocates that parcel cost across the saleable batch yield (`batch_units / units_per_parcel * parcel cost`) independently of `perUnit()`. Actual shipping needs a confirmed shipment or invoice reference. The form starts with separate filament, packaging and shipping lines and can add more materials/components; Fusion project/document/version fields on a revision are optional manual provenance only.
- Stage can only move to Live once *every* non-detached listing mapping has an active release (not merely one of several); moving a project's internal stage never calls the Etsy API or publishes anything. Attaching a new, not-yet-released listing mapping to a project already at Live drops its stage back to Design rather than leaving Live claiming coverage it no longer has.
## Branding
The sidebar/login logo, favicon and in-app colour tokens use the supplied HL3D brand assets under `public/branding/` (carbon `#111316`, alloy `#A7ADB5`, fluorescent yellow `#E7FF00`, ice `#F4F6F8`, slate `#515963`). The CSS font stack (`resources/css/app.css`) names "Avenir Next" first and falls back to the local system sans/monospace stack; no font files are bundled or fetched from Google/Bunny Fonts, and the wordmark is supplied as vector outlines so no font is needed to render the logo itself. **Follow-up:** no redistributable Avenir Next web-font licence has been secured — if a licensed web font becomes available, add it as a self-hosted asset (no external font CDN calls) and keep the system-stack fallback.
## Backups
A restorable backup needs **both** of the following, taken together (the private files reference projects/revisions by ID, so restoring one without the other breaks file bundles):
1. The database (`mysqldump` in production; `database/database.sqlite` only exists for local/dev use).
2. The private file store at `storage/app/project-files/` (dedicated, non-public disk for uploaded STL/3MF/G-code bytes — distinct from `storage/app/public`).
Verify a restore against an isolated copy before trusting it, and spot-check a `project_files.sha256` value against the restored bytes on disk.
## Verification
```sh