procurement-ingest/lambdas/api/docs.html
Adam Moussa b89e98a879
Some checks are pending
Deploy / deploy (push) Waiting to run
feat(api): Redocly lint gate + SHOC-themed /docs (Redoc theming, topbar, collapsible samples) (#130)
* feat(api): Add @redocly/cli as a dev dependency

Signed-off-by: Adam Moussa <adam@seahavenind.com>

* feat(api): Add Redocly configuration file with custom rules

Signed-off-by: Adam Moussa <adam@seahavenind.com>

* chore(api): Redocly lint config + bring openapi.json into compliance

redocly.yaml from the Redocly guidelines builder, with three generated
rules corrected: response-contains-property had the status codes as the
required body fields (intent was the Error schema's top-level 'error';
403 exempt since API Gateway emits AWS's {message} shape, 501 not 503);
operation-4xx-problem-details-rfc7807 off (adopting RFC 7807 would be a
runtime + SHOC-contract change, decided against); the two inert casing
rules (parameter names, schema properties) removed because both name
sets are contract-pinned (gateway resource paths, DynamoDB items).

Spec changes, no runtime impact: operationIds renamed to method-prefixed
kebab-case (get-work-orders, post-work-order-comment, ...); tags added to
all 15 operations + root tags object (groups the Redoc sidebar); examples
on all six parameters; license field; server description punctuation; two
descriptions reworded to start capitalized. Real linter catches fixed:
the two x-planned ops were missing their {workOrderId} path parameter
and any 4xx response (403 added - true today, gateway rejects unsigned).

.redocly.lint-ignore.yaml pins the six deliberate exceptions: webhook
keys are the shipped SHOC contract event names (not renameable), and the
x-planned ops answer only 501 (no 2xx to document).

package.json: npm run lint:api. Verified: lint 0 errors, 675 pytest,
headless-Chrome render of the tagged docs page.

* feat(api): SHOC design-system theme for /docs (vendored fonts)

Themes the Redoc page with the canonical SHOC token set: Montserrat 600
headings / DM Sans body / JetBrains Mono code, primary #1c75bc, navy
#262262 sidebar text + right panel, #f9fafb background, 244px sidebar.
sortRequiredPropsFirst on; 200 responses pre-expanded.

Fonts ship as lambdas/api/fonts.css (latin woff2 subsets from
@fontsource 5.3.0, embedded as data URIs, ~90KB) and inline via a new
__FONTS_CSS__ placeholder with the same </style breakout guard --
the offline single-response invariant holds, nothing fetches Google
Fonts (test-pinned). Bundling cp + bundle-consistency pin + spec-drift
asset checks extended.

Verified: headless-Chrome render (theme + fonts applied), ruff, 675
pytest, cdk synth + staged-asset check.

* feat(api): SHOC gradient topbar on /docs

64px fixed header with the SHOC shell gradient token (#1b1f52 ->
#1c4f8f -> #1c75bc), Sea Haven wordmark in Montserrat 600, page name
right-aligned in DM Sans. Redoc's scrollYOffset: 64 keeps the sticky
sidebar and anchor scrolling clear of the fixed bar. Verified via
headless-Chrome render.

* style(api): normalize /docs header and right-panel blues

The right panel's #262262 is a purple-leaning navy that clashed with
the cyan-leaning gradient, and the bar's brightest point sat directly
over the dark panel. Right panel now uses #1b1f52 (the gradient's own
dark endpoint) and the gradient runs bright-to-dark so its dark end
lands flush on the panel -- no seam, one blue family. Verified via
headless-Chrome render.

* style(api): right-panel gradient on /docs via bundle-pinned override

Redoc's theme only takes solid colors (it derives shades from
rightPanel.backgroundColor), so the gradient (#1b3d79 -> #1b3068 ->
#1b1f52, continuing the topbar blend) rides as a CSS override on the
styled-components classes of the per-section right-panel divs
(.sc-iGgWBj.sc-gsFSXq + the .sc-dExYaf stub). Those names are
deterministic for the vendored 2.5.3 bundle (verified across loads) but
change on any Redoc bump: re-derive via headless probe (find elements
whose computed background equals the rightPanel color). If they stop
matching, the panel falls back to the solid #1b1f52 theme color --
cosmetic only. Verified via headless-Chrome render.

* feat(api): collapsible samples column on /docs

Redoc CE has no built-in panel toggle, so the topbar gains a Hide/Show
samples button that flips .samples-collapsed on <html>: the right-panel
divs hide (same bundle-pinned styled-components classes as the gradient
override) and each section's content half takes the full width. Choice
persists in localStorage; aria-pressed tracks state. If the pinned
classes stop matching after a Redoc bump the toggle goes inert --
cosmetic only. Both states verified via headless-Chrome render.

* ci(api): spec-lint CI gate + npm Dependabot coverage

New spec-lint job mirrors the local npm run lint:api so openapi.json
cannot drift from redocly.yaml with green CI. Dependabot gains the npm
ecosystem (package.json is new; nothing watched @redocly/cli).

* feat(api): docs finishing touches - x-tagGroups, favicon, docs:preview

x-tagGroups sections the Redoc sidebar (Read API / Meta / SHOC Feed);
inline data-URI SVG favicon (SHOC blue) stops the browser's follow-up
/favicon.ico request 403ing at the gateway; npm run docs:preview wraps
the real-handler local render (scripts/preview_docs.py); README gains a
docs-page architecture section covering the inline pattern, theme,
pinned-selector caveat, and tooling. Lint 0 errors, 675 pytest,
headless render verified.

---------

Signed-off-by: Adam Moussa <adam@seahavenind.com>
2026-07-24 18:04:30 +00:00

162 lines
6.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procurement Ingest API</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3E%3Crect%20width='16'%20height='16'%20rx='3'%20fill='%231c75bc'/%3E%3Ctext%20x='8'%20y='12'%20font-size='10'%20font-family='sans-serif'%20font-weight='700'%20text-anchor='middle'%20fill='white'%3ES%3C/text%3E%3C/svg%3E">
<!--
Redoc (redoc.standalone.js, MIT), vendored offline and inlined server-side
so /docs is a single token-gated request with no CDN or follow-up asset
fetch. The bundle, the SHOC design-system fonts (fonts.css, data-URI
@font-face), and the OpenAPI spec are substituted for the placeholders
below by lambdas/api/handler.py::_docs_shell / _render_docs_html. Redoc is
read-only by design -- there is no try-it-out to disable; live calls go
through Postman (Authorization type "AWS Signature") since data routes
require SigV4.
Theme = the SHOC design-system token set (the Sea Haven visual standard):
Montserrat headings / DM Sans body / JetBrains Mono code, primary #1c75bc,
navy #262262, page background #f9fafb, 244px sidebar. Fonts are vendored --
nothing here may fetch from Google Fonts.
-->
<style>__FONTS_CSS__</style>
<style>
html, body { margin: 0; padding: 0; background: #f9fafb; }
/* SHOC shell topbar: 64px, gradient header token. Redoc has no topbar of
its own; scrollYOffset below keeps its sticky sidebar/scroll accounting
clear of this fixed bar. Gradient runs bright-to-dark (reversed from the
SHOC shell) so its dark end (#1b1f52) sits flush over the right panel,
which uses the same color -- one continuous blue family, no seam. */
#topbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
z-index: 10;
display: flex;
align-items: center;
padding: 0 24px;
box-sizing: border-box;
background: linear-gradient(90deg, #1c75bc 0%, #1c4f8f 45%, #1b1f52 100%);
color: #ffffff;
font-family: "Montserrat", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}
#topbar .brand {
font-size: 16px;
font-weight: 600;
letter-spacing: 0.02em;
}
#topbar .page {
margin-left: 16px;
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 13px;
font-weight: 400;
opacity: 0.85;
}
#samples-toggle {
margin-left: auto;
padding: 5px 14px;
background: transparent;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 6px;
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 12px;
cursor: pointer;
}
#samples-toggle:hover { border-color: #ffffff; background: rgba(255, 255, 255, 0.1); }
#redoc { padding-top: 64px; }
/* Collapsible samples column: Redoc CE has no built-in panel toggle, so
the topbar button flips .samples-collapsed on <html>, hiding the
right-panel divs (same bundle-pinned classes as the gradient below)
and letting each section's content half take the full width. If the
pinned classes stop matching after a Redoc bump the toggle goes inert
-- cosmetic only. Choice persists in localStorage. */
html.samples-collapsed .sc-iGgWBj.sc-gsFSXq,
html.samples-collapsed div.sc-dExYaf { display: none; }
html.samples-collapsed [data-section-id] > div:first-child { width: 100%; }
/* Right-panel gradient (#1b3d79 -> #1b1f52, continuing the topbar blend).
Redoc's theme only accepts solid colors (it derives shades from
rightPanel.backgroundColor), so the solid #1b1f52 stays in the theme as
the fallback and this override layers the gradient on top.
BUNDLE-PINNED SELECTORS: .sc-iGgWBj.sc-gsFSXq are the styled-components
classes of the per-section right-panel divs (plus .sc-dExYaf, the
full-height background stub) generated by the vendored
redoc.standalone.js 2.5.3. They are deterministic for this exact bundle
but WILL change on any Redoc bump -- re-derive them then (headless probe:
find elements whose computed background is the rightPanel color). If
they stop matching, the panel silently falls back to solid #1b1f52 --
cosmetic only, nothing breaks. Every stripe shares the same horizontal
geometry, so the per-section gradients read as one continuous column. */
.sc-iGgWBj.sc-gsFSXq,
div.sc-dExYaf {
background-image: linear-gradient(90deg, #1b3d79 0%, #1b3068 45%, #1b1f52 100%);
}
</style>
</head>
<body>
<div id="topbar">
<span class="brand">Sea Haven Industries</span>
<button id="samples-toggle" type="button" aria-pressed="false">Hide samples</button>
<span class="page">Procurement Ingest API</span>
</div>
<div id="redoc"></div>
<script>__REDOC_JS__</script>
<script>
Redoc.init(
__OPENAPI_SPEC_JSON__,
{
sortRequiredPropsFirst: true,
expandResponses: "200",
scrollYOffset: 64,
theme: {
colors: {
primary: { main: "#1c75bc" }
},
typography: {
fontFamily: '"DM Sans", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif',
fontWeightBold: "600",
headings: {
fontFamily: '"Montserrat", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif',
fontWeight: "600"
},
code: {
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace'
}
},
sidebar: {
width: "244px",
backgroundColor: "#f9fafb",
textColor: "#262262"
},
rightPanel: {
backgroundColor: "#1b1f52"
},
fab: {
backgroundColor: "#1c75bc"
}
}
},
document.getElementById("redoc")
);
(function () {
var KEY = "procurement-docs-samples-collapsed";
var btn = document.getElementById("samples-toggle");
function apply(collapsed) {
document.documentElement.classList.toggle("samples-collapsed", collapsed);
btn.textContent = collapsed ? "Show samples" : "Hide samples";
btn.setAttribute("aria-pressed", String(collapsed));
}
var initial = false;
try { initial = localStorage.getItem(KEY) === "1"; } catch (e) {}
apply(initial);
btn.addEventListener("click", function () {
var collapsed = !document.documentElement.classList.contains("samples-collapsed");
try { localStorage.setItem(KEY, collapsed ? "1" : "0"); } catch (e) {}
apply(collapsed);
});
})();
</script>
</body>
</html>