From e97f0ea9b475715a1ea58110a27cdf50a2b3cb19 Mon Sep 17 00:00:00 2001 From: Jeffrey Hales Date: Fri, 30 Oct 2026 00:03:21 -0700 Subject: [PATCH] feat(portal): add service drawers and employee portal preview --- .gitignore | 3 + www/.env.example | 5 + www/integrations/employee-portal/README.md | 101 +++++++ www/integrations/nixos/README.md | 72 +++++ www/integrations/nixos/portal-secrets.nix | 26 ++ www/reports/SESSION_HANDOFF.md | 31 ++- ...loyee-portal-migration-brief-2026-10-08.md | 23 ++ www/reports/migration-summary.md | 17 ++ ...-technical-readiness-package-2026-10-22.md | 13 + ...a-consultation-isolated-test-2026-10-29.md | 12 + ...-service-portal-consolidated-2026-10-30.md | 225 ++++++++++++++++ ...rm-integration-specification-2026-10-28.md | 23 ++ www/src/components/Button.astro | 4 +- www/src/components/JotformIframe.astro | 17 +- www/src/components/ServiceInquiryDrawer.astro | 113 +------- www/src/components/pages/EmployeePortal.astro | 42 +++ www/src/content/pages/en/aba-therapy.mdx | 2 +- .../content/pages/en/autism-evaluations.mdx | 2 +- .../content/pages/en/learner-social-club.mdx | 2 +- www/src/content/pages/es/aba-therapy.mdx | 2 +- .../content/pages/es/autism-evaluations.mdx | 2 +- .../content/pages/es/learner-social-club.mdx | 2 +- www/src/data/service-inquiry.ts | 22 ++ www/src/layouts/PageLayout.astro | 3 + www/src/scripts/employee-portal.ts | 74 ++++++ www/src/scripts/service-inquiry.ts | 118 +++++++++ www/src/utils/drawer-preference.ts | 12 + www/src/utils/form-integration.ts | 4 +- www/src/utils/portal-crypto.mjs | 42 +++ www/src/utils/portal-input.mjs | 10 + www/tools/employee-portal-migrate.mjs | 90 +++++++ www/tools/jotform-consultation-browser.mjs | 7 +- www/tools/jotform-form-a.test.mjs | 7 +- www/tools/phase10-artifact-verify.mjs | 2 + www/tools/phase10-artifact.test.mjs | 11 + www/tools/portal-resources.mjs | 90 +++++++ www/tools/seo.test.mjs | 12 +- www/tools/service-portal-browser.mjs | 247 ++++++++++++++++++ www/tools/service-portal.test.mjs | 86 ++++++ 39 files changed, 1446 insertions(+), 130 deletions(-) create mode 100644 www/integrations/employee-portal/README.md create mode 100644 www/integrations/nixos/README.md create mode 100644 www/integrations/nixos/portal-secrets.nix create mode 100644 www/reports/phase-9-service-portal-consolidated-2026-10-30.md create mode 100644 www/src/components/pages/EmployeePortal.astro create mode 100644 www/src/data/service-inquiry.ts create mode 100644 www/src/scripts/employee-portal.ts create mode 100644 www/src/scripts/service-inquiry.ts create mode 100644 www/src/utils/drawer-preference.ts create mode 100644 www/src/utils/portal-crypto.mjs create mode 100644 www/src/utils/portal-input.mjs create mode 100644 www/tools/employee-portal-migrate.mjs create mode 100644 www/tools/portal-resources.mjs create mode 100644 www/tools/service-portal-browser.mjs create mode 100644 www/tools/service-portal.test.mjs diff --git a/.gitignore b/.gitignore index e84420c..b56ac25 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ temp/ .DS_Store Thumbs.db Desktop.ini + +# Private portal collection, mappings and ciphertext; never a public asset source. +/www/private/employee-portal/ diff --git a/www/.env.example b/www/.env.example index 8103059..2208b06 100644 --- a/www/.env.example +++ b/www/.env.example @@ -18,3 +18,8 @@ PUBLIC_AIA_FORM_A_PRODUCTION_APPROVED=false # Explicit, local-only Form A meeting preview; requires PUBLIC_AIA_FORMS_MODE=mock. PUBLIC_AIA_FORM_A_PREVIEW=false PUBLIC_ALLOW_INDEXING=false + +# Employee portal ciphertext is an opt-in local compatibility preview only. +# Never supply a passphrase through environment variables or build arguments. +AIA_PORTAL_LOCAL_PREVIEW_APPROVED=false +# AIA_PORTAL_CIPHERTEXT_FILE=/owner-controlled/private/ciphertext.json diff --git a/www/integrations/employee-portal/README.md b/www/integrations/employee-portal/README.md new file mode 100644 index 0000000..7569030 --- /dev/null +++ b/www/integrations/employee-portal/README.md @@ -0,0 +1,101 @@ +# Employee portal compatibility and private migration + +Recorded 2026-10-30. Browser-side shared-code decryption is approved only for +local compatibility development. It is not authentication, individual revocation, +a secure session or a HIPAA compliance determination. Resources need separate +server authorization before cutover. + +## Source and private collection + +Read-only legacy footer source: +`/home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt`. +The original stays outside Git as rollback evidence. No access code was available +to the agent. The exact resource count and real content compatibility remain +unverified. Do not infer the earlier approximate 43 links as a measured count. + +From `www`, in the owner's private interactive terminal: + +```sh +node tools/employee-portal-migrate.mjs collect \ + /home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt +``` + +Enter each legitimate view code at its hidden prompt. Never place codes in +arguments, environment variables, shell history or reports. The utility checks +both streams, keeps plaintext in memory, and collects only referenced `/hubfs/` +resources from the verified source origin. Other origins require explicit +review/code allowlisting; redirects are bounded and never inherit credentials. +No Jotform, Google or other vendor forms are fetched. + +Private output is `private/employee-portal/`: directories 0700, manifests and +binary resources 0600. This exact subtree is gitignored and outside Astro's +public directory. Filenames are SHA-256 hashes of source URLs, independently +verified by content hashes. The manifest retains original references, labels, +view membership, MIME, bytes, status and delivery/classification decisions. +All new resources default to UNRESOLVED. Publicly reachable does not mean public +approved. Resuming verifies an existing file's content hash; mismatches are not +overwritten. HTML error documents, executable responses, invalid known signatures, +redirects outside approved destinations and resources over 50 MiB are rejected. +The first pass allows only the verified site origin. If a legitimate `/hubfs/` +response redirects to a HubSpot CDN, inspect that exact HTTPS origin first, then +add its origin to the private manifest's `approvedOrigins` list and rerun. Never +approve a redirect based only on a failed download or a broadly shared vendor +domain. Failures remain private review items. The utility prints counts only. + +Before every artifact transfer, verify that only `dist` is copied and that it +contains no private manifest, document or real ciphertext unless that ciphertext +has separately been authorized for the local preview. Never archive the whole +working directory as a deployable artifact. + +## Approved rewriting + +The owner classifies records as PUBLIC-APPROVED, STAFF-INTERNAL, +MANAGEMENT-INTERNAL or UNRESOLVED in the private manifest. Set `deliveryPath`, +`rewriteApproved`, `destinationImplemented`, `destinationVerified`, and (for +internal documents) `destinationAccessControlled` only with actual evidence. +The utility does not create those destinations or infer their approval. + +```sh +node tools/employee-portal-migrate.mjs rewrite \ + /home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt +``` + +It reauthenticates both views, changes approved quoted href/src attributes only, +preserves unrelated HTML/labels, and re-encrypts with fresh salts and IVs. It +verifies round-trip decryption and distinct codes before writing new ciphertext +with exclusive filenames. The original source is untouched. CSS/srcset mappings +require separate review and are rejected rather than silently rewritten. +Unresolved links retain their original destination as a recorded dependency, +not a claim of verified destination health. A changed manifest alone never +changes links in the encrypted stream. + +## Local preview and content safety + +Use a private ciphertext file generated by `collect` or `rewrite`: + +```sh +AIA_PORTAL_LOCAL_PREVIEW_APPROVED=true \ +AIA_PORTAL_CIPHERTEXT_FILE=/absolute/private/ciphertext-TIMESTAMP.json \ +PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test \ +PUBLIC_AIA_FORMS_MODE=off PUBLIC_ALLOW_INDEXING=false \ +npm run dev -- --host 127.0.0.1 +``` + +The page defaults to locked/unavailable without approved valid input. Indexing +builds cannot include it. The codes never go to a server, storage or URL. No +native form submits; Enter and Unlock use local WebCrypto. Only the matching +stream is displayed; refresh or leaving the page locks it again. + +The passive DOM renderer preserves headings, lists, text, tables and safe +HTTPS/mailto/tel links, with no referrer on links. It rejects scripts, inline +handlers, styles, media, embedded forms and unknown tags. Real portal markup +has not been decrypted or reviewed; any removed behavior must be reviewed with +the owner before claiming fidelity. It must not silently become arbitrary +script execution. No real encrypted stream or plaintext is in Git. + +Before owner acceptance: validate two real view codes privately, inspect every +resource/category, responsive directories, focus, link semantics and sanitization +impact. Check wrong-code feedback, keyboard unlock, no-JS explanation and refresh. +Production must use a separately reviewed runtime ciphertext delivery mechanism; +this batch intentionally provides only a gated local build input. Browser-delivered +ciphertext is publicly downloadable by anyone able to reach that preview. diff --git a/www/integrations/nixos/README.md b/www/integrations/nixos/README.md new file mode 100644 index 0000000..4d16d1f --- /dev/null +++ b/www/integrations/nixos/README.md @@ -0,0 +1,72 @@ +# Inactive SOPS/age preparation, 2026-10-30 + +The owner selected SOPS, age and sops-nix. This directory is a template, not a +host configuration. The development flake remains unchanged. No real recipient, +key, encrypted secret or server user is supplied. No module is deployed. + +Official references inspected for this pass: +- [SOPS documentation](https://getsops.io/docs/) +- [sops-nix installation and secret permissions](https://github.com/Mic92/sops-nix) + +SOPS encrypts configuration at rest. sops-nix can supply decrypted values at +runtime under `/run/secrets` with explicit owners and modes. This does not make +browser ciphertext or publicly served documents private. + +## Owner setup before activation + +1. Supply the actual NixOS topology, host flake, deploy user, persisted key + location, backup owner, HTTPS termination and resource-service identity. +2. Pin a reviewed `github:Mic92/sops-nix` input in that host flake; import + `inputs.sops-nix.nixosModules.sops` and the reviewed template. Do not add the + input to the Astro development flake just to evaluate a deployment sketch. +3. The owner generates independent developer/recovery and VPS age identities in + private terminals. Record only public recipients in the reviewed SOPS + creation rule. Use distinct recipients, never a placeholder recipient. Keep + the VPS private identity on persistent root-controlled storage (0600), outside + `/nix/store`, Git, the public static root and CI artifacts. +4. Encrypt the approved credential material using SOPS with both recipients. + Review recipient access, least privilege and an offline recovery copy. SOPS + encrypted files may enter a deployment repository only after owner approval. +5. Set `aia.portalSecrets` options explicitly, then evaluate the complete host + configuration and test decryption on authorized nonproduction infrastructure. + Runtime consumers use the generated `.path`, not Nix evaluation of plaintext. +6. Decide which service reloads after rotation. Rotate application credentials + as well as SOPS recipients when revoking access: rewrapping alone does not + invalidate an already copied password. Test recovery and key restoration. + +## Separate staff and management resource authorization + +Keep original documents outside Astro `public`, `dist`, release archives and +CDN roots. Proposed delivery namespaces are `/staff-resources/` and +`/management-resources/`; neither is implemented or considered working here. +Use nginx `auth_request` to an approved authorization service or an equivalent +server-enforced policy with individually revocable identities and staff and +management roles. Staff credentials must not authorize management resources; +management access to staff resources must be an explicit policy. Backend file +locations must be internal, inaccessible through static aliases or alternate +paths. Never trust a browser-provided role header. + +A separate shared Basic Auth credential file per namespace can be considered +only as an explicitly approved weaker interim model, not identity management or +automatic role inheritance. The template prepares separate secret inputs without +selecting that access model or installing an identity provider. + +Use HTTPS, `Cache-Control: private, no-store`, no shared/CDN caching, and +`Referrer-Policy: no-referrer` for protected delivery. Avoid credentials and +sensitive resource paths in access/error logs; review monitoring and backups. +Authorization failures must fail closed, including at proxy/cache boundaries. +Test direct requests, alternate encodings, role crossover and rollback. + +## Distinct boundaries and release checks + +- SOPS: deployment secrets at rest and controlled runtime access. +- Portal HTML: shared-code browser decryption, offline-guessable access friction. +- Documents: require server authorization even if URLs are encrypted in the UI. + +The compatibility preview cannot grant production portal access. Before cutover, +classify every document, implement and verify its delivery destination, rewrite +and re-encrypt links, test two-view separation, rotate shared codes privately, +check the public artifact for private files, and obtain owner/security approval. +Back up encrypted configuration and documents separately from keys; rehearse +restore and rollback. Revoking a static release cannot retract ciphertext or +files already downloaded by visitors. Site cutover is still unauthorized. diff --git a/www/integrations/nixos/portal-secrets.nix b/www/integrations/nixos/portal-secrets.nix new file mode 100644 index 0000000..c1881d4 --- /dev/null +++ b/www/integrations/nixos/portal-secrets.nix @@ -0,0 +1,26 @@ +# INACTIVE TEMPLATE. Not imported by the development flake or any deployed host. +# Requires an owner-reviewed sops-nix NixOS module import in the host flake. +{ config, lib, ... }: +let cfg = config.aia.portalSecrets; +in { + options.aia.portalSecrets = { + enable = lib.mkEnableOption "owner-reviewed portal deployment secrets"; + encryptedFile = lib.mkOption { type = lib.types.path; description = "SOPS-encrypted YAML only; never a plaintext file"; }; + ageKeyFile = lib.mkOption { type = lib.types.str; description = "Private persisted host age key path outside the Nix store"; }; + readerUser = lib.mkOption { type = lib.types.str; description = "Approved resource authorization service user"; }; + }; + config = lib.mkIf cfg.enable { + assertions = [{ assertion = lib.hasPrefix "/" cfg.ageKeyFile && !(lib.hasPrefix "/nix/store/" cfg.ageKeyFile); + message = "The host age identity must be an absolute runtime path outside the Nix store."; }]; + sops.defaultSopsFile = cfg.encryptedFile; + sops.age.keyFile = cfg.ageKeyFile; + sops.age.generateKey = false; + sops.age.sshKeyPaths = []; + sops.gnupg.sshKeyPaths = []; + sops.secrets."portal/staff-credentials" = { owner = cfg.readerUser; mode = "0400"; }; + sops.secrets."portal/management-credentials" = { owner = cfg.readerUser; mode = "0400"; }; + # Consumers reference config.sops.secrets..path at runtime. + # Do not use builtins.readFile on a decrypted secret, environment.etc.text, + # PUBLIC_* values, or Astro/Vite imports to transport secrets. + }; +} diff --git a/www/reports/SESSION_HANDOFF.md b/www/reports/SESSION_HANDOFF.md index 8537570..faa005e 100644 --- a/www/reports/SESSION_HANDOFF.md +++ b/www/reports/SESSION_HANDOFF.md @@ -1,9 +1,9 @@ # AIA Astro Migration — Living Session Handoff -**Last updated:** October 29, 2026 (UTC) +**Last updated:** October 30, 2026 (UTC) **Repository:** `AIAMarketing/azinstitute4autism.com` **Working branch:** `faithful-astro-migration` -**Last accepted technical implementation:** Phase 9 bounded landing-page integration (presentation and inactive lead UI only; human decisions and production activation pending) +**Last accepted technical implementation:** Phase 9 isolated consultation integration at `ff80ee516f0185023af1f29937e29dfb30d31f47`; this handoff also describes the consolidated service-drawer and employee-portal checkpoint candidate **Previous accepted checkpoint:** P9-03 documentation, signed `537f9864a0a3d0806180cbeca909bf1d03200c6f` **Implementation checkpoint:** the commit containing this handoff update; obtain its actual SHA from Git history after the user creates the signed commit. **Next milestone:** continue Phase 9 owner decisions and authorized integration batches; then Phase 10 — Final QA/Cutover Readiness. @@ -374,3 +374,30 @@ The old `.phase7-stage-paths.nul` is absent; do not recreate it. The staging aid is for the user's host terminal only; never stage it or use a broad Git add. The actual future checkpoint SHA comes from Git after the user signs; do not amend merely to insert it here. + +## Consolidated service drawers and portal checkpoint, 2026-10-30 + +This section supersedes earlier explicit-only drawer behavior. The checkpoint +containing this update adds four-second/50%-scroll consultation opening, one +shared 30-day localStorage expiry preference, manual override through the six +existing EN/ES service CTAs, and typed bilingual service introductions. The +Academy configuration is prepared without a new page. The fixed seven production +and separate nine consultation-test route lists remain unchanged. Consultation +indexing approval must explicitly be `false`. + +The employee route now has a locked, local-decryption UI and a private owner-run +collection/rewrite utility. Real passphrases were unavailable: real portal +content/resource inventory and collection remain blocked pending the owner-run +step. Do not claim approximately 43 resources as a measured inventory. Do not +commit the original source, ciphertext, decrypted views or private resources. +`www/private/employee-portal/` is ignored and outside Astro public output. +The accepted Jotform CSS backup and reconciliation archives are unchanged. + +SOPS/age/sops-nix preparation is inactive, with no generated production identity, +recipient, deployed module or server-access activation. Portal browser encryption +remains access friction; internal resources require separate server-enforced +staff/management authorization. See +[consolidated implementation and validation](phase-9-service-portal-consolidated-2026-10-30.md) +and [owner utility instructions](../integrations/employee-portal/README.md). +The actual checkpoint SHA is the commit containing this section; obtain it from +Git history. No push, deployment or cutover is authorized by this work. diff --git a/www/reports/employee-portal-migration-brief-2026-10-08.md b/www/reports/employee-portal-migration-brief-2026-10-08.md index 3d0a2d0..ed08827 100644 --- a/www/reports/employee-portal-migration-brief-2026-10-08.md +++ b/www/reports/employee-portal-migration-brief-2026-10-08.md @@ -153,3 +153,26 @@ This is **documentation only**. It must not change Phase 3A publication policy, Phase 3B hreflang implementation, Astro routing, production access, forms, analytics, or server configuration. It does not claim authentication, HIPAA compliance, or release readiness. + +## 2026-10-30 owner authorization and implementation addendum + +The owner explicitly authorized the two-stream browser-encrypted compatibility +model for development, superseding the earlier implementation hold for that +bounded scope. Production authorization and server-side resource protection are +still unresolved. The new scoped EmployeePortal component preserves the existing +route/SEO policy, local password entry, HMAC verification before decryption, +locked/unavailable default and passive content rendering. + +The read-only source is at +`/home/alice/projects/wrk/aia/local-archives/aia-employee-portal-hubspot-footer-source.txt`. +Its two-stream structure and algorithm were inspected without exposing ciphertext +or plaintext. Legitimate codes were not available to the agent. Real-view +fidelity and exact resource counts remain blocked, not accepted. No real document +was fetched or published. The owner-run migration utility performs private +collection and approved link rewriting/re-encryption; see +[private workflow instructions](../integrations/employee-portal/README.md). + +SOPS/age/sops-nix templates are prepared but inactive. Encryption of deployment +secrets does not authorize public delivery of staff/management documents. The +[consolidated report](phase-9-service-portal-consolidated-2026-10-30.md) records +synthetic tests and the separate remaining owner/cutover gates. diff --git a/www/reports/migration-summary.md b/www/reports/migration-summary.md index db1076d..1255072 100644 --- a/www/reports/migration-summary.md +++ b/www/reports/migration-summary.md @@ -4333,3 +4333,20 @@ The owner also authorized `/lp/early-intervention`. It is migrated independently At the owner's request, added a simple five-link review index modeled on the publicly retrieved `https://astro.web3app.dev/` index (normal TLS). `src/pages/lp/[...review].astro` generates only `/lp`, only when `PUBLIC_ALLOW_INDEXING` is not `true`. It is explicitly labeled temporary/testing, always `noindex,nofollow`, absent from sitemap/hreflang and normal navigation, and links only to eligible local landing-page records. No submissions, scripts, tracking or external assets were added. Production/indexing-enabled output remains 127 routes; normal staging has 128, including this review utility. Indexing is a build gate, not access control: never deploy the staging artifact as a production release. Build and link audit pass. Library regression coverage checks the exact five destinations, staging robots, sitemap exclusion and absence from the indexing-enabled build. Guarded Chromium checks at 320/1440px pass with visible keyboard focus, no overflow and all five local links returning 200; no external browser requests occurred. Screenshots and browser evidence are temporary at `/tmp/aia-campaign-browser-HdWNvy/`. Prior five-page integration evidence remains historical; the new offline reconciliation records the extra staging utility. Nothing was staged, committed or deployed. + +## 2026-10-30 service and employee-portal development revision + +Owner-authorized intentional deviations: service CTAs now say Request a +Consultation / Solicitar una consulta and progressively open authorized test +drawers; automatic timing is four seconds or half-page scroll with a shared +30-day dismissal preference. Service-specific bilingual copy replaces generic +introductions. The same page layout, slide-in treatment and iframe restrictions +remain. Live public read-only captures were cache-qualified editorial reference, +not a new clinical approval. + +The metadata-only employee route now includes the source-backed Access Code +Required interface. Real encrypted views remain unavailable without private +owner input. A passive DOM renderer intentionally disallows executable markup, +remote media and embedded forms; real-view fidelity therefore needs owner review. +No internal resource was published. Full evidence and boundaries are in +[the consolidated report](phase-9-service-portal-consolidated-2026-10-30.md). diff --git a/www/reports/phase-10-technical-readiness-package-2026-10-22.md b/www/reports/phase-10-technical-readiness-package-2026-10-22.md index 3210654..050a01a 100644 --- a/www/reports/phase-10-technical-readiness-package-2026-10-22.md +++ b/www/reports/phase-10-technical-readiness-package-2026-10-22.md @@ -64,3 +64,16 @@ P9-01 through P9-10 remain **Pending decision**. No technical feasibility result ## Recommended checkpoint boundaries Keep the existing Phase 9 implementation, audit tooling and reports as one owner-reviewed technical batch. If a signed checkpoint is requested, stage exact paths only after reviewing the final status and reports. Keep any subsequent host rehearsal, Jotform configuration, counter live adapter and measurement work in separately authorized commits. + +## 2026-10-30 portal and secrets preparation addendum + +The local portal compatibility implementation does not close P9-02 or P9-09. +The owner must supply legitimate codes privately to collect and classify legacy +resources, accept the two decrypted views, approve delivery per resource and +verify server-side staff/management separation. SOPS preparation is an inactive +[module and operational guide](../integrations/nixos/README.md), not deployed +infrastructure. Real age recipients, host topology, access policy, recovery and +runtime key provisioning remain pending. Do not copy `private/` into a release. +The artifact verifier now rejects private resource paths and portal ciphertext +from release artifacts. A local ciphertext preview is deliberately not a +releasable artifact. No site cutover authorization is implied. diff --git a/www/reports/phase-9-form-a-consultation-isolated-test-2026-10-29.md b/www/reports/phase-9-form-a-consultation-isolated-test-2026-10-29.md index 624c683..cb9d2b2 100644 --- a/www/reports/phase-9-form-a-consultation-isolated-test-2026-10-29.md +++ b/www/reports/phase-9-form-a-consultation-isolated-test-2026-10-29.md @@ -76,3 +76,15 @@ Focused resolver/artifact/nonproduction tests passed **21/21**. The existing For | Consultation isolated-test | 128 | 0 | 0 | 10 | 6 | In the isolated artifact, ten static frames are the seven frozen contact/landing paths plus the three standalone consultation pages. The six service-drawer adapters retain an approved source only as deferred data until a visitor opens the dialog. The final normal staging build was restored: 128 `noindex,nofollow` pages, an empty sitemap, zero hreflang links and zero Form A frames. Link, image and blog audits reported 0 broken internal links, 0 missing mapped page images and 0 blog-audit failures. + +## Later consolidated owner revision, 2026-10-30 + +The explicit-only drawer behavior above is the earlier checkpoint's historical +evidence. The subsequent consolidated pass replaces it with four-second/50% +automatic opening, global 30-day expiry-only suppression, existing CTA override +and bilingual service-specific introductory copy. Iframe sources remain deferred +until opening, which may now be automatic under the authorized test mode. +The production allowlist is unchanged. See the current +[consolidated report](phase-9-service-portal-consolidated-2026-10-30.md) for tests, +privacy boundaries and remaining owner acceptance. Historical test counts above +are not claims about the revised behavior. diff --git a/www/reports/phase-9-service-portal-consolidated-2026-10-30.md b/www/reports/phase-9-service-portal-consolidated-2026-10-30.md new file mode 100644 index 0000000..9139a0e --- /dev/null +++ b/www/reports/phase-9-service-portal-consolidated-2026-10-30.md @@ -0,0 +1,225 @@ +# Consolidated service-drawer and employee-portal development + +Date: 2026-10-30 UTC. Starting branch: `faithful-astro-migration`. +Starting HEAD: `ff80ee516f0185023af1f29937e29dfb30d31f47`. +Tracked tree and index were clean; only owner-provided `prompt.txt` was untracked. +This is a local development checkpoint, not production authorization. + +## Service implementation + +The six existing EN/ES ABA Therapy, Autism Evaluations and Learner Social Club +pages retain their URLs and consultation anchor destinations. Each existing +anchor now has an explicit service trigger and accurate consultation label. +Only an authorized isolated-test drawer intercepts ordinary anchor activation; +modified clicks and no-JavaScript/unauthorized builds preserve navigation. +No competing opener is added. + +`service-inquiry.ts` owns four-second and half-scroll triggers, first-trigger-wins, +positive scroll-distance handling, restored/changed height, resize observation, +disposal, reduced-motion close, native dialog focus and close behavior. Competing +modal/field editing cancels an automatic opportunity without polling. Closing +stores one shared 30-day expiry timestamp, including manual-close renewals. +Invalid, expired and implausibly future values are ignored. Storage denial uses +memory for the current document; across full navigation suppression cannot be +guaranteed when storage is disabled or cleared. No service or entered value is +stored. Closing/reopening preserves the one deferred iframe and its data. + +`service-inquiry.ts` does not modify Jotform or detect submissions. The local +preference is functional only. Service-specific EN/ES copy is centralized in +`data/service-inquiry.ts`, including an unpublished Academy configuration without +an invented route. ABA 18 months–8 years, Academy 2–6 and Social Club 8–17 are +kept distinct. No conditional diagnostic note, booking promise or preselection +is added. Existing evaluation-page body claims still need the separately tracked +clinical/editorial review; this pass changes CTAs and drawer copy only. + +The separate nine-route consultation gate remains isolated-test only. A missing +or malformed indexing flag previously passed that consultation check; it now +requires explicit `PUBLIC_ALLOW_INDEXING=false`. The seven production-eligible +contact/landing paths are unchanged. Full test configuration: + +```sh +PUBLIC_AIA_FORMS_MODE=isolated-test +PUBLIC_AIA_FORMS_ENVIRONMENT=isolated-test +PUBLIC_AIA_FORMS_ISOLATED_TEST_APPROVED=true +PUBLIC_AIA_FORMS_LIVE_APPROVED=false +PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true +PUBLIC_ALLOW_INDEXING=false +``` + +## Portal implementation and fidelity boundary + +The scoped `EmployeePortal.astro` preserves the route, metadata/noindex, sitemap +exclusion and lack of hreflang. It provides the source-backed access-code UI, +local Enter/Unlock handling without a native submitting form, accessible feedback, +HMAC-before-decrypt compatibility, two-stream selection, focused passive content, +password clearing after unlock, refresh/BFCache relocking and no-JavaScript help. +No plaintext/code goes to a server, browser storage or URL. + +The original source remains read-only outside Git. Structural inspection verified +two streams, PBKDF2-SHA256 200,000 iterations, AES-256-CBC and HMAC-SHA256 over +IV plus ciphertext, with 16-byte salt/IV and 32-byte MAC. No legitimate passphrase +was available. An independently constructed Node crypto fixture establishes +algorithm compatibility, but actual legacy decryption has not been performed. +Real content layout, categories, contacts and linked workflows remain unverified. + +Passive DOM reconstruction preserves text, headings, lists, tables and safe +links. Scripts, handlers, inline CSS, media and embedded forms are not executed. +Any real-source reliance on removed markup requires review; visual parity is not +claimed. Shared-code ciphertext remains offline-guessable access friction, +not authentication or a compliance certification. + +`portal-input.mjs` accepts only an explicit local-preview approval, isolated +role, false indexing and validated private ciphertext. Default, missing and +production-shaped builds remain unavailable. The release verifier rejects portal +ciphertext and private paths. No real ciphertext or resource is committed. + +## Sanitized resource reconciliation + +| Measure | Verified result | +|---|---| +| Original encrypted views | 2; structural metadata only | +| Total original /hubfs/ references | Unknown, decryption blocked | +| Distinct resources / duplicates | Unknown, not assumed to be 43 | +| Successfully downloaded real resources | 0 | +| Failed/unavailable resource downloads | Not tested; no decrypted URLs available | +| Public-approved resources | 0 approved by this task | +| Staff/internal and management/internal counts | Unknown pending decryption/classification | +| Unresolved resource classifications | All real inventory remains unreviewed; count unknown | +| Real links rewritten | 0 | +| Links awaiting protected destinations | Unknown pending inventory | +| Remaining HubSpot dependencies | Unresolved encrypted references; not declared migrated | +| SOPS/NixOS | Inactive template prepared; no real keys or deployment | + +The owner-run utility extracts decrypted references in memory, deduplicates by +source URL, collects bounded HTTPS resources into ignored 0700 directories, +retains hashes/size/MIME/status in a private 0600 manifest, and defaults to +UNRESOLVED. It rejects unapproved redirects and executable/error responses. +Approved rewriting requires implemented/verified destinations and server access +control for internal files; it regenerates salts/IVs, verifies round trips and +keeps the original as rollback. No mapping is considered migrated merely because +a proposed URL exists. See [owner commands](../integrations/employee-portal/README.md). + +## Source and infrastructure evidence + +Read-only public source references on this date: +[ABA](https://www.azinstitute4autism.com/aba-therapy), +[evaluations](https://www.azinstitute4autism.com/autism-evaluations), +[Social Club](https://www.azinstitute4autism.com/learner-social-club), +[locked portal](https://www.azinstitute4autism.com/employee-portal). +These were cache-qualified text captures. No authenticated live view or private +source content was retrieved through the web tool. The owner-provided modal copy +is the current development authority; fluent review remains pending. + +The existing development flake and deployment plan were inspected. The inactive +[NixOS guide](../integrations/nixos/README.md) cites current official SOPS/sops-nix +documentation. It separates deployment encryption, browser access friction and +server resource authorization. No host topology, real key/recipient or access +service is invented. The module is not imported, activated or deployed. + +## Validation + +`npm run check` passed across 149 Astro files with zero diagnostics. Sequential +core suites passed: Markdown 20/20, publication 33/33, SEO 35/35, Library 53/53, +routes 10/10, campaigns 11/11 and Phase 8 contrast 5/5. The combined current +Form A, nonproduction, artifact, drawer, crypto and resource tests passed 33/33; +the narrower final artifact/resource rerun passed 18/18. The inactive Nix module +also passed `nix-instantiate --parse`. + +Mock, indexing-enabled, production-gated, isolated-test and final off/staging +builds passed the artifact verifier. The isolated artifact contains 128 HTML +routes, ten approved immediate Form A frames and six approved deferred drawer +frames. The production-gated artifact retains exactly the original seven Form A +frames. The final restored staging artifact contains 128 `noindex,nofollow` +routes, an empty sitemap, zero hreflang and zero Form A frames. Link, image and +blog audits each reported zero failures. `git diff --check` passed. + +Guarded Chromium results: + +- Revised service/portal runner: 39 checks, 28 screenshots, six EN/ES services + at 320/390/768/1440px, timer and scroll races, shared suppression, storage and + modal/editing failure cases, lifecycle reinitialization, and synthetic staff + and management portal unlocks. It intercepted 31 fixed Jotform GET attempts. +- Nine-placement consultation regression: 45 checks, 36 screenshots and one + held-request fallback check; 40 fixed Jotform GET attempts intercepted. +- Existing seven-placement Form A regression: 35 checks, 28 screenshots and + four fallback-state checks; 38 Form A GET attempts intercepted. +- Off-mode browser verification attempted no Jotform request and emitted no + Form A adapter. + +The browser guards recorded attempted requests before fulfillment or blocking. +No non-GET/HEAD form request, Jotform submission, production API call, portal +password transfer, advertising request, analytics request or WebSocket was +transmitted. Child Form A responses were local fixtures; these runs do not prove +real Jotform application behavior. Portal fixtures used random synthetic codes +created in memory and were never written to Git. + +Final ephemeral evidence is outside Git: revised service and synthetic portal +screenshots/ledger at `/tmp/aia-service-portal-uncmM3`, consultation regression +at `/tmp/aia-form-a-consultation-zMsmpB`, and the frozen seven-placement Form A +regression at `/tmp/aia-form-a-iframe-77duED`. The normal staging artifact was +restored last and passed with 128 routes, zero sitemap/hreflang/Form A frames and +zero active native form actions. + +## Owner acceptance and cutover dependencies + +1. On loopback isolated preview, open each EN/ES service page at 320, 390, 768 and + 1440px. Verify four-second opening and (after clearing the functional preference) + half-scroll opening; dismiss, navigate across services/locales, verify suppression + and manual CTA reopening. Inspect real Form A errors, Submit reachability and + focus across the frame. Do not infer child application success from frame load. +2. Review provisional Spanish/service copy and real assistive-technology behavior. +3. Run the private collection utility with legitimate codes, review local manifest, + classify resources, resolve failed/redirected downloads and approve destinations. +4. Test real staff and management decryption with local ciphertext preview, inspect + sanitizer effects, links, responsive directories, keyboard and refresh behavior. +5. Supply actual NixOS topology and separate age recipients privately, approve + staff/management authorization and recovery, and verify delivery before rewriting. +6. Forms C, production consultation routes, portal access, protected document + serving and the overall site cutover all require separate authorization. + +## Exact checkpoint file inventory + +- `.gitignore` +- `www/.env.example` +- `www/integrations/employee-portal/README.md` +- `www/integrations/nixos/README.md` +- `www/integrations/nixos/portal-secrets.nix` +- `www/reports/SESSION_HANDOFF.md` +- `www/reports/employee-portal-migration-brief-2026-10-08.md` +- `www/reports/migration-summary.md` +- `www/reports/phase-10-technical-readiness-package-2026-10-22.md` +- `www/reports/phase-9-form-a-consultation-isolated-test-2026-10-29.md` +- `www/reports/phase-9-service-portal-consolidated-2026-10-30.md` +- `www/reports/phase-9-two-form-integration-specification-2026-10-28.md` +- `www/src/components/Button.astro` +- `www/src/components/JotformIframe.astro` +- `www/src/components/ServiceInquiryDrawer.astro` +- `www/src/components/pages/EmployeePortal.astro` +- `www/src/content/pages/en/aba-therapy.mdx` +- `www/src/content/pages/en/autism-evaluations.mdx` +- `www/src/content/pages/en/learner-social-club.mdx` +- `www/src/content/pages/es/aba-therapy.mdx` +- `www/src/content/pages/es/autism-evaluations.mdx` +- `www/src/content/pages/es/learner-social-club.mdx` +- `www/src/data/service-inquiry.ts` +- `www/src/layouts/PageLayout.astro` +- `www/src/scripts/employee-portal.ts` +- `www/src/scripts/service-inquiry.ts` +- `www/src/utils/drawer-preference.ts` +- `www/src/utils/form-integration.ts` +- `www/src/utils/portal-crypto.mjs` +- `www/src/utils/portal-input.mjs` +- `www/tools/employee-portal-migrate.mjs` +- `www/tools/jotform-consultation-browser.mjs` +- `www/tools/jotform-form-a.test.mjs` +- `www/tools/phase10-artifact-verify.mjs` +- `www/tools/phase10-artifact.test.mjs` +- `www/tools/portal-resources.mjs` +- `www/tools/seo.test.mjs` +- `www/tools/service-portal-browser.mjs` +- `www/tools/service-portal.test.mjs` + +The owner-provided `prompt.txt` remains untracked and outside the checkpoint. +Private directories contain no collected real resources in this run. Protected +`merge-plan.md`, archived reconciliations and the accepted Jotform CSS backup +are untouched. diff --git a/www/reports/phase-9-two-form-integration-specification-2026-10-28.md b/www/reports/phase-9-two-form-integration-specification-2026-10-28.md index 6ce5485..a707675 100644 --- a/www/reports/phase-9-two-form-integration-specification-2026-10-28.md +++ b/www/reports/phase-9-two-form-integration-specification-2026-10-28.md @@ -100,3 +100,26 @@ This decision does not close P9-01, P9-03, P9-05, P9-06, P9-07, P9-09 or P9-10. The owner reports that the Form A `consultation` context and its conditional rules have been configured through the connected Jotform app. The repository now permits the nine matrix placements only when every isolated-test gate is present, including `PUBLIC_AIA_FORMS_CONSULTATION_TEST_APPROVED=true`. Three standalone pages load the fixed consultation URL immediately; six English/Spanish service drawers retain no iframe `src` until the visitor explicitly opens them. This does not change the frozen seven-route production allowlist. The earlier version of this specification required a conditional Autism Diagnostic Evaluation explanation. That requirement is superseded: no conditional explanatory note is required in Form A or Astro. The historical decision is preserved by this dated addendum; the current contract is the client-advocate conversation described above. + +## 2026-10-30 consolidated drawer behavior revision + +The later owner prompt supersedes explicit-only opening for the six EN/ES +service drawers. Authorized consultation-test drawers now open after four seconds +or 50% of positive available page-scroll distance, whichever occurs first. +Opening consumes both automatic triggers. A different open modal or actively +edited field cancels that automatic opportunity without polling. Deferred +Jotform loading begins only when the drawer actually opens. + +Dismissal stores only `aia:service-inquiry:auto-suppressed-until`, a first-party +expiry timestamp for 30 days shared across services and locales. Existing +consultation anchors are explicitly marked as drawer triggers; they bypass +suppression manually and retain their consultation-page href without JavaScript +or an authorized drawer. Closing renews the preference. It is not an audience, +tracking identifier, service-selection record or form-response store. + +Centralized bilingual headings/introductions use provisional owner-supplied +service copy. The Academy service key has EN/ES copy for ages 2–6 but no route, +navigation change, new iframe allowance or publication entry. All original +seven production and nine isolated consultation placements remain unchanged. +No diagnostic conditional note or service preselection is introduced. The +clinical and fluent-language review gates remain open. diff --git a/www/src/components/Button.astro b/www/src/components/Button.astro index 3eb715d..1b6ffe9 100644 --- a/www/src/components/Button.astro +++ b/www/src/components/Button.astro @@ -1,6 +1,7 @@ --- interface Props { href?: string; + serviceInquiry?: string; variant?: 'filled' | 'outlined' | 'light'; size?: 'default' | 'small'; center?: boolean; @@ -14,6 +15,7 @@ interface Props { const { href, + serviceInquiry, variant = 'filled', size = 'default', center = false, @@ -34,7 +36,7 @@ const classes = [ --- {href ? ( - + ) : ( )} diff --git a/www/src/components/JotformIframe.astro b/www/src/components/JotformIframe.astro index 5e1ebdf..f87c066 100644 --- a/www/src/components/JotformIframe.astro +++ b/www/src/components/JotformIframe.astro @@ -88,13 +88,20 @@ const headingId = `${idBase}-heading`; if (timer) window.clearTimeout(timer); // A late frame load supersedes the transport timeout. It is still not // treated as a success, conversion, or cross-origin health signal. + container.dataset.jotformDocumentLoaded = 'true'; hideFallback(); }; - const dispose = () => { + const dispose = (event) => { disposed = true; if (timer) window.clearTimeout(timer); frame.removeEventListener('load', onLoad); container.removeEventListener('aia:jotform-activate', activate); + window.removeEventListener('pagehide', dispose); + document.removeEventListener('astro:before-swap', dispose); + delete container.dataset.jotformFallbackInitialized; + if (event?.type === 'pagehide' && event.persisted) { + window.addEventListener('pageshow', () => { if (container.isConnected) initialize(container); }, { once: true }); + } }; const activate = () => { @@ -103,9 +110,11 @@ const headingId = `${idBase}-heading`; const approvedUrl = frame.dataset.jotformIframeUrl; if (deferred && !approvedUrl) return; activated = true; - frame.addEventListener('load', onLoad, { once: true }); - timer = window.setTimeout(showFallback, fallbackTimeoutMs); - if (deferred) frame.src = approvedUrl; + if (container.dataset.jotformDocumentLoaded !== 'true') { + frame.addEventListener('load', onLoad, { once: true }); + timer = window.setTimeout(showFallback, fallbackTimeoutMs); + } + if (deferred && frame.getAttribute('src') !== approvedUrl) frame.src = approvedUrl; container.dataset.jotformIframeActivated = 'true'; }; diff --git a/www/src/components/ServiceInquiryDrawer.astro b/www/src/components/ServiceInquiryDrawer.astro index 3489328..eca072d 100644 --- a/www/src/components/ServiceInquiryDrawer.astro +++ b/www/src/components/ServiceInquiryDrawer.astro @@ -8,7 +8,7 @@ import { resolveMockFormResult, } from '../utils/form-integration'; -type ServiceKey = 'aba-therapy' | 'autism-evaluations' | 'learner-social-club'; +import { serviceInquiryCopy, type ServiceKey } from '../data/service-inquiry'; type SupportedLocale = 'en' | 'es'; interface Props { @@ -22,11 +22,13 @@ const serviceNames: Record> = { 'aba-therapy': 'ABA Therapy', 'autism-evaluations': 'Autism Evaluations', 'learner-social-club': 'Learner Social Club', + 'aia-preparatory-academy': 'AIA Preparatory Academy', }, es: { 'aba-therapy': 'Terapia ABA', 'autism-evaluations': 'Evaluaciones de autismo', 'learner-social-club': 'Learner Social Club', + 'aia-preparatory-academy': 'AIA Preparatory Academy', }, }; @@ -116,6 +118,7 @@ const drawerCopy: Record -