Skip to content

feat: report style presets, inspector credentials, email attachment fix - #260

Merged
important-new merged 16 commits into
InspectorHub:mainfrom
important-new:feat/report-styling-credentials
Jul 21, 2026
Merged

important-new merged 16 commits into
InspectorHub:mainfrom
important-new:feat/report-styling-credentials

Conversation

@important-new

Copy link
Copy Markdown
Contributor

Adds two inspection features, an email defect fix, and a small UI polish.

Report style presets

Repurposes the inert report_theme field into a data-driven appearance-profile engine. Three built-in profiles (Signature / Meridian / Terra) resolve three-tier (per-inspection override → template default → tenant default → signature) and render via var(--report-*) CSS variables. The colour axis (tenant brand) stays independent of the typography/layout axis (preset). Workspace gallery picker with a live mini-report preview, a template-default binding, and a collapsed per-inspection override. Migration 0018 (D1-safe ADD+DROP for the tenant default, RENAME/ADD for inspections).

Inspector credentials & association badges

Per-inspector self-asserted credentials (label, member number, uploaded badge image; no expiry field). The engine ships no association trademark assets — inspectors upload their own images. Upload-first profile editor (label/member# behind a collapsed disclosure), tenant+user-scoped CRUD API with image upload, and a CredentialBadges renderer. The report service snapshots the inspector's active credentials into the payload; the cover renders the badge strip arranged by the resolved profile's badge layout. Migration 0019 (new table).

Email attachment fix

The SendGrid, Postmark, and Mailgun adapters now forward args.attachments (previously only Resend did), so report-PDF / evidence-pack / .ics attachments actually send on every provider. Mailgun switches to multipart form-data when attachments are present.

Address autocomplete polish

The suggestions dropdown is portaled to <body> as position: fixed, so opening it floats above a modal instead of growing the modal a scrollbar.

RENDER_VERSION r7→r10 (report DOM changed). Full unit + web suite green; migrations are D1-safe (append-at-end / RENAME COLUMN / new table).

🤖 Generated with Claude Code

important-new and others added 16 commits July 20, 2026 22:50
Only Resend read args.attachments; the other three silently dropped report-PDF /
evidence-pack / .ics attachments. SendGrid + Postmark add the attachment array to
their JSON payload; Mailgun switches to multipart FormData when attachments are
present (binary cannot ride urlencoded) and keeps the urlencoded path otherwise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…njector

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…gine

Rename tenant_configs.report_theme -> default_profile_id and
inspections.report_theme_override -> profile_override; add per-inspection
badge_layout_override/report_photo_columns tweaks and templates.default_profile_id.
Wire resolveProfile into the report service (three-tier: inspection override ->
template default -> tenant default -> signature) and emit styleProfile in the
report payload (replaces the modern|classic|minimal theme string). Remove the
stale workspace theme radio (rebuilt as a profile picker in Plan 1b). Bump
RENDER_VERSION r7->r8. Migration 0018 uses ADD+DROP for the tenant_configs
default change (not FK-referenced -> D1-safe) and RENAME/ADD for inspections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
… dead-export)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…cker & live preview

Inject presetTokens at the ReportView root and repoint the report title, section
headings, section band, defect-card radius, and typed signature to var(--report-*)
so the three built-ins render distinctly (Meridian uppercase+navy, Terra serif,
Signature sans). Shared REPORT_HEADING_STYLE const. Activate the existing
per-section alwaysPageBreak flag. Add the Workspace 'Report style' section: a
gallery picker (each card previews its own typography) beside a live mini-report
preview reacting to the draft profile + brand colour. App-side profile mirror in
profiles-client.ts. Bump RENDER_VERSION r8->r9; ratchet file-size baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…on override

Completes the three-tier appearance resolution. Template editor gains a 'Default
report appearance' select in the metadata bar (threaded through the template PUT
validation + updateTemplate service to templates.default_profile_id, exposed by
the loader). InspectionSettingsSheet gains a per-inspection profileOverride select
behind a default-collapsed <details> (progressive disclosure — the default path
never touches it); '' maps to null via the existing settings-patch/crud path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
… for template defaultProfileId

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…d-export)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
Per-inspector self-asserted credentials (label, member number, uploaded image).
No expiry field by design (Spec B §5). No .references() (app-layer tenant+user
scoping). Migration 0019 is a pure CREATE TABLE + two indexes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…y/serving

