diff --git a/.woodpecker/hypertwist-bootstrap.yml b/.woodpecker/hypertwist-bootstrap.yml index 6c8d2e7..55f5b26 100644 --- a/.woodpecker/hypertwist-bootstrap.yml +++ b/.woodpecker/hypertwist-bootstrap.yml @@ -57,3 +57,19 @@ steps: - npm test when: - event: [push, pull_request, manual, tag] + + website_live_deploy: + image: powershell + depends_on: + - website_frontend_validation + - website_auth_validation + commands: + - Set-Location repo/website + - $identityFile = if ($env:HYPERTWIST_VPS_SSH_KEY_PATH) { $env:HYPERTWIST_VPS_SSH_KEY_PATH } else { 'C:\ScriptoriumAI\ops\ssh\scriptorium_vps_ed25519' } + - $manifestPath = if ($env:HYPERTWIST_SAME_ORIGIN_MANIFEST) { $env:HYPERTWIST_SAME_ORIGIN_MANIFEST } else { 'deploy/hypertwist.same-origin.preview.bundle.example.json' } + - if (-not (Test-Path -LiteralPath $identityFile)) { throw "HyperTwist live deploy SSH key not found at '$identityFile'. Set HYPERTWIST_VPS_SSH_KEY_PATH on the Woodpecker builder." } + - if (-not (Test-Path -LiteralPath $manifestPath)) { throw "HyperTwist same-origin manifest not found at '$manifestPath'. Set HYPERTWIST_SAME_ORIGIN_MANIFEST or commit the intended manifest." } + - npm run run:vps-same-origin-live-deploy -- --manifest $manifestPath --identity-file $identityFile --wait-seconds 60 + when: + - event: push + branch: main diff --git a/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md b/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md index 116da87..e5b25a6 100644 --- a/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md +++ b/docs/ops/HYPERTWIST_WEBSITE_SAME_ORIGIN_DEPLOYMENT_HANDOFF_2026-06-22.md @@ -33,11 +33,16 @@ Current truthful interpretation: - the live public origin is still a preview deployment rather than full public launch because the current auth/launch JSON still reports missing Windows download, checkout, billing-map, and webhook-secret launch requirements -- a plain repo push is not itself a production deploy; live website changes are - only published after +- as of `2026-07-02`, `.woodpecker/hypertwist-bootstrap.yml` includes a + `website_live_deploy` step for `main` pushes that invokes `npm run run:vps-same-origin-live-deploy -- --manifest --identity-file ` - or an equivalent CI job that invokes the same helper and revalidates the - public origin + after website frontend and auth-server validation +- a repo push only counts as published after that helper-backed CI deploy, or a + manual invocation of the same helper, completes and revalidates the public + origin +- login/register overlay safety is now part of the browser launch invariant: + the decorative auth backdrop may render, but the auth form must stay visibly + above it on the live public origin This is the next operator step after: diff --git a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md index fdfba12..65651d3 100644 --- a/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md +++ b/docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md @@ -430,6 +430,11 @@ When a new packet materially adds or changes a normalized feature: review path - live commerce/download/auth configuration plus physical headset/controller observation remain explicit launch gates +- the same browser-account lane now treats auth-shell backdrop stacking as a + launch-readiness invariant rather than decoration-only CSS: `/login` and + `/register` must render their forms above the fixed decorative/auth backdrop, + and the website test suite now carries both source-level and browser-level + regression proof for that condition - this is a copy and layout coherence hardening pass, not a topology widening and not a promotion of the optional full-browser simulator branch diff --git a/website/deploy/README.md b/website/deploy/README.md index 589a2f2..5ca726e 100644 --- a/website/deploy/README.md +++ b/website/deploy/README.md @@ -46,9 +46,14 @@ Before using these templates: to stage the committed checkout, upload the rendered bundle, install env, build the site, replace the live `systemd` plus `nginx` files, and validate the public origin through the same readiness lane -- a plain git push does **not** publish `https://hypertwist.app` by itself; - production changes are only live after this helper, or a CI job that invokes - the same helper, has completed and the public origin has been revalidated +- as of `2026-07-02`, `.woodpecker/hypertwist-bootstrap.yml` includes a + `website_live_deploy` step for `main` pushes that invokes this same helper + after website frontend and auth-server validation, using + `HYPERTWIST_VPS_SSH_KEY_PATH` and `HYPERTWIST_SAME_ORIGIN_MANIFEST` when the + builder provides them +- a git push only counts as published after that helper-backed CI deploy, or a + manual invocation of the same helper, has completed and the public origin has + been revalidated - set `publicOrigin` to the exact live origin that should own both the website and auth cookies; for the canonical production lane that value is `https://hypertwist.app` diff --git a/website/src/__tests__/auth-shell-backdrop.test.ts b/website/src/__tests__/auth-shell-backdrop.test.ts index 09e68c9..e2f7c96 100644 --- a/website/src/__tests__/auth-shell-backdrop.test.ts +++ b/website/src/__tests__/auth-shell-backdrop.test.ts @@ -1,3 +1,5 @@ +import fs from 'node:fs' +import path from 'node:path' import { beforeEach, describe, expect, it } from 'vitest' import { AUTH_SHELL_BACKDROP_ID, @@ -6,6 +8,12 @@ import { syncAuthShellBackdrop, } from '../auth/auth-shell-backdrop' +function readAuthShellCssRule() { + const stylesPath = path.resolve(process.cwd(), 'src/styles/global.css') + const styles = fs.readFileSync(stylesPath, 'utf8') + return styles.match(/\.auth-shell\s*\{(?[^}]*)\}/m)?.groups?.rule || '' +} + describe('auth-shell-backdrop', () => { beforeEach(() => { document.body.innerHTML = '
' @@ -31,6 +39,14 @@ describe('auth-shell-backdrop', () => { expect(backdrop.style.zIndex).toBe('0') }) + it('keeps auth route content above the decorative backdrop layer', () => { + const authShellRule = readAuthShellCssRule() + + expect(authShellRule).toContain('position: relative') + expect(authShellRule).toContain('z-index: 1') + expect(authShellRule).toContain('isolation: isolate') + }) + it('removes the auth backdrop when navigation leaves auth routes', () => { syncAuthShellBackdrop('/login') expect(document.getElementById(AUTH_SHELL_BACKDROP_ID)).not.toBeNull() diff --git a/website/src/site-data.ts b/website/src/site-data.ts index c808908..1268ea7 100644 --- a/website/src/site-data.ts +++ b/website/src/site-data.ts @@ -704,6 +704,7 @@ export const deploymentReadinessTracks = [ title: '1. Identity and access posture', steps: [ 'Confirm shared browser auth is live before inviting operators into the protected release lane.', + 'Treat login/register visibility above decorative auth backdrops as a launch-readiness invariant, not a cosmetic preference.', 'Use the protected dashboard to resolve plan, entitlement, and desktop-link token posture.', 'Keep browser-to-desktop handoff explicit so the installed app never depends on password reuse.', ], @@ -1494,6 +1495,11 @@ export const releasePacketCards = [ ] as const export const changelogEntries = [ + { + date: 'July 2, 2026', + title: 'Browser login shell now has explicit overlay-safety proof before live deploy', + details: 'Login and registration keep the decorative auth backdrop, but the auth shell now owns a higher stacking layer so production users do not see an opaque overlay over the form. The responsive public-route suite now verifies the auth shell sits above the same-origin auth backdrop, and the main-branch CI path now follows validation with the same-origin live-deploy helper so pushed website fixes can reach hypertwist.app through the supported production chain.', + }, { date: 'June 30, 2026', title: 'Downloadable operator manual now mirrors the richer public manual routes', diff --git a/website/src/styles/global.css b/website/src/styles/global.css index d2365bd..ba392a8 100644 --- a/website/src/styles/global.css +++ b/website/src/styles/global.css @@ -719,6 +719,9 @@ img { .auth-shell { min-height: 100vh; + position: relative; + z-index: 1; + isolation: isolate; display: grid; place-items: center; padding: 1.25rem; diff --git a/website/tests/e2e/responsive-public-pages.spec.ts b/website/tests/e2e/responsive-public-pages.spec.ts index 4f56be5..c64fcf3 100644 --- a/website/tests/e2e/responsive-public-pages.spec.ts +++ b/website/tests/e2e/responsive-public-pages.spec.ts @@ -11,7 +11,7 @@ type PublicRouteExpectation = { const responsivePublicRoutes: readonly PublicRouteExpectation[] = [ { path: '/', - heading: 'HyperTwist turns cube practice into a real operator-grade training stack.', + heading: 'From first solves to 120-cell, HyperTwist keeps the real simulator in the downloadable.', cta: 'Download desktop app', }, { @@ -57,7 +57,7 @@ const responsivePublicRoutes: readonly PublicRouteExpectation[] = [ }, { path: '/changelog', - heading: 'Recent public-facing HyperTwist changes.', + heading: 'Release notes you can actually use for rollout.', }, { path: '/open-source-notices', @@ -95,9 +95,44 @@ async function assertResponsivePublicRoute(page: Page, route: PublicRouteExpecta } } + if (route.path === '/login' || route.path === '/register') { + await expectAuthShellAboveBackdrop(page) + } + await expectNoHorizontalOverflow(page) } +async function expectAuthShellAboveBackdrop(page: Page) { + await expect(page.locator('#hypertwist-auth-shell-backdrop')).toHaveCount(1) + + const stacking = await page.evaluate(() => { + const backdrop = document.getElementById('hypertwist-auth-shell-backdrop') + const shell = document.querySelector('.auth-shell') + if (!(backdrop instanceof HTMLElement) || !(shell instanceof HTMLElement)) { + return null + } + + const backdropStyle = window.getComputedStyle(backdrop) + const shellStyle = window.getComputedStyle(shell) + + return { + backdropPointerEvents: backdropStyle.pointerEvents, + backdropZIndex: backdropStyle.zIndex, + shellIsolation: shellStyle.isolation, + shellPosition: shellStyle.position, + shellZIndex: shellStyle.zIndex, + } + }) + + expect(stacking).toEqual({ + backdropPointerEvents: 'none', + backdropZIndex: '0', + shellIsolation: 'isolate', + shellPosition: 'relative', + shellZIndex: '1', + }) +} + test.describe('responsive public routes (mobile)', () => { test.use({ viewport: { width: 390, height: 844 } })