Skip to content

feat(isn): add a published report's link to its ISN order - #361

Merged
important-new merged 3 commits into
InspectorHub:mainfrom
NCHILLC:feat/isn-report-outbound
Sep 24, 2026
Merged

important-new merged 3 commits into
InspectorHub:mainfrom
NCHILLC:feat/isn-report-outbound

Conversation

@NCHILLC

@NCHILLC NCHILLC commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When a report is published and the inspection's Reference Number holds an ISN order number, the report link (the primary client's portal-token URL) is added to that ISN order as a public report, then read back from /orderfiles before the sync counts as done. Scope follows the discussion on #321: outbound link + read-back only. This app's agreement, payment and report-release gates are unchanged, and ISN does not control them.

  • Credentials are per company, set in Settings → Advanced → ISN (ISN_DOMAIN, ISN_COMPANY_KEY, ISN_ACCESS_KEY, ISN_SECRET_KEY). They are catalogued secrets and tenant-owned, so a platform key cannot put every company's reports on one ISN account. There is no default domain, because ISN is white-labelled.
  • A failed sync never undoes the publish. The outcome is returned as isnSync on the publish response, with ISN's own message; it is null when ISN is not configured or the inspection has no Reference Number.
  • Two ISN behaviours its published API document does not state, both pinned in tests/contract/isn/: errors arrive as HTTP 200 with status: "error", so success is the body's status and never the HTTP code; and PUT /orders/addreporturl reads the query string, not the documented form body. Deleting a report row in ISN also hides it (show: false in /orderfiles) rather than removing it, so the read-back requires the row to be shown.
  • Laid out per docs/develop/integration-adapters.md: api-base / pure payload / sync, a vendored excerpt of ISN's schema with provenance and a contract spec, a live spec that writes nothing (needs an ISN company; not run in CI), and docs/integrations/isn.md. The ISN module is imported lazily by the publish route, so deployments without ISN pay nothing at startup.

Not in this PR: importing ISN orders (footprints/polling), field mapping, per-inspector opt-in, persisted sync-failure records with retry, completing the ISN order from here, and whether ISN should eventually control agreement, payment or report release.

CI gate fixes

Two gates failed on this branch. Both are fixed in code; no baseline file changed.

  • lint:tenant-scope. server/services/isn/report-link-sync.ts read reports.title by id alone. It now also matches reports.tenantId, the same pattern the inspection lookup above it uses.
  • lint:middleware-budget. app/lib/isn-settings.server.ts added its own PUT /api/secrets call site. The ISN save now shares one call site with the existing Integration Keys save: a putSecrets closure inside settings-advanced.tsx's action. Keys, intents, error messages and response shape are unchanged for both panels. It isn't exported, because splitRouteModules is "enforce".

Note on the budget. This save is an action-only PUT. It runs once per submit and never from a loader, so it adds no render fan-out. Re-baselining with check-middleware-budget.mjs --update would have been the smaller change, and I held off on purpose. If you'd rather keep the ISN save in its own module and take the one-call re-baseline, say so and I'll switch it. In that case, the three saveIsnSettings unit tests this commit replaced with loadIsnSettings tests come back too.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation only
  • Refactor / chore (no user-visible change)

Related issues / discussions

Refs #321 (the report half only; the issue stays open for the inbound half).

Screenshots / video

No images; observations from npm run dev:hmr against a local D1, signed in as an owner, at /settings/advanced:

  • The ISN panel shows four credential fields and one Save, between Integration API keys and Data management, and ISN appears in the section nav.
  • 375 px: no horizontal overflow (page scrollWidth 375), fields fit the panel, hints wrap. 1440 px: panel 768 px wide, no overflow. Dark theme at both. Light theme was checked at the pane's 682 px width: white panel and inputs, "SET" badges legible.
  • An address without https:// is refused under its field with the ISN_DOMAIN message; https://inspectionsupport.com saves, and address and company key both show as set after a reload. The access-key and secret-key fields were not exercised.

Test plan

  • npm run type-check:app and npm run type-check:api are green (the e2e and tests programs were not run locally; CI runs them)
  • ESLint (type-aware) on the changed source files: 0 errors; whole-repo npm run lint left to CI. The pre-commit gate rung passed 27 of 27.
  • Unit: tests/unit/integrations + tests/unit/inspections (118 files, 755 tests) green; tests/contract/isn (3 tests) green; web: app/lib + app/components/settings (138 files, 1236 tests) green. The full test:unit, test:workers and test:e2e suites were not run locally.
  • Tested manually in npm run dev:hmr (see above)