Extract the duplicated brand-asset regex into isServableBrandAsset (single source
for the route + its guard test), widened to serve tenant-rooted credential images
under the credentials/ prefix. Add r2Keys.credentialImage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
Tenant+user scoped credential CRUD (fail-closed queries), a multipart image
upload route (replace purges the old R2 object), wired into DI + mounted at
/api/credentials. Owner scope = the signed-in inspector's own credentials.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
Settings -> Profile credentials editor (upload-first; label/member# behind a
collapsed details; no expiry) wired through the profile route (loader fetch + 4
action intents, per-module hono client). CredentialBadges renders image + text
credentials; the report service snapshots the inspector's active credentials into
the payload and ReportView mounts the badge strip on the cover, arranged by the
resolved styleProfile.badgeLayout. Bump RENDER_VERSION r9->r10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…ypes

Add the 'credentials' tag to VALID_TAGS, give the update/delete summaries 4-12
words, describe every request field/param (>=50 chars), and drop the unused
CreateCredentialInput/UpdateCredentialInput exports (knip). Refresh the snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
The suggestions <ul> was position:absolute inside a modal's overflow-y-auto box,
so opening it grew the modal a scrollbar. Render it via createPortal to <body>
with position:fixed, positioned from the input's getBoundingClientRect and
re-measured on scroll (capture:true, to catch the modal's own scroll) + resize.
li onMouseDown+preventDefault still beats the input's 120ms blur-close across the
portal. SSR-safe: the portal only renders once pos is measured client-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
…, booking

Follow-up to Spec B: surface each inspector's active credentials on the three
remaining render surfaces that were deferred from the core PR.

- ReportSignatureBlock: small credential badge image under the license line
  (ReportView passes the first credential with an image).
- inspector-signature email footer: renders credential badge images
  (host-absolutized) plus every credential as text in BOTH html and plain-text
  variants, so a blocked image never drops the credential.
- booking.service sigInspector: loads the inspector's active credentials
  (tenant + user scoped, sorted) into the signature payload.
- New message key pca_signature_credential_alt; paraglide recompiled.
- 2 new unit tests for the email-footer credential rendering (9/9 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45
@important-new
important-new merged commit a5c19b8 into InspectorHub:main Jul 21, 2026
5 checks passed
@important-new
important-new deleted the feat/report-styling-credentials branch July 21, 2026 00:22
important-new added a commit to important-new/OpenInspection that referenced this pull request Aug 23, 2026
…ix (InspectorHub#260)

* fix(email): SendGrid/Postmark/Mailgun adapters forward attachments

Only Resend read args.attachments; the other three silently dropped report-PDF /
evidence-pack / .ics attachments. SendGrid + Postmark add the attachment array to
their JSON payload; Mailgun switches to multipart FormData when attachments are
present (binary cannot ride urlencoded) and keeps the urlencoded path otherwise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(report-style): profile constants, resolveProfile, presetTokens injector

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(report-style): repurpose report_theme into appearance-profile engine

Rename tenant_configs.report_theme -> default_profile_id and
inspections.report_theme_override -> profile_override; add per-inspection
badge_layout_override/report_photo_columns tweaks and templates.default_profile_id.
Wire resolveProfile into the report service (three-tier: inspection override ->
template default -> tenant default -> signature) and emit styleProfile in the
report payload (replaces the modern|classic|minimal theme string). Remove the
stale workspace theme radio (rebuilt as a profile picker in Plan 1b). Bump
RENDER_VERSION r7->r8. Migration 0018 uses ADD+DROP for the tenant_configs
default change (not FK-referenced -> D1-safe) and RENAME/ADD for inspections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* chore(report-style): make PROFILE_SCHEMA_VERSION module-private (knip dead-export)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(report-style): consume styleProfile in ReportView + workspace picker & live preview

Inject presetTokens at the ReportView root and repoint the report title, section
headings, section band, defect-card radius, and typed signature to var(--report-*)
so the three built-ins render distinctly (Meridian uppercase+navy, Terra serif,
Signature sans). Shared REPORT_HEADING_STYLE const. Activate the existing
per-section alwaysPageBreak flag. Add the Workspace 'Report style' section: a
gallery picker (each card previews its own typography) beside a live mini-report
preview reacting to the draft profile + brand colour. App-side profile mirror in
profiles-client.ts. Bump RENDER_VERSION r8->r9; ratchet file-size baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(report-style): template-default binding + collapsed per-inspection override

Completes the three-tier appearance resolution. Template editor gains a 'Default
report appearance' select in the metadata bar (threaded through the template PUT
validation + updateTemplate service to templates.default_profile_id, exposed by
the loader). InspectionSettingsSheet gains a per-inspection profileOverride select
behind a default-collapsed <details> (progressive disclosure — the default path
never touches it); '' maps to null via the existing settings-patch/crud path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* chore(report-style): refresh openapi snapshot + tenant-scope baseline for template defaultProfileId

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* chore(report-style): make StyleProfileClient module-private (knip dead-export)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(credentials): inspector_credentials table + migration

Per-inspector self-asserted credentials (label, member number, uploaded image).
No expiry field by design (Spec B §5). No .references() (app-layer tenant+user
scoping). Migration 0019 is a pure CREATE TABLE + two indexes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(credentials): shared brand-asset predicate + credential image key/serving

Extract the duplicated brand-asset regex into isServableBrandAsset (single source
for the route + its guard test), widened to serve tenant-rooted credential images
under the credentials/ prefix. Add r2Keys.credentialImage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(credentials): CRUD service + API + image upload

Tenant+user scoped credential CRUD (fail-closed queries), a multipart image
upload route (replace purges the old R2 object), wired into DI + mounted at
/api/credentials. Owner scope = the signed-in inspector's own credentials.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(credentials): editor + CredentialBadges + report cover render

Settings -> Profile credentials editor (upload-first; label/member# behind a
collapsed details; no expiry) wired through the profile route (loader fetch + 4
action intents, per-module hono client). CredentialBadges renders image + text
credentials; the report service snapshots the inspector's active credentials into
the payload and ReportView mounts the badge strip on the cover, arranged by the
resolved styleProfile.badgeLayout. Bump RENDER_VERSION r9->r10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* chore(credentials): refresh openapi snapshot for /api/credentials routes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* chore(credentials): satisfy route-metadata gate + drop unused input types

Add the 'credentials' tag to VALID_TAGS, give the update/delete summaries 4-12
words, describe every request field/param (>=50 chars), and drop the unused
CreateCredentialInput/UpdateCredentialInput exports (knip). Refresh the snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* fix(address): portal the autocomplete dropdown so it floats above modals

The suggestions <ul> was position:absolute inside a modal's overflow-y-auto box,
so opening it grew the modal a scrollbar. Render it via createPortal to <body>
with position:fixed, positioned from the input's getBoundingClientRect and
re-measured on scroll (capture:true, to catch the modal's own scroll) + resize.
li onMouseDown+preventDefault still beats the input's 120ms blur-close across the
portal. SSR-safe: the portal only renders once pos is measured client-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

* feat(report): render inspector credentials on signature, email footer, booking

Follow-up to Spec B: surface each inspector's active credentials on the three
remaining render surfaces that were deferred from the core PR.

- ReportSignatureBlock: small credential badge image under the license line
  (ReportView passes the first credential with an image).
- inspector-signature email footer: renders credential badge images
  (host-absolutized) plus every credential as text in BOTH html and plain-text
  variants, so a blocked image never drops the credential.
- booking.service sigInspector: loads the inspector's active credentials
  (tenant + user scoped, sorted) into the signature payload.
- New message key pca_signature_credential_alt; paraglide recompiled.
- 2 new unit tests for the email-footer credential rendering (9/9 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtriEUniSuDJwgKQpmhm45

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant