Consumes the a2loft cropper-token-auth change: the save now sends the
session's PocketBase token as a bearer credential and no phone/otp. The
backend authorizes with OpenFGA crop_image (domain:<code> -> domain:idid),
so an operator no longer needs super-user rights in the profile's domain —
which is what made saves fail with a blanket 'Invalid OTP specified.'.
- the save's domain comes from a2loft's own record of the picture
(dims/picture/{uploadId} -> domainCode), falling back to the queue label,
guarded by a path-safety pattern instead of the catalog check
- the 'core' -> 'uc' alias is deleted: 'core' is a2loft's own pseudo-domain
for a profile picture (special-cased there), so rewriting it changed the
attribution and the ID-card notification
- queue loads are enriched with the profile's domainCode; the panel shows
the profile's domain and, when it differs, the picture target
- the cropper's OTP field, state and parameters are gone; the fingerprint
wizard is untouched (its own OTP flow on its own endpoints)
- tests updated: bearer + no query auth, resolved domain wins, unsafe values
refused, no OTP field served (46 pass)
Requires the a2loft change deployed first.
Evidence: GET {a2loft}/identity/{code}/units resolves domain codes — uc,
ucth, mtr, uri, yrka are valid, core answers 'Invalid domain code
specified.' The label comes from the Telegram intake hashtag, not a2loft.
Saving failed with 400 'Unknown domain.' because queue items carry the
legacy domain 'core', which the old bot mapped to 'uc' for a2loft URLs —
my domain validation (added in the review pass) rejected it. The save now
applies the same alias before validating and forwarding.
Also: the client showed axios's generic 'Request failed with status code
400' because the catch destructured error.message; all cropper handlers now
prefer the server's response.data.message (errMessage helper), so the real
reason is visible.
Verified: save with a 'core' item forwards to /identity/uc/profile/picture;
a 400 shows 'Unknown domain.'; 46 tests.
Crop / fingerprint-recognition / logout Carbon icons (16px, inlined into the
shared header so they inherit the theme colour) in the side-nav-link
'title-icon' slot — the slot is named title-icon, not nav-icon.
The extra cropped-result panel duplicated what the crop pad already shows and
the cleaned image supersedes it. Removes the panel, its state, the pointer-
release refresh handlers and the app.js plumbing. The crop result is still
uploaded (croppa stays the source when nothing is cleaned).
'Load next queued' did nothing because the running server had been started
without its env file (a restart used '-f .env' from tools/, where no .env
exists), so the cropper was unconfigured and every action answered 503 —
and none of that was visible, because notifications never rendered either:
- notifications used cds-toast/cds-toast-item, which the Carbon bundle does
not define; a cds-toast-notification element renders nothing standalone in
this build. The cropper now uses cds-inline-notification in the fixed
toast viewport (renders, themed, close button) like the wizard
- an unconfigured deployment now says so on the page (503 'Cropper
unavailable') instead of rendering actions that all fail, and logs a
startup warning naming the missing credentials
- tests: notifications use a registered element; unconfigured page 503
Three defects from the Carbon rewrite, found while fixing the review panel:
- reason checkboxes were invisible: Carbon's .cds--checkbox input is
visually clipped and needs the .cds--form-item/.cds--checkbox-wrapper +
.cds--checkbox-label markup to draw the box
- 'Others (please specify)' now appears only when 'Others not specified.'
is ticked, and clears when unticked (hidden text must not be submitted);
the textarea gets Carbon's __wrapper so it has a proper height
- new 'Cropped preview' between the original and cleaned previews, so the
operator sees what will be uploaded; refreshed on pointer release over the
pad and after load/clean
- REGRESSION: Vue.use(Croppa) had been dropped — the crop canvas never
rendered, and saving only worked because the cleaned image bypassed
generateDataUrl. Restored.
- REGRESSION: the OTP field bound v-model="otpSave" while the data property
is "otp", so Vue's render threw ReferenceError and froze the panel as
soon as accept was clicked (the OTP could never be entered). Bound to otp;
the croppa instance is now reached via $refs.croppa (the v-model object is
not always the live instance)
- after a successful save the review panel stayed open: reset() recreated an
empty message object, which the template treats as truthy. message is null
when idle now.
45 tests pass; save round-trip and reject/Others flow verified in the browser.
- favicon is the Youreka logo (64px PNG generated from the source mark),
on the tools pages and server error pages
- signing out asks first: a Carbon cds-modal (Cancel / danger Sign out)
shared by the header and the mobile side nav; Cancel closes, Sign out
confirms and logs out
- the current tool is now visibly selected: Carbon marks active nav via
a[aria-current=page] INSIDE the shadow, but aria-current sits on the
host, so the rule never matched — the header item now gets a blue
underline and the side-nav link a blue left border (via ::part(link)
for colour, plus the host for the marker)
- logo and wordmark are middle-aligned inside a flex brand wrapper
- tests: favicon/modal/signout-trigger markup and the active-tool marking
on both navs (45 total)
- all pages (landing, both tools, server error pages) render on Carbon's
g100 theme via <body class="app cds--g100">; token consumers follow
automatically, including component shadow styles through the inherited
--cds-* custom properties
- dark base on <html> so no light flash around the viewport
- step chips and tool icons use the brand blue: layer-selected-inverse
inverts to a LIGHT token in g100, which had made them white-on-white
- page titles breathe: .page-head top margin 1.5rem -> 2.75rem
- new test asserting every page renders on g100 (44 total)
- header: Carbon ui-shell (cds-header) with the Youreka logo (from
idid-enterprise-portal), Image Cropper / Fingerprint Agent nav items
(aria-current on the active tool), the agent status tag on the wizard,
and the operator chip + Sign out on desktop
- mobile: cds-side-nav (logo above the items, both tools, a divider, then
Sign out) toggled by the ui-shell menu button; account actions move out
of the header below 1056px, the nav is hidden at desktop widths
- the header is built once in server.js (appHeader) and injected into the
landing, both tools and error pages — no per-page duplication
- removed my duplicate menu-button toggle: ui-shell coordinates the menu
button and side nav itself (the duplicate cancelled it out)
- fixes: 'cds-header' was hidden by a selector-list typo
(cds-header, cds-side-nav:not(:defined)); the collapsed side nav no
longer captures taps
Tools banner logo asset added (public/assets/youreka-logo.png).
The pages are rendered by the server, which already holds the session — so
/fingerprint, /cropper and the landing now embed window.__SESSION__ (JSON,
with '<' escaped) instead of each fetching /auth/session on load:
- /fingerprint gets token, identity, agentUrl and the permitted domains
(the same cached context the endpoint returned); /cropper gets identity
- landing nav is now server-rendered (Sign in vs user chip + Sign out);
its client fetch script is deleted
- per-user HTML is sent with Cache-Control: no-store and Vary: Cookie
- express.static runs with index:false so '/' can be session-aware while
assets stay session-free
- GET /auth/session remains as the documented JSON contract (and the
canonical place the domain-filter caching is exercised in tests)
Client code shrinks by a fetch + error path per page; 3 new tests
(injection, escaping, landing header) — 43 total.
.env.example moves to the repository root with dotenvx setup/run notes; the
README documents 'dotenvx run -f .env -- node tools/server.js' and the
Docker caveat (--env-file cannot read encrypted values).
HIGH
- async handlers: Express 4 does not catch rejected promises and Node 22
exits on unhandled rejections — a malformed body ({} to /cropper/dequeue
etc.) could hang a request or kill the process. Every cropper route is
now wrapped (throw → 500 via next), request bodies are validated up
front (400), and JSON.parse of queue items is guarded. Server-wide
error middleware added, honouring err.status (413 for oversized bodies).
- error pages HTML-escape all interpolated values — the login failure page
rendered upstream (portal) error text raw.
MEDIUM
- outbound calls carry AbortSignal timeouts (5s OpenFGA verify, 15s
cropper upstreams) — a hung upstream can no longer pin a request.
- body limits scoped: /cropper gets 50mb (picture data URLs), everything
else 1mb (previously 50mb globally, memory-DoS surface pre-auth).
- cropper save validates the client-supplied domain against the catalog;
picture fetches validate the Minio object key (id-domains/ prefix, no ..).
LOW
- static assets served before the session middleware (no PocketBase load
per asset); landing header builds DOM nodes with textContent instead of
innerHTML; picture load checks upstream ok.
5 new tests (40 total) covering malformed bodies, malformed queue items,
unknown domains, object-key traversal, and error-page escaping.
- .container redefined on Carbon tokens (was Bootstrap's class — pages had
no edge padding after the Bootstrap removal)
- landing tiles use cds-clickable-tile and — critically — matching closing
tags: the fingerprint tile was parsed as NESTED inside the cropper tile
(mismatched </cds-tile>), painting it on top and swallowing clicks
- landing header reflects the active session (user chip + Sign out instead
of Sign in)
- vertical rhythm: larger panel padding, row gaps and section margins
- @carbon/web-components v2.50.0 via the IBM CDN (version-pinned);
@carbon/styles CSS + IBM Plex Sans for tokens/typography
- landing: Carbon header + cds-tile tool cards
- fingerprint wizard: Carbon-styled form controls, cds-progress-bar scan
progress, cds-tag badges, cds-inline-notification replacing swal
- cropper: cds-toast replacing toastr, cds-checkbox review reasons,
cds-tag process badge; vue-croppa pad kept in the Carbon layout
- Bootstrap 4, swal, toastr and jQuery removed from all pages
- behavior frozen: same flows, endpoints, validation and copy
Centered card with navbar, lock icon, sans-serif stack (error pages
previously rendered in the browser default serif), and action-aware
copy: permission denial offers only 'Back to Tools' instead of a
pointless 'Try again'; auth failures keep Try again + Back to Tools.
Two-column editor at >=900px (crop pad + cleaned preview left, cleaning/
review controls right), single column below; panel headers wrap their
hints onto their own line on narrow screens.
- Smart Photo Editor ported from id-image-bot: queue/by-ID picture
loading, vue-croppa editing, carvekit background cleanup, nodered
review flow, a2loft save — restyled to the tools design language
- unified login: YOUreka session identity, phone prefilled, OTP only at
save (a2loft's requirement); artist-login step removed
- crop_image @ domain:idid OpenFGA gate on every cropper route: cached
5-min on reads, live on save, fail closed
- credentials (minio/azure/nodered/carvekit) via env, sole exception to
hardcoded config; 503 when unconfigured; native FormData replaces
form-data; new deps minio + azure-storage
- 10 new tests (35 total) with fake minio/queue/nodered/removebg/a2loft