Checklist

  • My code follows the contributing guidelines (conventional commit, design tokens, 375 px and 1440 px smoke done; the full-repo type-check / lint / unit runs were scoped, as listed above)
  • I have read and agreed to the Code of Conduct
  • I have added tests that prove my feature works (the ISN API layer, payload and settings save are covered; the publish-time glue syncPublishedReportToIsn has no automated test of its own, and its ISN calls are covered through addReportLinkToIsnOrder)
  • I have updated the documentation in docs/ where relevant (docs/integrations/isn.md plus the index links)
  • I have added entries to CHANGELOG.md (release-please generates it from the feat(isn): commit)
  • My commits include Co-Authored-By: lines for any AI assistants used

🤖 Generated with Claude Code

NCHILLC and others added 2 commits September 19, 2026 08:56
The report half of InspectorHub#321. When a report is published and the inspection's
Reference Number holds an ISN order number, the report link (the primary
client's portal-token URL) is added to that ISN order as a public report, then
read back from /orderfiles before the sync counts as done. A failed sync never
undoes the publish; the outcome is returned as `isnSync` on the publish
response, with ISN's own message.

Two ISN behaviours its published document does not state, both verified
against a live company and pinned in tests/contract/isn/isn-api.live.spec.ts
(which writes nothing):
- errors arrive as HTTP 200 with `status: "error"`, so success is the body's
  status, never the HTTP code;
- PUT /orders/addreporturl ignores the documented form body and reads the
  query string.

Credentials (ISN_DOMAIN, ISN_COMPANY_KEY, ISN_ACCESS_KEY, ISN_SECRET_KEY) are
catalogued secrets set in Settings -> Advanced -> ISN, and tenant-owned: a
platform key must not put every company's reports on one ISN account. No
default domain, since ISN is white-labelled.

The sync touches none of OpenInspection's agreement, payment or report-release
gates: the link is an ordinary report link, so those gates apply to it as to
any other. Whether ISN should hold or release them is left for a later change.

Laid out per docs/develop/integration-adapters.md: api-base / pure payload /
sync, a vendored excerpt of ISN's schema with provenance and a contract spec,
and docs/integrations/isn.md. The ISN module is imported lazily by the publish
route so deployments without ISN pay nothing at startup.

Chrome: /settings/advanced — ISN panel in dark theme at 682px wide: four credential fields (address, company key, access key, secret key) then one Save, no hold switch or help text (0 checkboxes in the panel), ISN tab in the section nav, panel sits after the integration keys and before Data management
Chrome: /settings/advanced — light theme (root attribute and dark class flipped as useTheme does): panel and inputs white, SET badges and "Not configured" placeholders legible
Chrome: /settings/advanced — "inspectionsupport.com" refused under the ISN address field with the ISN_DOMAIN message; https://inspectionsupport.com saved (action returned success) and address and company key both show SET after a reload

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@important-new

Copy link
Copy Markdown
Contributor

@NCHILLC Are you still working on this PR? Just checking in since the CI is still failing.

@NCHILLC

NCHILLC commented Sep 24, 2026 via email

Copy link
Copy Markdown
Contributor Author

@NCHILLC

NCHILLC commented Sep 24, 2026 via email

Copy link
Copy Markdown
Contributor Author

@important-new

Copy link
Copy Markdown
Contributor

I know the type-checks are painful right now. The current type-check has fairly high memory requirements, and I’m waiting for a more stable version of tsgo before making changes around that.

And the type-checks are killing me. [image: https://inspectionsupport.com/newcharlottehome/online-scheduler?t=aGtkWFQ2QXhiLzJ0Yw==&office=10aa5407-2ebc-11f1-bdcb-0a63506e1b9d] https://inspectionsupport.com/newcharlottehome/online-scheduler?t=aGtkWFQ2QXhiLzJ0Yw==&office=10aa5407-2ebc-11f1-bdcb-0a63506e1b9d On Thu, Sep 24, 2026, 12:07 PM Aaron Scott - New Charlotte < @.> wrote:
…
I am. I'm low tier subscription with Claude and codex. I just burnt through a lot getting ai-memory and a homelab server set-up, so I could work more on the repo. I'm working on my fork of it, today. I don't ever want to commit anything to your repo until it's an honest attempt for a PR. [image: https://inspectionsupport.com/newcharlottehome/online-scheduler?t=aGtkWFQ2QXhiLzJ0Yw==&office=10aa5407-2ebc-11f1-bdcb-0a63506e1b9d] https://inspectionsupport.com/newcharlottehome/online-scheduler?t=aGtkWFQ2QXhiLzJ0Yw==&office=10aa5407-2ebc-11f1-bdcb-0a63506e1b9d On Thu, Sep 24, 2026, 12:00 PM important-new @.
> wrote: > important-new left a comment (InspectorHub/OpenInspection#361) > <#361 (comment)> > > @NCHILLC https://github.com/NCHILLC Are you still working on this PR? > Just checking in since the CI is still failing. > > — > Reply to this email directly, view it on GitHub > <#361?email_source=notifications&email_token=CEGDFF4R6GCQ67TWOUTM6A35QVALRA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKOBRG43DCMRRGM22M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5817612135>, > or unsubscribe > https://github.com/notifications/unsubscribe-auth/CEGDFFZ5XORVXKYMA776G3L5QVALRAVCNFSNUABGKJSXA33TNF2G64TZHMYTEMBXGU3TQNRWHE5US43TOVSTWNJVGEYDGOBRGMZDLILWAI > . > You are receiving this because you were mentioned.Message ID: > @.***> >

Two CI gates failed on this branch; both are fixed with the code, not the
baseline.

lint:tenant-scope — server/services/isn/report-link-sync.ts:71 selected
`reports.title` by `reports.id` alone, with no `tenantId` predicate in the
same `.where()`. `reports` is a tenant-scoped table (tenant_id column), so
this was a cross-tenant read: given another tenant's report id it would
happily title the ISN report link with that tenant's report title. Fixed by
adding `eq(reports.tenantId, tenantId)` alongside the id predicate, using the
tenantId `syncPublishedReportToIsn` already has in scope — the same pattern
the inspection lookup two lines above it already uses. `targetId` itself
comes from `resolvePublishTargetReport`, which re-resolves within the tenant,
so this is defense in depth on the read that actually names the row.

lint:middleware-budget — the new app/lib/isn-settings.server.ts gave itself
its own `api.secrets.secrets.$put(...)` call, and check-middleware-budget.mjs
ratchets in-process API fan-out per file under app/: a brand-new file with a
nonzero call count fails outright, because the gate counts call SITES, not
runtime invocations, and has no way to know only one intent ever runs per
form submit. settings-advanced.tsx's own `save-advanced-secrets` branch
(Integration Keys panel) already made the identical call, so the fix shares
one call site between both panels instead of each owning its own: a
`putSecrets` closure defined once inside `action` and used by both the
`save-isn` and `save-advanced-secrets` branches. Same key lists, same intent
strings, same fallback error messages, same response shape for both — this
is a call-site merge, not a behavior change, and Integration Keys' save is
covered by the same (pre-existing, already-unwritten) level of test trust it
had before this commit.

The helper stays module-private (not exported from the route file): this
repo's react-router.config.ts sets `splitRouteModules: "enforce"`, and an
exported async function that calls the API is not one of the route module's
recognized server-only exports, so exporting it risks either a failed split
or the call leaking into the client chunk. Nesting it as a nested function
inside `action` keeps every line of it part of `action`'s own server-only
closure, exactly as the two branches it replaces already were.

That reshuffle then tripped two more static ratchets that only look at a
file's post-change shape: check-file-size.mjs (settings-advanced.tsx crossed
400 lines) and check-loader-awaits.mjs (a module-scope helper's body sits at
the same 2-space indent the gate reads as "top level of a route module", so
its one `await` — not sequential with anything, it's the only await in its
own function — was counted as a new one). Nesting the helper inside `action`
(4-space indent) instead of at module scope, plus trimming its comment, fixed
both without touching either baseline: settings-advanced.tsx is back to 374
lines and 4 top-level awaits, matching what it was before this branch.

app/lib/isn-settings.server.ts keeps only `loadIsnSettings` (the read side,
which owns no API call and was untested before this commit — now covered).
Its test file moves with it: the three `saveIsnSettings`-shaped cases move to
being covered by the shared `putSecrets` closure at the same trust level
`save-advanced-secrets` already had (no dedicated test), and a new
`loadIsnSettings` test fills the gap that existed before.

No baseline touched: scripts/middleware-budget-baseline.json,
scripts/file-size-baseline.json, scripts/loader-awaits-baseline.json, and
scripts/tenant-scoping-baseline.json are all unchanged. `node
scripts/run-gates.mjs` (81/81), tests/unit/integrations/isn-report-link.spec.ts
(7/7), and app/lib/isn-settings.server.test.ts (2/2) all pass.

chrome-allow: no rendered output changed — this moves an existing
`api.secrets.secrets.$put` call and its existing error-message key between
files; no JSX, className, or message text was added or changed, for either
the ISN panel or the Integration Keys panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@important-new
important-new merged commit 62ae80f into InspectorHub:main Sep 24, 2026
20 checks passed
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.

2 participants