Skip to content

Documentation Update Plan — current-system

Changes and additions to current-system/ documentation required to capture patterns, architecture, and API surfaces introduced by the image upload feature. Organized by priority; high-priority items should ship with or shortly after PR arda-cards/arda-frontend-app#742.

architecture/patterns/user-interface/rich-cell-editors.md

Section titled “architecture/patterns/user-interface/rich-cell-editors.md”

What it documents: AG Grid’s cell-editor lifecycle assumes the editor renders within the cell’s focus scope. Portal-based modal editors (Radix Dialog, MUI Dialog, any React portal) violate that assumption — stopEditingWhenCellsLoseFocus unmounts the editor when the portal steals focus. This file documents the workaround pattern and its industry validation.

Content outline:

  • The impedance mismatch: AG Grid edit lifecycle vs. portalled modals.
    • cellEditorPopup: true wraps the editor in a 0×0 AG-Grid-owned container (hidden subtree defeats focus traps).
    • Without cellEditorPopup, stopEditingWhenCellsLoseFocus: true tears down the editor on portal focus-steal.
    • isPopup() runtime handle suppresses AG Grid’s tooltip feature but does not exempt the editor from focus-loss teardown.
  • The pattern: grid becomes trigger, host owns editor lifecycle, commit flows back through the grid’s public transaction API (applyTransaction + dirty-row tracking + debounced publish).
  • Trigger mechanisms: onCellDoubleClicked, onCellKeyDown (Enter, F2), or gridContext.openRichEditor(field, row) for renderer-driven triggers.
  • Commit integration: image edits flow through the same dirtyRowIdsRef / publishRow pipeline as onNotesSave / onCardNotesSave — one save per row per edit session regardless of which fields changed.
  • Industry alignment: MUI X DataGrid, Glide Data Grid, Handsontable, TanStack Table all prescribe the same split. Core principle restated across sources: “if the editor owns focus in a way the grid cannot observe, take it out of the grid’s edit state machine.”
  • Forward reference to ColumnEditSpec (#748) — the typed abstraction that generalizes this pattern across inline / custom-editor / rich column kinds.

Decision references: FD-19 (superseded), FD-20, FD-21.


What it documents: The full image storage subsystem: upload flow, CDN delivery, CORS configuration, and signed-cookie authentication. Consolidates the S3 + CloudFront + BFF signing story in one place.

Content outline:

  • Architecture overview: SPA → BFF → Backend → S3 presigned POST → SPA direct upload → CDN delivery.
  • BFF routes:
    • POST /api/image-upload — proxy to backend’s /v1/item/image-upload; returns presigned POST URL + form fields + CDN URL.
    • POST /api/storage/check-url — URL reachability check with SSRF protection.
    • POST /api/storage/fetch-url — external image fetch (10 MB limit).
    • POST /api/storage/cdn-cookies — CloudFront signed cookie issuance.
    • GET /api/storage/sign-url?url=<cdn-url> — CloudFront signed URL for a single resource (Approach D / FD-22 fallback for non-*.arda.cards origins).
  • Authentication: all BFF routes require both Authorization: Bearer (access token) and X-ID-Token (ID token). The BFF extracts custom:tenant from the ID token; sending only the access token causes 401 “JWT token missing required attributes or expired”. Implemented via shared getBffAuthHeaders() helper.
  • Error response format: machine-readable codes (RATE_LIMITED, VALIDATION_FAILED, etc.), not user-facing text. SPA API layer translates codes to localized messages.
  • Dual-mode CDN auth (FD-22, Approach D):
    • On *.arda.cards origins: cookie-based auth (production path, zero-cost pass-through via CdnAuthProvider).
    • On all other origins (Amplify preview, localhost): per-image signed URL via GET /api/storage/sign-url. CloudFront natively accepts both signed cookies and signed URLs.
    • useResolvedImageUrl(cdnUrl) hook and ResolvedImage / ResolvedImg components resolve URLs transparently. All image render sites in the app use these.
  • Two levels of CDN image access (critical nuance):
    • Display (<img src>, grid cells, hover previews, card views): works everywhere with signed URLs. No CORS headers needed.
    • Canvas-based editing (react-easy-crop in ImagePreviewEditor): requires CORS headers from CloudFront because the <img> element has crossOrigin="use-credentials" (FD-17). Signed URLs authenticate the request but do not satisfy the browser’s CORS check. The CloudFront Response Headers Policy must include the page’s origin.
    • Implication: image display works on all origins; edit-existing crop works on *.arda.cards (permanent) and *.amplifyapp.com (permanent per infrastructure#442) but requires a temporary CLI change for localhost.
  • CloudFront CORS (Response Headers Policy):
    • Access-Control-Allow-Origins: https://*.arda.cards (all envs), https://*.amplifyapp.com (non-prod only).
    • http://localhost:<port> excluded from CDK; applied via CLI for local dev only (knowledge-base documented).
  • S3 bucket CORS:
    • AllowedOrigins: https://*.arda.cards, https://*.amplifyapp.com (non-prod).
    • AllowedMethods: POST.
    • Required for browser-direct presigned POST uploads.
    • http://localhost:<port> excluded from CDK; applied via ad-hoc CLI for local dev only (see knowledge-base).
  • crossOrigin="use-credentials" on crop <img> elements:
    • Required only when loading CDN images onto an HTML canvas (react-easy-crop). Triggers CORS preflight.
    • Display-only <img> tags (thumbnails, previews) do NOT set crossOrigin to avoid unnecessary preflights.
  • CloudFront signing key management:
    • CLOUDFRONT_KEY_PAIR_ID env var (public key identifier, not secret).
    • Private key fetched from AWS Secrets Manager at runtime using derived name ${Infrastructure}-${Partition}-ImageCdnSigningKey.
    • 8-hour in-memory cache TTL.
  • Sequence diagram (PlantUML):
    • Upload: SPA → BFF /api/image-upload → Backend POST /v1/item/image-upload → STS AssumeRole → S3 presigned POST → response to SPA → SPA direct POST to S3 → BFF PUT /api/arda/items/{id} to persist URL.
    • Display: SPA → BFF /api/storage/cdn-cookies → CloudFront signer → Set-Cookie headers → <img src> to CDN with cookies.

Decision references: FD-10, FD-11, FD-12, FD-13, FD-17. Infrastructure references: #439 (CloudFront CORS), #441 (STS AssumeRole), #442 (S3 bucket CORS + CloudFront CORS for preview domains).


architecture/patterns/user-interface/design-system-integration.md

Section titled “architecture/patterns/user-interface/design-system-integration.md”

What it documents: How @arda-cards/design-system (canary) components are consumed by arda-frontend-app; the factory-hook injection pattern; Tailwind v4 @source directive coupling.

Content outline:

  • Package entry points: @arda-cards/design-system (stable), /canary (experimental), /extras (supplementary).
  • Factory hook injection pattern (ImageCellEditorConfig):
    • useImageUpload and useCheckReachability are required fields (not optional). Storybook provides stubs.
    • Compile-time safety: missing hooks fail tsc, not runtime console.warn.
  • Tailwind v4 @source directive:
    • arda-frontend-app/src/app/globals.css must include @source "../../node_modules/@arda-cards/design-system/dist" for Tailwind to scan design-system classes.
    • When using npm link for local dev, the @source path must point at the linked package’s source.
  • Consumer responsibilities: include @source, provide factory hooks, wrap in required providers (CdnCookieProvider, QueryClientProvider).

Decision references: FD-01, FD-15.


architecture/patterns/user-interface/cdn-image-recovery.md

Section titled “architecture/patterns/user-interface/cdn-image-recovery.md”

What it documents: Client-side CDN 403 recovery mechanism: automatic cookie refresh and cache-busted retry.

Content outline:

  • CdnCookieProvider context: wraps the app, calls POST /api/storage/cdn-cookies on mount and at 50% TTL.
  • useImageWithCdnRecovery(cdnUrl, options?) hook:
    • Detects CDN URLs (matches *.assets.arda.cards).
    • On <img> error event: calls refreshNow() from context, bumps src with cache-buster query param.
    • Maximum one retry per URL per mount — prevents refresh storms.
  • ImageDisplay stays pure (design-system component, no auth awareness). The recovery hook is wired at the app level.
  • Failure mode: if cookie refresh itself fails (BFF 500), the image shows the error placeholder (initials + badge). No infinite retry loop.

Decision references: FD-16.


What it documents: The hybrid layer-with-feature-grouping code organization pattern adopted for arda-frontend-app.

Content outline:

  • Directory structure:
    • src/server/ — BFF routes + server-only utilities. Guarded by import 'server-only' or import '@/server'.
    • src/server/lib/ — server-only utilities (cloudfront-signer, secrets, ssrf-validator, rate-limiter).
    • src/api/ — SPA-side API functions (plain fetch wrappers calling BFF routes). No TanStack, no @arda-cards/api-proxy runtime imports.
    • src/hooks/ — feature-grouped React hooks (e.g., src/hooks/image-upload/, src/hooks/cdn/).
    • src/providers/ — React context providers.
    • src/lib/ — shared utilities consumed by both client and server (e.g., auth-headers.ts, shared/aws-naming.ts).
  • Dependency direction rules:
    • src/server/ may import from src/lib/ and src/lib/shared/.
    • src/api/ may import from src/lib/ (for auth headers) and type-only from @arda-cards/api-proxy.
    • src/hooks/ imports from src/api/ (not from src/server/).
    • src/components/ imports from src/hooks/ and src/lib/.
  • BFF byte-buffering policy (REQ-FE-020): BFF routes must not buffer or store image file bytes. Enforced by code review; future structural lint planned.

Decision references: FD-02, FD-11.


architecture/patterns/user-interface/copyright-acknowledgment.md

Section titled “architecture/patterns/user-interface/copyright-acknowledgment.md”
  • Inline text in ImageUploadDialog confirm footer.
  • No checkbox gate or blocking acknowledgment step.
  • Server-side logging deferred (TD-04).

Decision references: FD-18, SD-07.


Add a new section for image storage / BFF routes:

MethodPathPurpose
POST/api/image-uploadProxy to backend; returns presigned POST URL + CDN URL
POST/api/storage/check-urlURL reachability check with SSRF protection
POST/api/storage/fetch-urlExternal image fetch (10 MB limit, SSRF-guarded)
POST/api/storage/cdn-cookiesCloudFront signed cookie issuance (204, Set-Cookie)

Expand “Integrations” section to cover:

  • Image upload sequence: SPA → BFF → S3 presigned POST → Item Service PUT /v1/item/{id} to persist imageUrl.
  • CDN cookie requirement for displaying uploaded images.
  • Canvas-editing CORS requirement for edit-existing flow.
  • Reference to functional/image-storage/index.md for full details.

architecture/patterns/user-interface/frontend-architecture.md

Section titled “architecture/patterns/user-interface/frontend-architecture.md”

Add subsection “State Management Strategy”:

  • Redux Toolkit (redux-persist): entity CRUD, auth, UI preferences — the established pattern for domain state.
  • TanStack Query: image-specific operations (upload credentials, reachability, CDN cookies) — service-oriented, cache-managed queries with automatic refresh.
  • Rationale: Redux pattern is established for item CRUDQ; introducing TanStack for new service-oriented operations avoids refactoring the existing Redux slices while providing better cache semantics for operations with TTLs (CDN cookies) and mutations (uploads).

architecture/user-interaction/edit-lifecycle/ (any appropriate file)

Section titled “architecture/user-interaction/edit-lifecycle/ (any appropriate file)”

Add image-upload-specific note:

  • EditLifecycleCallbacks<ImageUploadResult> specialization.
  • Scope limited to 6 image-related components (FD-03).
  • Future: useComposedDraft<T> for composed validation across parent and child components.

  • All new files must include frontmatter: title, tags, domain, maturity, author.
  • Internal links must include .md extension.
  • Diagrams use PlantUML exclusively (not Mermaid).
  • Run make pr-checks (not just make test) before pushing.
  • These documentation updates can ship as a separate PR from the code changes. They do not block the feature release but should land before the project moves to completed status.