From abc6eafd0c9ef4be6745d32f6a8d4fcf4db56960 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 01:52:10 +0800 Subject: [PATCH 1/8] fix(ui): install locked workspace dependencies in Docker --- Dockerfile | 3 +-- apps/studio/Dockerfile | 3 +-- apps/studio/src/styles/graphic.module.css | 2 +- apps/waterfall-wiki/Dockerfile | 3 +-- apps/waterfall-wiki/src/styles/landing.module.css | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index f081c17..f5911c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,8 @@ RUN corepack enable && corepack prepare pnpm@11.25.0 --activate COPY apps/waterfall-wiki/pnpm-lock.yaml ./ COPY pnpm-workspace.yaml /repo/pnpm-workspace.yaml COPY packages/brand/ /repo/packages/brand/ -RUN pnpm --ignore-workspace fetch --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8 COPY apps/waterfall-wiki/package.json ./ -RUN pnpm --filter @prompt/waterfall-wiki install --offline --frozen-lockfile +RUN pnpm --filter @prompt/waterfall-wiki install --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8 FROM dependencies AS build # .dockerignore excludes docs and operational notes without extra legacy COPY snapshots. diff --git a/apps/studio/Dockerfile b/apps/studio/Dockerfile index 8338c32..accf262 100644 --- a/apps/studio/Dockerfile +++ b/apps/studio/Dockerfile @@ -4,9 +4,8 @@ RUN corepack enable && corepack prepare pnpm@11.25.0 --activate COPY apps/studio/pnpm-lock.yaml ./ COPY pnpm-workspace.yaml /repo/pnpm-workspace.yaml COPY packages/brand/ /repo/packages/brand/ -RUN pnpm --ignore-workspace fetch --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8 COPY apps/studio/package.json ./ -RUN pnpm --filter @prompt/studio install --offline --frozen-lockfile +RUN pnpm --filter @prompt/studio install --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8 COPY apps/studio/ . # Compare the two public parameter validators inside the isolated CI build. COPY apps/waterfall-wiki/src/lib/layout-parameters.ts /repo/apps/waterfall-wiki/src/lib/layout-parameters.ts diff --git a/apps/studio/src/styles/graphic.module.css b/apps/studio/src/styles/graphic.module.css index 1fb7265..48db1b3 100644 --- a/apps/studio/src/styles/graphic.module.css +++ b/apps/studio/src/styles/graphic.module.css @@ -13,4 +13,4 @@ .layer[data-selected=true],.tabs button[aria-pressed=true]{background:var(--selection);color:var(--foreground)} .element[data-selected=true],.corner{outline-color:var(--selection-border);border-color:var(--selection-border)} /* The artwork board is deliberately independent of dark UI chrome. */ -.blankHint{color:#49372E} +.blankHint{color:var(--el-espresso)} diff --git a/apps/waterfall-wiki/Dockerfile b/apps/waterfall-wiki/Dockerfile index 5098e00..d1efa99 100644 --- a/apps/waterfall-wiki/Dockerfile +++ b/apps/waterfall-wiki/Dockerfile @@ -11,9 +11,8 @@ RUN corepack enable && corepack prepare pnpm@11.25.0 --activate COPY apps/waterfall-wiki/pnpm-lock.yaml ./ COPY pnpm-workspace.yaml /repo/pnpm-workspace.yaml COPY packages/brand/ /repo/packages/brand/ -RUN pnpm --ignore-workspace fetch --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8 COPY apps/waterfall-wiki/package.json ./ -RUN pnpm --filter @prompt/waterfall-wiki install --offline --frozen-lockfile +RUN pnpm --filter @prompt/waterfall-wiki install --frozen-lockfile --fetch-timeout=300000 --network-concurrency=8 FROM dependencies AS build # .dockerignore excludes docs and operational notes without extra legacy COPY snapshots. diff --git a/apps/waterfall-wiki/src/styles/landing.module.css b/apps/waterfall-wiki/src/styles/landing.module.css index 1538b30..19e9ea4 100644 --- a/apps/waterfall-wiki/src/styles/landing.module.css +++ b/apps/waterfall-wiki/src/styles/landing.module.css @@ -4,4 +4,4 @@ .dark:hover{background:var(--primary-hover)} .hero h1>span{color:var(--primary)} .end>span{display:inline-flex}.end>span :global(.endoleap-mark){width:64px;height:64px}.end>span :global(.endoleap-wordmark){display:none!important} -.demoWorld,.miniCanvas{background-color:#FAFAFA;background-image:radial-gradient(var(--grid) .7px,transparent .7px);color:#49372E} +.demoWorld,.miniCanvas{background-color:var(--el-canvas-white);background-image:radial-gradient(var(--grid) .7px,transparent .7px);color:var(--el-espresso)} -- 2.52.0 From a3935c5adc4f5f79bd417c9add09f601c8abf44b Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 02:00:00 +0800 Subject: [PATCH 2/8] fix(ui): preserve authored sheets and keyboard link contrast --- apps/waterfall-wiki/src/styles/guidance.module.css | 4 ++-- apps/waterfall-wiki/src/styles/learning-series.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/waterfall-wiki/src/styles/guidance.module.css b/apps/waterfall-wiki/src/styles/guidance.module.css index f2c3a01..b7a0765 100644 --- a/apps/waterfall-wiki/src/styles/guidance.module.css +++ b/apps/waterfall-wiki/src/styles/guidance.module.css @@ -71,8 +71,8 @@ top: 8px; left: 250px; transform: translateY(-200%); - background: var(--ink); - color: var(--foreground) !important; + background: var(--primary); + color: var(--primary-foreground) !important; padding: 10px 16px; border-radius: 8px; } diff --git a/apps/waterfall-wiki/src/styles/learning-series.css b/apps/waterfall-wiki/src/styles/learning-series.css index 2027664..97c1ef8 100644 --- a/apps/waterfall-wiki/src/styles/learning-series.css +++ b/apps/waterfall-wiki/src/styles/learning-series.css @@ -1,5 +1,5 @@ -.learning-reader{--learning-ink:var(--foreground);--learning-paper:var(--surface);background:var(--surface-muted);color:var(--learning-ink);padding:25px 28px;border-radius:20px}.learning-reader button{cursor:pointer}.learning-reader button:focus-visible,.learning-reader a:focus-visible{outline:3px solid var(--ring);outline-offset:4px}.learning-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:20px;max-width:780px;margin:0 auto 24px}.learning-reader-bar>div{display:grid;gap:9px}.learning-reader-badge{font-size:11px;letter-spacing:1px;color:var(--muted-foreground)}.learning-reader-bar strong{font-size:15px;font-weight:550}.learning-reader-bar>button{display:flex;align-items:center;gap:7px;font-size:12px;white-space:nowrap;background:var(--surface);padding:10px 13px;border-radius:25px;border:1px solid var(--border)}.learning-boards{max-width:720px;margin:auto;outline-offset:6px;container-type:inline-size}.learning-boards.is-continuous{display:grid;gap:24px}.learning-cover,.learning-sheet{width:100%;aspect-ratio:3/4;box-shadow:0 5px 22px #343b2810;overflow:hidden}.learning-cover img{display:block;width:100%;height:100%;object-fit:contain}.learning-sheet{position:relative;background:var(--learning-paper);padding:5.6cqw 6.8cqw;color:var(--learning-ink);font-size:1.8cqw;line-height:1.5}.learning-sheet-top{display:flex;justify-content:space-between;align-items:center;letter-spacing:.08em;font-size:1.25cqw;color:var(--muted-foreground);padding-bottom:2.6cqw;border-bottom:1px solid var(--border)}.learning-sheet-top>span:last-child{font-family:Arial,sans-serif;font-size:1.15cqw}.learning-sheet-footer{position:absolute;bottom:3.3cqw;left:6.8cqw;right:6.8cqw;border-top:1px solid var(--border);padding-top:1.9cqw;display:flex;justify-content:space-between;font-size:1.2cqw;color:var(--muted-foreground)}.learning-sheet-footer b{margin-left:2cqw;color:var(--foreground-strong);font-weight:600}.learning-intro{padding-top:4.5cqw;margin-bottom:3.6cqw}.learning-chapter{font-size:1.5cqw;letter-spacing:.08em;color:var(--muted-foreground)}.learning-intro h2,.learning-ending h2{font-size:5.8cqw;line-height:1.28;font-weight:800;letter-spacing:-.04em;margin:2.6cqw 0 2cqw}.learning-sheet mark{color:inherit;background:linear-gradient(transparent 45%,var(--learning-accent) 45%,var(--learning-accent) 93%,transparent 93%);padding:0 .15em}.learning-intro>p,.learning-ending>p{color:var(--muted-foreground);font-size:1.85cqw;margin:0}.learning-audience{display:inline-block;font-size:1.35cqw;letter-spacing:.02em;margin-top:2.2cqw;padding:.7cqw 1.4cqw;border:1px solid var(--border);border-radius:20px}.learning-index{display:grid;grid-template-columns:1fr;gap:0}.learning-index button{display:flex;text-align:left;align-items:center;gap:1.6cqw;padding:1.35cqw 0;border-bottom:1px solid var(--border);background:transparent}.learning-index button>span{font-family:Georgia,serif;font-size:3.2cqw;min-width:4cqw;color:var(--muted-foreground);font-style:italic}.learning-index button>div{flex:1;min-width:0}.learning-index strong{font-size:2cqw;font-weight:650;display:block}.learning-index small{font-size:1.2cqw;color:var(--muted-foreground);display:block;line-height:1.55}.learning-index svg{width:2.3cqw;flex-shrink:0;color:var(--muted-foreground)}.learning-index-note{display:flex;align-items:center;gap:1cqw;font-size:1.4cqw;color:var(--muted-foreground);margin-top:2.4cqw}.learning-index-note svg{width:2cqw}.learning-pair-title{padding:3.7cqw 0 2.7cqw}.learning-pair-title>span{font-size:1.3cqw;color:var(--muted-foreground)}.learning-pair-title h2{margin:.6cqw 0 0;font-size:3.5cqw;font-weight:750;letter-spacing:-.035em}.learning-skill{padding:2.6cqw 0;border-top:1px solid var(--border)}.learning-skill-heading{display:flex;gap:1.9cqw;align-items:center}.learning-number{font-family:Georgia,serif;font-size:5.3cqw;font-style:italic;line-height:1;color:var(--foreground-strong);min-width:6.7cqw;position:relative;z-index:0}.learning-number:after{content:"";position:absolute;left:0;right:-.2cqw;height:2cqw;bottom:.1cqw;z-index:-1;background:var(--learning-accent);transform:rotate(-5deg)}.learning-category{font-size:1.2cqw;color:var(--muted-foreground);letter-spacing:.08em}.learning-skill h3{font-size:2.7cqw;line-height:1.4;font-weight:750;letter-spacing:-.025em;margin:.3cqw 0 0}.learning-skill-name{margin:1.15cqw 0 1.55cqw 8.6cqw;display:flex;gap:.7cqw;align-items:center;width:fit-content;max-width:calc(100% - 8.6cqw);font-size:1.25cqw;font-family:Arial,sans-serif;color:var(--muted-foreground);overflow-wrap:anywhere}.learning-skill-name svg{width:1.4cqw;height:1.4cqw;flex-shrink:0}.learning-io{display:grid;gap:.75cqw;margin-bottom:1.7cqw}.learning-io p{display:flex;align-items:baseline;gap:1.2cqw;margin:0;font-size:1.7cqw;line-height:1.6}.learning-io b{font-size:1.35cqw;flex-shrink:0;font-weight:600;padding:.2cqw .8cqw;background:var(--selection);border-radius:4px;color:var(--foreground)}.learning-sample{border:1px solid var(--border);border-radius:.9cqw;padding:1.2cqw 1.9cqw;background:var(--selection);position:relative;margin-bottom:1.7cqw}.sample-label{font-size:1.05cqw;display:block;color:#939987;margin-bottom:.8cqw}.learning-sample>div{display:flex;align-items:center;gap:1.2cqw;font-size:1.5cqw;padding:.4cqw 0}.learning-sample>div>span{font-family:Arial,sans-serif;font-size:1cqw;color:var(--muted-foreground)}.learning-sample p{margin:0;flex:1}.learning-sample svg{color:var(--primary);width:1.6cqw;height:1.6cqw}.sample-1{background:repeating-linear-gradient(#fffefa 0,#fffefa 3.05cqw,#e8eade 3.05cqw,#e8eade 3.15cqw)}.sample-2>div:first-of-type p{background:linear-gradient(transparent 55%,var(--learning-accent) 55%);width:fit-content;flex:initial}.learning-prompt{display:flex;gap:1.2cqw;align-items:baseline}.learning-prompt>span{font-size:1.2cqw;white-space:nowrap;font-weight:600;color:var(--foreground)}.learning-prompt p{font-size:1.55cqw;line-height:1.8;margin:0}.learning-caveat{font-size:1.2cqw;line-height:1.6;color:var(--muted-foreground);margin:1.1cqw 0 0}.learning-ending{padding:5cqw 0 3cqw}.learning-ending h2{font-size:5.2cqw}.learning-workflow{list-style:none;padding:0;margin:1cqw 0 3.5cqw;display:grid;gap:1cqw}.learning-workflow li{position:relative;display:flex;align-items:center;gap:1.6cqw;padding:1.4cqw 1.8cqw;background:var(--selection);border-radius:.7cqw;font-size:1.9cqw}.learning-workflow b{font-family:Georgia,serif;font-size:2.6cqw;font-style:italic}.learning-workflow svg{margin-left:auto;width:2cqw;height:2cqw;color:var(--muted-foreground)}.learning-source-box{border:1px solid var(--border);padding:2.5cqw;border-radius:.8cqw;background:var(--selection)}.learning-source-box h3{display:flex;align-items:center;justify-content:space-between;font-size:2.3cqw;margin:0 0 1.6cqw;font-weight:700}.learning-source-box h3 svg{width:2.4cqw;height:2.4cqw}.learning-source-box>div{display:grid;grid-template-columns:1fr 1fr;column-gap:2.5cqw}.learning-source-box a{display:flex;align-items:center;gap:.9cqw;font-family:Arial,sans-serif;font-size:1.25cqw;padding:1.1cqw 0;border-bottom:1px solid var(--selection-border);overflow-wrap:anywhere;min-width:0}.learning-source-box a>span{font-size:1cqw;color:var(--muted-foreground)}.learning-source-box a svg{margin-left:auto;flex-shrink:0;width:1.3cqw;height:1.3cqw}.learning-source-box p{font-size:1.2cqw;line-height:1.8;color:var(--muted-foreground);margin:1.8cqw 0 0}.learning-series-reminder{display:grid;gap:.9cqw;margin-top:3cqw;font-size:1.35cqw}.learning-series-reminder>b{color:var(--muted-foreground);font-size:1.2cqw}.learning-series-reminder a{display:flex;align-items:center;gap:.8cqw}.learning-series-reminder svg{width:1.4cqw;height:1.4cqw}.learning-pagination{display:flex;align-items:center;justify-content:center;gap:30px;margin:22px 0 18px}.learning-pagination>button{width:39px;height:39px;display:grid;place-items:center;border-radius:50%;background:var(--surface);border:1px solid var(--border)}.learning-pagination>button:disabled{opacity:.35;cursor:default}.learning-pagination>span{min-width:105px;text-align:center;font-size:13px;font-variant-numeric:tabular-nums}.learning-pagination small{display:block;font-size:10px;margin-top:5px;color:var(--muted-foreground)}.learning-page-tabs{max-width:720px;margin:0 auto;display:flex;gap:7px;overflow-x:auto;padding:5px 2px 12px}.learning-page-tabs>button{flex:1;min-width:65px;display:grid;gap:5px;border:1px solid var(--border);border-radius:9px;background:var(--selection);padding:10px 8px;font-size:10px;color:var(--muted-foreground);white-space:nowrap}.learning-page-tabs>button>span{font-size:12px;font-family:Georgia,serif;font-style:italic}.learning-page-tabs>button.selected{background:var(--selection);color:var(--foreground);border-color:var(--selection-border)}.learning-related{max-width:720px;margin:22px auto 0;border-top:1px solid var(--border);padding-top:20px}.learning-related>span{font-size:11px;color:var(--muted-foreground)}.learning-related>div{display:grid;gap:8px;margin-top:12px}.learning-related a{display:flex;align-items:center;gap:10px;padding:10px 0;font-size:12px;color:var(--foreground)}.learning-related a>b{font-family:Georgia,serif;font-style:italic;font-size:19px;color:var(--muted-foreground)}.learning-related a>svg{margin-left:auto;flex-shrink:0}.learning-related a.selected{color:var(--primary);font-weight:600} +.learning-reader{padding:16px 10px;border-radius:14px}.learning-reader button{cursor:pointer}.learning-reader button:focus-visible,.learning-reader a:focus-visible{outline:3px solid var(--ring);outline-offset:4px}.learning-reader-bar{gap:10px;margin-bottom:16px;padding:0 3px}.learning-reader-bar>div{display:grid;gap:9px}.learning-reader-badge{font-size:10px}.learning-reader-bar strong{font-size:12px;line-height:1.6}.learning-reader-bar>button{font-size:11px;padding:9px 10px}.learning-boards{max-width:720px;margin:auto;outline-offset:6px;container-type:inline-size}.learning-boards.is-continuous{display:grid;gap:24px}.learning-cover,.learning-sheet{width:100%;aspect-ratio:3/4;box-shadow:0 5px 22px #343b2810;overflow:hidden}.learning-cover img{display:block;width:100%;height:100%;object-fit:contain}.learning-sheet{position:relative;background:var(--learning-paper);padding:5.6cqw 6.8cqw;color:var(--learning-ink);font-size:1.8cqw;line-height:1.5}.learning-sheet-top{display:flex;justify-content:space-between;align-items:center;letter-spacing:.08em;font-size:1.25cqw;color:#707665;padding-bottom:2.6cqw;border-bottom:1px solid #dcded1}.learning-sheet-top>span:last-child{font-family:Arial,sans-serif;font-size:1.15cqw}.learning-sheet-footer{position:absolute;bottom:3.3cqw;left:6.8cqw;right:6.8cqw;border-top:1px solid #dcded1;padding-top:1.9cqw;display:flex;justify-content:space-between;font-size:1.2cqw;color:#787c70}.learning-sheet-footer b{margin-left:2cqw;color:#34392b;font-weight:600}.learning-intro{padding-top:4.5cqw;margin-bottom:3.6cqw}.learning-chapter{font-size:1.5cqw;letter-spacing:.08em;color:#727963}.learning-intro h2,.learning-ending h2{font-size:5.8cqw;line-height:1.28;font-weight:800;letter-spacing:-.04em;margin:2.6cqw 0 2cqw}.learning-sheet mark{color:inherit;background:linear-gradient(transparent 45%,var(--learning-accent) 45%,var(--learning-accent) 93%,transparent 93%);padding:0 .15em}.learning-intro>p,.learning-ending>p{color:#676e5f;font-size:1.85cqw;margin:0}.learning-audience{display:inline-block;font-size:1.35cqw;letter-spacing:.02em;margin-top:2.2cqw;padding:.7cqw 1.4cqw;border:1px solid #d6d9cb;border-radius:20px}.learning-index{display:grid;grid-template-columns:1fr;gap:0}.learning-index button{display:flex;text-align:left;align-items:center;gap:1.6cqw;padding:1.35cqw 0;border-bottom:1px solid #e2e4d9;background:transparent}.learning-index button>span{font-family:Georgia,serif;font-size:3.2cqw;min-width:4cqw;color:#84916a;font-style:italic}.learning-index button>div{flex:1;min-width:0}.learning-index strong{font-size:2cqw;font-weight:650;display:block}.learning-index small{font-size:1.2cqw;color:#858c79;display:block;line-height:1.55}.learning-index svg{width:2.3cqw;flex-shrink:0;color:#7e886c}.learning-index-note{display:flex;align-items:center;gap:1cqw;font-size:1.4cqw;color:#727b63;margin-top:2.4cqw}.learning-index-note svg{width:2cqw}.learning-pair-title{padding:3.7cqw 0 2.7cqw}.learning-pair-title>span{font-size:1.3cqw;color:#7c846d}.learning-pair-title h2{margin:.6cqw 0 0;font-size:3.5cqw;font-weight:750;letter-spacing:-.035em}.learning-skill{padding:2.6cqw 0;border-top:1px solid #d9ddcf}.learning-skill-heading{display:flex;gap:1.9cqw;align-items:center}.learning-number{font-family:Georgia,serif;font-size:5.3cqw;font-style:italic;line-height:1;color:#343b2a;min-width:6.7cqw;position:relative;z-index:0}.learning-number:after{content:"";position:absolute;left:0;right:-.2cqw;height:2cqw;bottom:.1cqw;z-index:-1;background:var(--learning-accent);transform:rotate(-5deg)}.learning-category{font-size:1.2cqw;color:#778168;letter-spacing:.08em}.learning-skill h3{font-size:2.7cqw;line-height:1.4;font-weight:750;letter-spacing:-.025em;margin:.3cqw 0 0}.learning-skill-name{margin:1.15cqw 0 1.55cqw 8.6cqw;display:flex;gap:.7cqw;align-items:center;width:fit-content;max-width:calc(100% - 8.6cqw);font-size:1.25cqw;font-family:Arial,sans-serif;color:#717b62;overflow-wrap:anywhere}.learning-skill-name svg{width:1.4cqw;height:1.4cqw;flex-shrink:0}.learning-io{display:grid;gap:.75cqw;margin-bottom:1.7cqw}.learning-io p{display:flex;align-items:baseline;gap:1.2cqw;margin:0;font-size:1.7cqw;line-height:1.6}.learning-io b{font-size:1.35cqw;flex-shrink:0;font-weight:600;padding:.2cqw .8cqw;background:#e8eadd;border-radius:4px;color:#626c53}.learning-sample{border:1px solid #dce0d1;border-radius:.9cqw;padding:1.2cqw 1.9cqw;background:#fffefa;position:relative;margin-bottom:1.7cqw}.sample-label{font-size:1.05cqw;display:block;color:#939987;margin-bottom:.8cqw}.learning-sample>div{display:flex;align-items:center;gap:1.2cqw;font-size:1.5cqw;padding:.4cqw 0}.learning-sample>div>span{font-family:Arial,sans-serif;font-size:1cqw;color:#9ca48e}.learning-sample p{margin:0;flex:1}.learning-sample svg{color:#849c59;width:1.6cqw;height:1.6cqw}.sample-1{background:repeating-linear-gradient(#fffefa 0,#fffefa 3.05cqw,#e8eade 3.05cqw,#e8eade 3.15cqw)}.sample-2>div:first-of-type p{background:linear-gradient(transparent 55%,var(--learning-accent) 55%);width:fit-content;flex:initial}.learning-prompt{display:flex;gap:1.2cqw;align-items:baseline}.learning-prompt>span{font-size:1.2cqw;white-space:nowrap;font-weight:600;color:#63714f}.learning-prompt p{font-size:1.55cqw;line-height:1.8;margin:0}.learning-caveat{font-size:1.2cqw;line-height:1.6;color:#858c79;margin:1.1cqw 0 0}.learning-ending{padding:5cqw 0 3cqw}.learning-ending h2{font-size:5.2cqw}.learning-workflow{list-style:none;padding:0;margin:1cqw 0 3.5cqw;display:grid;gap:1cqw}.learning-workflow li{position:relative;display:flex;align-items:center;gap:1.6cqw;padding:1.4cqw 1.8cqw;background:#ecefdf;border-radius:.7cqw;font-size:1.9cqw}.learning-workflow b{font-family:Georgia,serif;font-size:2.6cqw;font-style:italic}.learning-workflow svg{margin-left:auto;width:2cqw;height:2cqw;color:#8b9579}.learning-source-box{border:1px solid #d5dac8;padding:2.5cqw;border-radius:.8cqw;background:#fffefa}.learning-source-box h3{display:flex;align-items:center;justify-content:space-between;font-size:2.3cqw;margin:0 0 1.6cqw;font-weight:700}.learning-source-box h3 svg{width:2.4cqw;height:2.4cqw}.learning-source-box>div{display:grid;grid-template-columns:1fr 1fr;column-gap:2.5cqw}.learning-source-box a{display:flex;align-items:center;gap:.9cqw;font-family:Arial,sans-serif;font-size:1.25cqw;padding:1.1cqw 0;border-bottom:1px solid #e8ebdf;overflow-wrap:anywhere;min-width:0}.learning-source-box a>span{font-size:1cqw;color:#98a18b}.learning-source-box a svg{margin-left:auto;flex-shrink:0;width:1.3cqw;height:1.3cqw}.learning-source-box p{font-size:1.2cqw;line-height:1.8;color:#87917a;margin:1.8cqw 0 0}.learning-series-reminder{display:grid;gap:.9cqw;margin-top:3cqw;font-size:1.35cqw}.learning-series-reminder>b{color:#7e886d;font-size:1.2cqw}.learning-series-reminder a{display:flex;align-items:center;gap:.8cqw}.learning-series-reminder svg{width:1.4cqw;height:1.4cqw}.learning-pagination{margin-top:17px}.learning-pagination>button{width:39px;height:39px;display:grid;place-items:center;border-radius:50%;background:var(--surface);border:1px solid var(--border)}.learning-pagination>button:disabled{opacity:.35;cursor:default}.learning-pagination>span{min-width:105px;text-align:center;font-size:13px;font-variant-numeric:tabular-nums}.learning-pagination small{display:block;font-size:10px;margin-top:5px;color:var(--muted-foreground)}.learning-page-tabs{max-width:720px;margin:0 auto;display:flex;gap:7px;overflow-x:auto;padding:5px 2px 12px}.learning-page-tabs>button{flex:1;min-width:65px;display:grid;gap:5px;border:1px solid var(--border);border-radius:9px;background:var(--selection);padding:10px 8px;font-size:10px;color:var(--muted-foreground);white-space:nowrap}.learning-page-tabs>button>span{font-size:12px;font-family:Georgia,serif;font-style:italic}.learning-page-tabs>button.selected{background:var(--selection);color:var(--foreground);border-color:var(--selection-border)}.learning-related{max-width:720px;margin:22px auto 0;border-top:1px solid var(--border);padding-top:20px}.learning-related>span{font-size:11px;color:var(--muted-foreground)}.learning-related>div{display:grid;gap:8px;margin-top:12px}.learning-related a{font-size:11px;line-height:1.7}.learning-related a>b{font-family:Georgia,serif;font-style:italic;font-size:19px;color:var(--muted-foreground)}.learning-related a>svg{margin-left:auto;flex-shrink:0}.learning-related a.selected{color:var(--primary);font-weight:600} @media(max-width:650px){.learning-reader{padding:16px 10px;border-radius:14px}.learning-reader-bar{gap:10px;margin-bottom:16px;padding:0 3px}.learning-reader-bar strong{font-size:12px;line-height:1.6}.learning-reader-bar>button{font-size:11px;padding:9px 10px}.learning-reader-badge{font-size:10px}.learning-related a{font-size:11px;line-height:1.7}.learning-pagination{margin-top:17px}.learning-sheet{font-synthesis:none}} -.detail-layout.coded-detail{grid-template-columns:minmax(0,1fr);max-width:1050px;border:0;box-shadow:none;overflow:visible}.coded-detail .detail-media{background:transparent}.coded-detail .detail-info{margin:28px auto 0;max-width:780px;width:100%;border-top:1px solid var(--border)}.coded-detail .detail-info>.eyebrow{display:none} +.detail-layout.coded-detail{grid-template-columns:minmax(0,1fr);max-width:1050px;border:0;box-shadow:none;overflow:visible}.coded-detail .detail-media{background:transparent}.coded-detail .detail-info{margin:28px auto 0;max-width:780px;width:100%;border-top:1px solid #e5e7df}.coded-detail .detail-info>.eyebrow{display:none} .learning-index button{padding:1cqw 0} .learning-standalone{width:1080px;height:1440px;margin:0;background:var(--selection);overflow:hidden}.learning-standalone .learning-boards{width:1080px;max-width:none;margin:0;container-type:inline-size}.learning-standalone .learning-cover,.learning-standalone .learning-sheet{box-shadow:none}.learning-html-cover svg{display:block;width:100%;height:100%}.snapshot-reader{padding:24px;background:var(--surface);border-radius:20px}.snapshot-reader>header{display:flex;justify-content:space-between;align-items:center;gap:15px;margin:0 auto 20px;max-width:800px}.snapshot-reader header p{font-size:12px;color:var(--muted-foreground)}.snapshot-reader header b{font-size:15px}.snapshot-reader header button{font-size:12px}.snapshot-stage{max-width:720px;margin:auto;aspect-ratio:3/4;background:var(--surface)}.snapshot-stage img{display:block;width:100%;height:auto}.snapshot-placeholder{height:100%;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:20px;color:var(--muted-foreground)}.snapshot-reader>nav{display:flex;align-items:center;justify-content:center;gap:20px;font-size:13px;margin:20px 0}.snapshot-reader nav button{border-radius:50%;padding:9px;background:var(--surface)}.snapshot-reader nav a{display:flex;align-items:center;gap:7px;color:var(--foreground)}.snapshot-reader button:disabled{opacity:.4;cursor:default}.snapshot-thumbs{display:flex;max-width:720px;gap:8px;margin:auto}.snapshot-thumbs>button{width:calc(100% / 8);aspect-ratio:3/4;background:var(--surface-muted);border:2px solid transparent;border-radius:8px;overflow:hidden}.snapshot-thumbs>button[aria-pressed=true]{border-color:var(--border)}.snapshot-thumbs img{width:100%;height:100%;object-fit:cover} -- 2.52.0 From ada5338af5f938bafff5169657ea020777a68c7f Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 02:04:51 +0800 Subject: [PATCH 3/8] fix(ui): resolve linked brand styles from the monorepo root --- apps/studio/next.config.ts | 3 ++- apps/waterfall-wiki/next.config.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/studio/next.config.ts b/apps/studio/next.config.ts index 1e9cf49..992e9a4 100644 --- a/apps/studio/next.config.ts +++ b/apps/studio/next.config.ts @@ -1,3 +1,4 @@ +import { resolve } from "node:path"; import type { NextConfig } from "next"; -const config: NextConfig = { basePath: "/studio", output: "standalone", outputFileTracingRoot: process.cwd(), poweredByHeader: false }; +const config: NextConfig = { turbopack: { root: resolve(process.cwd(), "../..") }, basePath: "/studio", output: "standalone", outputFileTracingRoot: process.cwd(), poweredByHeader: false }; export default config; diff --git a/apps/waterfall-wiki/next.config.ts b/apps/waterfall-wiki/next.config.ts index 5eb6e40..762ca8e 100644 --- a/apps/waterfall-wiki/next.config.ts +++ b/apps/waterfall-wiki/next.config.ts @@ -1,7 +1,9 @@ +import { resolve } from "node:path"; import type { NextConfig } from "next"; import managedMedia from "./src/content/managed-media.json"; const nextConfig: NextConfig = { + turbopack: { root: resolve(process.cwd(), "../..") }, serverExternalPackages: ["playwright-core"], async redirects(){return [ {source:'/studio/graphic',destination:'/graphic',permanent:true}, -- 2.52.0 From aa3c0cf11dd15e2ee91b7693c38028588dd1b7b4 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 02:12:34 +0800 Subject: [PATCH 4/8] fix(ui): align standalone tracing and workspace runtime paths --- Dockerfile | 3 ++- apps/studio/Dockerfile | 4 ++-- apps/studio/next.config.ts | 2 +- apps/studio/scripts/smoke.mjs | 2 +- apps/waterfall-wiki/Dockerfile | 3 ++- apps/waterfall-wiki/next.config.ts | 2 +- apps/waterfall-wiki/scripts/prepare-standalone.mjs | 6 +++--- apps/waterfall-wiki/scripts/smoke.mjs | 4 ++-- apps/waterfall-wiki/tests/integration.test.ts | 2 +- packages/brand/docs/integration.md | 2 +- 10 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5911c3..245d4e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,9 +26,10 @@ USER node CMD ["npm", "run", "test:integration"] FROM base AS runtime +WORKDIR /app/apps/waterfall-wiki ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 \ FFMPEG_PATH=/usr/bin/ffmpeg FFPROBE_PATH=/usr/bin/ffprobe -COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ ./ +COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ /app/ COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/dist/worker.cjs /repo/apps/waterfall-wiki/dist/bootstrap.cjs /repo/apps/waterfall-wiki/dist/publish-learning-series.cjs /repo/apps/waterfall-wiki/dist/auth-cli.cjs ./ COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/db/ ./db/ COPY --chown=node:node apps/waterfall-wiki/scripts/container-health.mjs ./ diff --git a/apps/studio/Dockerfile b/apps/studio/Dockerfile index accf262..37ada5b 100644 --- a/apps/studio/Dockerfile +++ b/apps/studio/Dockerfile @@ -13,9 +13,9 @@ COPY apps/waterfall-wiki/src/lib/series.ts /repo/apps/waterfall-wiki/src/lib/ser COPY apps/waterfall-wiki/src/lib/canvas-document.ts /repo/apps/waterfall-wiki/src/lib/canvas-document.ts RUN pnpm typecheck && pnpm test && pnpm build && pnpm test:smoke FROM node:24-bookworm-slim -WORKDIR /app +WORKDIR /app/apps/studio ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 -COPY --from=build --chown=node:node /repo/apps/studio/.next/standalone/ ./ +COPY --from=build --chown=node:node /repo/apps/studio/.next/standalone/ /app/ COPY --from=build --chown=node:node /repo/apps/studio/.next/static/ ./.next/static/ COPY --from=build --chown=node:node /repo/apps/studio/public/ ./public/ USER node diff --git a/apps/studio/next.config.ts b/apps/studio/next.config.ts index 992e9a4..67416ad 100644 --- a/apps/studio/next.config.ts +++ b/apps/studio/next.config.ts @@ -1,4 +1,4 @@ import { resolve } from "node:path"; import type { NextConfig } from "next"; -const config: NextConfig = { turbopack: { root: resolve(process.cwd(), "../..") }, basePath: "/studio", output: "standalone", outputFileTracingRoot: process.cwd(), poweredByHeader: false }; +const config: NextConfig = { turbopack: { root: resolve(process.cwd(), "../..") }, basePath: "/studio", output: "standalone", outputFileTracingRoot: resolve(process.cwd(), "../.."), poweredByHeader: false }; export default config; diff --git a/apps/studio/scripts/smoke.mjs b/apps/studio/scripts/smoke.mjs index aae6ca9..729976b 100644 --- a/apps/studio/scripts/smoke.mjs +++ b/apps/studio/scripts/smoke.mjs @@ -9,7 +9,7 @@ const assetId='00000000-0000-4000-8000-000000000005';doc.sources=[{assetId,autho let reads=0,mediaReads=0; const wiki=createHttpServer((req,res)=>{assert.equal(req.headers.cookie,'session=smoke-test');if(/^\/api\/media\/[0-9a-f-]{36}$/.test(req.url)){mediaReads++;res.setHeader('Content-Type','image/png');return res.end(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j3ioAAAAASUVORK5CYII=','base64'))}reads++;res.setHeader('Content-Type','application/json');if(req.url.endsWith(seriesId))return res.end(JSON.stringify({design:{id:seriesId,name:'完整三页作品',layoutId:'standard-01',revision:1,url:'/studio/template-layout?id='+seriesId,parameters:doc.pages[0].parameters,document:doc}}));const standard=req.url.endsWith(standardId);if(!standard&&!req.url.endsWith(designId)){res.writeHead(404);return res.end('{}')}res.end(JSON.stringify({design:{id:standard?standardId:designId,name:'已保存的实际页面',layoutId:standard?'standard-01':'ref-01',revision:3,url:'/studio/designs/'+designId,parameters:{added:[{id:'user-smoke',kind:'text',role:'Added text',box:[10,10,20,10],direction:'horizontal',text:'新增元素已恢复',reading:'custom'}],layers:{T01:{x:12,rotation:25,opacity:.7},'user-smoke':{z:12}},texts:{T01:{value:'确定的页面文字',color:'#123456'}},images:{I01:{color:'#abcdef'}},decorations:{D01:{hidden:true}},paper:'#eeeeee'}}}))}); wiki.listen(0,'127.0.0.1');await once(wiki,'listening'); -const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const port=socket.address().port;await new Promise(r=>socket.close(r));const child=spawn(process.execPath,[process.env.STUDIO_STANDALONE_SERVER || '.next/standalone/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1',STUDIO_WIKI_ORIGIN:'http://127.0.0.1:'+wiki.address().port},stdio:'inherit'}),base='http://127.0.0.1:'+port; +const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const port=socket.address().port;await new Promise(r=>socket.close(r));const child=spawn(process.execPath,[process.env.STUDIO_STANDALONE_SERVER || '.next/standalone/apps/studio/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1',STUDIO_WIKI_ORIGIN:'http://127.0.0.1:'+wiki.address().port},stdio:'inherit'}),base='http://127.0.0.1:'+port; try{let ready=false;for(let i=0;i<60;i++){try{if((await fetch(base+'/studio/api/health')).ok){ready=true;break}}catch{}await setTimeout(250)}assert.ok(ready);const entry=await fetch(base+'/studio',{redirect:'manual'});assert.equal(entry.status,307);assert.equal(entry.headers.get('location'),'/studio/graphic');const library=await fetch(base+'/studio/graphic').then(r=>r.text());assert.ok(library.includes('GRAPHIC'));assert.ok(!library.includes('ref-27')); const catalog=await fetch(base+'/studio/api/layouts').then(r=>r.json());assert.equal(catalog.layouts.length,28); for(const l of catalog.layouts){const page=await fetch(base+l.url);assert.equal(page.status,200,l.id);const html=await page.text();assert.ok(html.includes(l.name));assert.ok(html.includes('data-layout-id="'+l.id+'"'));assert.ok(!/r.json());assert.equal(schema.id,l.id);for(const s of schema.elements)assert.ok(html.includes('data-element="'+s.id+'"'));if(l.family==='standard'){assert.equal((await fetch(base+l.api+'?format=svg')).status,400);continue;}const original=await fetch(base+l.api+'?format=svg',{headers:{cookie:'session=smoke-test'}}).then(r=>r.text());assert.ok(original.includes('data-layout-id="'+l.id+'"'));const text=schema.elements.find(e=>e.kind==='text');const result=await fetch(base+l.api,{method:'POST',headers:{'Content-Type':'application/json',cookie:'session=smoke-test'},body:JSON.stringify({texts:{[text.id]:{value:'API render '}}})});assert.equal(result.status,200);assert.ok((await result.text()).includes('<'));} assert.equal(reads,0,'Default pages never load a saved design');assert.ok(mediaReads>0,'SVG exports load isolated media fixtures'); diff --git a/apps/waterfall-wiki/Dockerfile b/apps/waterfall-wiki/Dockerfile index d1efa99..0d77b6a 100644 --- a/apps/waterfall-wiki/Dockerfile +++ b/apps/waterfall-wiki/Dockerfile @@ -24,9 +24,10 @@ USER node CMD ["npm", "run", "test:integration"] FROM base AS runtime +WORKDIR /app/apps/waterfall-wiki ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 \ FFMPEG_PATH=/usr/bin/ffmpeg FFPROBE_PATH=/usr/bin/ffprobe -COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ ./ +COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/.next/standalone/ /app/ COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/dist/worker.cjs /repo/apps/waterfall-wiki/dist/bootstrap.cjs /repo/apps/waterfall-wiki/dist/publish-learning-series.cjs /repo/apps/waterfall-wiki/dist/auth-cli.cjs ./ COPY --from=build --chown=node:node /repo/apps/waterfall-wiki/db/ ./db/ COPY --chown=node:node apps/waterfall-wiki/scripts/container-health.mjs ./ diff --git a/apps/waterfall-wiki/next.config.ts b/apps/waterfall-wiki/next.config.ts index 762ca8e..21ddc67 100644 --- a/apps/waterfall-wiki/next.config.ts +++ b/apps/waterfall-wiki/next.config.ts @@ -22,7 +22,7 @@ const nextConfig: NextConfig = { {source:"/agent/index.md",destination:"/api/agent-index"}, {source:"/skills/:skill/:doc.md",destination:"/api/skill-docs/:skill/:doc"}, ]}, - output: "standalone", outputFileTracingRoot: process.cwd(), + output: "standalone", outputFileTracingRoot: resolve(process.cwd(), "../.."), outputFileTracingIncludes: {"/*":["./node_modules/playwright-core/**/*"]}, poweredByHeader: false, }; diff --git a/apps/waterfall-wiki/scripts/prepare-standalone.mjs b/apps/waterfall-wiki/scripts/prepare-standalone.mjs index edd5d63..376230b 100644 --- a/apps/waterfall-wiki/scripts/prepare-standalone.mjs +++ b/apps/waterfall-wiki/scripts/prepare-standalone.mjs @@ -1,4 +1,4 @@ import { cp,mkdir,access } from 'node:fs/promises'; -await mkdir('.next/standalone/.next',{recursive:true}); -await cp('.next/static','.next/standalone/.next/static',{recursive:true}); -try{await access('public');await cp('public','.next/standalone/public',{recursive:true});}catch(e){if(e.code!=='ENOENT')throw e;} +await mkdir('.next/standalone/apps/waterfall-wiki/.next',{recursive:true}); +await cp('.next/static','.next/standalone/apps/waterfall-wiki/.next/static',{recursive:true}); +try{await access('public');await cp('public','.next/standalone/apps/waterfall-wiki/public',{recursive:true});}catch(e){if(e.code!=='ENOENT')throw e;} diff --git a/apps/waterfall-wiki/scripts/smoke.mjs b/apps/waterfall-wiki/scripts/smoke.mjs index 0ec877f..b9a73dc 100644 --- a/apps/waterfall-wiki/scripts/smoke.mjs +++ b/apps/waterfall-wiki/scripts/smoke.mjs @@ -17,9 +17,9 @@ async function availablePort() { const port = process.env.SMOKE_PORT || await availablePort(); const release = "smoke-test"; -const previewFixture='.next/standalone/public/__smoke-preview.png'; +const previewFixture='.next/standalone/apps/waterfall-wiki/public/__smoke-preview.png'; await writeFile(previewFixture,await sharp({create:{width:48,height:48,channels:3,background:'#eee'}}).png().toBuffer()); -const child = spawn(process.execPath, [".next/standalone/server.js"], { +const child = spawn(process.execPath, [".next/standalone/apps/waterfall-wiki/server.js"], { // This loopback-only child checks retained resources without production credentials. // Production keeps failing closed; the bypass requires both explicit test settings. env: { ...process.env, AUTH_CONFIG_FILE: "", AUTH_DISABLED_FOR_TESTS: "true", PORT: port, HOSTNAME: "127.0.0.1", RELEASE_SHA: release }, diff --git a/apps/waterfall-wiki/tests/integration.test.ts b/apps/waterfall-wiki/tests/integration.test.ts index 297bed1..1f9953a 100644 --- a/apps/waterfall-wiki/tests/integration.test.ts +++ b/apps/waterfall-wiki/tests/integration.test.ts @@ -28,7 +28,7 @@ test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processi const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const address=socket.address();assert.ok(address&&typeof address!=='string');const port=address.port;await new Promise(r=>socket.close(()=>r())); const external=process.env.PROMPT_TEST_BASE_URL; const base=external||`http://127.0.0.1:${port}`; - const server=external?null:spawn(process.execPath,['.next/standalone/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1'},stdio:'inherit'}); + const server=external?null:spawn(process.execPath,['.next/standalone/apps/waterfall-wiki/server.js'],{env:{...process.env,PORT:String(port),HOSTNAME:'127.0.0.1'},stdio:'inherit'}); const worker=external?null:spawn(process.execPath,['dist/worker.cjs'],{env:process.env,stdio:'inherit'});const dir=await mkdtemp(join(tmpdir(),'prompt-test-')); const request=async(path:string,body?:unknown)=>{const res=await fetch(base+path,body?{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}:undefined);assert.ok(res.ok,`${path}: ${res.status} ${res.ok?'':await res.text()}`);return res.json();}; try{ diff --git a/packages/brand/docs/integration.md b/packages/brand/docs/integration.md index 0709b27..8b3d4f3 100644 --- a/packages/brand/docs/integration.md +++ b/packages/brand/docs/integration.md @@ -4,7 +4,7 @@ 在消费应用的 `package.json` 声明 `"@endoleap/brand": "workspace:*"`,按仓库独立锁文件策略更新该应用锁文件。包为私有内部资源,无需 npm 发布、React peer dependency 或运行时主题库。 -Studio / Wiki 已声明 `workspace:*` 依赖。Docker 构建在 `/repo/apps/` 保留与 `/repo/packages/brand` 的相对位置;运行镜像仍在 `/app` 执行 standalone 服务。保持消费应用与品牌包的相对位置,不恢复原有孤立安装流程。 +Studio / Wiki 已声明 `workspace:*` 依赖。Docker 构建在 `/repo/apps/` 保留与 `/repo/packages/brand` 的相对位置;Next.js 的模块解析与文件追踪都以 `/repo` 为根,standalone 保留 `apps/` 层次。运行镜像复制完整 standalone 到 `/app`,在 `/app/apps/` 执行服务;静态文件、图标、worker 与健康检查文件都放在对应应用目录。保持消费应用与品牌包的相对位置,不恢复原有孤立安装流程。 ## Tailwind CSS v4 -- 2.52.0 From 30ac83cd8545d3212cdf531acd0f63087d0eb759 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 02:14:39 +0800 Subject: [PATCH 5/8] fix(ui): serve approved brand icons on public login pages --- apps/waterfall-wiki/scripts/auth-cli.ts | 1 + apps/waterfall-wiki/src/proxy.ts | 2 ++ apps/waterfall-wiki/tests/auth.test.ts | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/waterfall-wiki/scripts/auth-cli.ts b/apps/waterfall-wiki/scripts/auth-cli.ts index 240402a..98c2159 100644 --- a/apps/waterfall-wiki/scripts/auth-cli.ts +++ b/apps/waterfall-wiki/scripts/auth-cli.ts @@ -48,6 +48,7 @@ async function main() { const request = (path:string,init:RequestInit={}) => fetch(base+path,{...init,redirect:'manual'}); const buildId = readFileSync('.next/BUILD_ID','utf8').trim(); for(const path of ['/','/workspace','/documents','/account'])assert.equal((await request(path)).status,200,'Public shell '+path); + for(const path of ['/favicon.ico','/brand/logo-light.svg','/brand/logo-dark.svg','/brand/icon-dark.svg','/apple-touch-icon.png']) assert.equal((await request(path)).status,200,'Public brand asset '+path); const studio=await request('/studio');assert.equal(studio.status,307);assert.equal(studio.headers.get('location'),'/workspace'); for(const [from,to] of [['/studio/graphic','/graphic'],['/studio/infinite_canvas','/infinite_canvas']]){const r=await request(from);assert.equal(r.status,308);assert.equal(r.headers.get('location'),to)} assert.equal((await request('/account')).status,200,'Public account page'); diff --git a/apps/waterfall-wiki/src/proxy.ts b/apps/waterfall-wiki/src/proxy.ts index 113de46..3c1ee00 100644 --- a/apps/waterfall-wiki/src/proxy.ts +++ b/apps/waterfall-wiki/src/proxy.ts @@ -2,6 +2,8 @@ import { NextResponse, type NextRequest } from 'next/server'; import { authConfig, SESSION_COOKIE, validSession } from './lib/auth'; export function proxy(request: NextRequest) { const path = request.nextUrl.pathname; + const publicBrandAssets = ['/favicon.ico','/apple-touch-icon.png','/brand/logo-light.svg','/brand/logo-dark.svg','/brand/icon-dark.svg','/studio/favicon.ico','/studio/apple-touch-icon.png','/studio/brand/logo-light.svg','/studio/brand/logo-dark.svg','/studio/brand/icon-dark.svg']; + if (['GET','HEAD'].includes(request.method) && publicBrandAssets.includes(path)) return NextResponse.next({headers:{'Referrer-Policy':'same-origin'}}); const responseHeaders = {'Cache-Control':'private, no-store', 'Referrer-Policy':'same-origin'}; // Public knowledge pages are read-only. Account handlers enforce their own origin and session checks. const publicRead = ['GET','HEAD'].includes(request.method) && (path === '/' || ['/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api'].includes(path) || path.startsWith('/studio/_next/') || path === '/protocol' || ['/llms.txt','/index.md','/agent','/agent/index.md','/api/agent-index','/api/guides'].includes(path) || path.startsWith('/guide/') || path.startsWith('/api/guides/')); diff --git a/apps/waterfall-wiki/tests/auth.test.ts b/apps/waterfall-wiki/tests/auth.test.ts index 53c8734..9c3890f 100644 --- a/apps/waterfall-wiki/tests/auth.test.ts +++ b/apps/waterfall-wiki/tests/auth.test.ts @@ -37,7 +37,7 @@ test('proxy exposes creative entry pages while protecting APIs, data and Studio for(const path of ['/library/import','/skills/example.png','/_next/data/build/library/import.json','/studio/layouts/ref-01']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,307,path); for(const path of ['/api/upload','/studio/api/export','/studio/api/layouts','/api/feed']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,401,path); for(const path of ['/','/_next/data/build/index.json','/llms.txt','/index.md','/agent','/agent/index.md','/api/agent-index','/api/guides','/write','/documents','/api/documents','/api/documents/preview','/guide/welcome','/protocol','/api/guides/welcome','/account','/api/account/session','/login','/brand-mark.svg','/_next/static/chunk.js','/api/health','/api/ready']) assert.equal(proxy(new NextRequest('http://wiki.test'+path)).status,200,path); - const creativeEntries=['/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api','/studio/_next/static/chunk.js']; + const creativeEntries=['/favicon.ico','/apple-touch-icon.png','/brand/logo-light.svg','/brand/logo-dark.svg','/brand/icon-dark.svg','/studio/favicon.ico','/studio/apple-touch-icon.png','/studio/brand/logo-light.svg','/studio/brand/logo-dark.svg','/studio/brand/icon-dark.svg','/workspace','/graphic','/infinite_canvas','/studio','/studio/graphic','/studio/infinite_canvas','/studio/api','/studio/_next/static/chunk.js']; for(const path of creativeEntries){ for(const method of ['GET','HEAD'])assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method})).status,200,`${method} ${path}`); assert.equal(proxy(new NextRequest('http://wiki.test'+path,{method:'POST'})).status,401,`POST ${path} must not bypass authentication`); -- 2.52.0 From 7dca4495bb90ecfb87ca128869245450919672a7 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 02:42:42 +0800 Subject: [PATCH 6/8] fix(ui): keep the Endoleap wordmark visible on mobile --- apps/waterfall-wiki/src/styles/landing.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/waterfall-wiki/src/styles/landing.module.css b/apps/waterfall-wiki/src/styles/landing.module.css index 19e9ea4..7f30a68 100644 --- a/apps/waterfall-wiki/src/styles/landing.module.css +++ b/apps/waterfall-wiki/src/styles/landing.module.css @@ -1,4 +1,4 @@ -.page{background:var(--selection);color:var(--foreground-strong);font-family:Arial,'PingFang SC',sans-serif}.page *{box-sizing:border-box}.page a{text-decoration:none;color:inherit}.nav{max-width:1320px;margin:auto;height:92px;display:flex;align-items:center;gap:28px;padding:0 38px;border-bottom:1px solid var(--border)}.logo{display:flex;align-items:center;gap:10px;font-size:24px}.logo span{font-size:11px;letter-spacing:2px;color:var(--muted-foreground)}.nav>div{display:flex;gap:30px;flex:1;margin-left:60px;font-size:13px}.nav>a:not(.logo){font-size:13px}.dark{display:inline-flex;align-items:center;justify-content:center;gap:16px;background:var(--primary);color:var(--primary-foreground)!important;border-radius:7px;padding:14px 20px;font-size:14px}.dark:hover{background:var(--primary-hover)}.hero{text-align:center;padding:78px 32px 40px;max-width:1320px;margin:auto}.eyebrow{font-size:10px;letter-spacing:2.5px;color:var(--muted-foreground);font-weight:600}.hero h1{font-size:clamp(36px,4.6vw,68px);line-height:1.24;letter-spacing:-3px;font-weight:600;margin:28px 0}.hero h1>span{color:var(--muted-foreground)}.intro{font-size:16px;line-height:1.95;color:var(--muted-foreground)}.actions{display:flex;align-items:center;justify-content:center;gap:28px;margin:28px 0 55px;font-size:13px}.product{text-align:left;border:1px solid var(--border);border-radius:13px;overflow:hidden;box-shadow:0 20px 65px var(--shadow);background:var(--surface)}.productBar{display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 23px;border-bottom:1px solid var(--border);font-size:10px;color:var(--muted-foreground)}.productBar b{letter-spacing:2px;color:var(--foreground-strong)}.demo{display:flex;min-height:385px}.demo>aside{width:175px;flex-shrink:0;padding:26px 20px;border-right:1px solid var(--border);font-size:11px;color:var(--muted-foreground)}.demo hr{border:0;border-top:1px solid var(--border);margin:20px 0}.demo aside p{padding:8px 0}.demo aside small{display:block;margin-top:90px;font-size:9px}.demoWorld{position:relative;flex:1;min-width:0;background:radial-gradient(var(--grid) .7px,transparent .7px) 0 0/18px 18px,var(--background)}.demoWorld>svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:var(--connector);stroke-width:1.5}.demoWorld>article{position:absolute;border:1px solid var(--selection-border);box-shadow:0 8px 20px var(--shadow);border-radius:7px;padding:23px;width:23%;background:var(--background)}.demoWorld small{font-size:8px;letter-spacing:1px;color:var(--muted-foreground)}.demoWorld h3{font-size:22px;font-weight:500;line-height:1.5;margin:18px 0 12px}.demoWorld p{font-size:10px;line-height:1.9;color:var(--muted-foreground)}.note{left:5%;top:55px;transform:rotate(-3deg)}.demoWorld>.art{left:36%;top:35px;width:25%;height:260px;overflow:hidden;background:var(--background);color:var(--primary)}.art h3{position:absolute;bottom:24px;z-index:1}.art>span{position:absolute;bottom:23px;font-size:6px;letter-spacing:2px}.orbit{width:165px;height:165px;border-radius:50%;background:linear-gradient(135deg,#f7f2d8,#7f9469);box-shadow:20px 10px 30px #52613e44;position:absolute;right:-28px;top:25px}.prompt{left:69%;top:100px;transform:rotate(3deg)}.prompt>svg{display:block;margin-bottom:16px;color:var(--muted-foreground)}.dock{position:absolute;bottom:15px;left:50%;transform:translateX(-50%);display:flex;gap:23px;padding:12px 18px;border:1px solid var(--border);background:var(--surface);border-radius:10px}.dock svg{width:17px;color:var(--foreground)}.caption{font-size:10px;color:var(--muted-foreground);margin-top:23px}.tools{max-width:1160px;padding:75px 32px;margin:auto}.page h2{font-size:clamp(30px,3.5vw,47px);line-height:1.3;letter-spacing:-1.8px;font-weight:500;margin:20px 0 35px}.toolGrid{display:grid;grid-template-columns:1fr 1fr;gap:24px}.toolGrid>a{border:1px solid var(--border);padding:30px;border-radius:12px;background:var(--surface);transition:transform .2s}.toolGrid>a:hover{transform:translateY(-4px)}.toolGrid>a>svg{width:21px;vertical-align:middle;color:var(--muted-foreground)}.toolGrid>a>span{font-size:9px;letter-spacing:2px;margin-left:13px;color:var(--muted-foreground)}.toolGrid h3{font-size:27px;letter-spacing:-1px;margin:25px 0 14px;font-weight:500}.toolGrid p{font-size:13px;line-height:1.9;color:var(--muted-foreground);max-width:370px}.toolGrid strong{display:flex;justify-content:space-between;font-weight:400;font-size:12px;margin-top:24px}.poster,.miniCanvas{height:225px;margin-top:30px;overflow:hidden;background:#ebeedf;position:relative;border-radius:6px}.poster>span{display:block;font-size:48px;letter-spacing:-3px;font-weight:700;line-height:1;padding:28px;color:#434e32}.poster i{font-family:Georgia,serif;font-weight:400}.poster>b{position:absolute;right:20px;top:20px;font-size:175px;line-height:1;color:#9cae79;transform:rotate(15deg)}.miniCanvas{display:flex;align-items:center;justify-content:center;gap:10px;background:radial-gradient(var(--grid) .7px,transparent .7px) 0 0/15px 15px,var(--background)}.miniCanvas b{font-size:11px;font-weight:400;padding:20px 13px;border:1px solid var(--selection-border);background:var(--background);border-radius:6px}.miniCanvas i{color:var(--muted-foreground)}.workflow{max-width:1160px;margin:20px auto;padding:65px 32px 90px;display:grid;grid-template-columns:1fr 1fr;gap:100px;border-top:1px solid var(--border)}.workflow p{font-size:13px;line-height:1.9;color:var(--muted-foreground)}.workflow a{display:inline-flex;gap:10px;font-size:12px;margin-top:20px}.workflow ol{list-style:none;padding:0}.workflow li{display:flex;gap:25px;border-bottom:1px solid var(--border);padding:22px 0}.workflow li>b{font-size:12px;color:var(--muted-foreground);padding-top:6px}.workflow h3{font-size:18px;font-weight:500;margin:0}.end{text-align:center;padding:75px 25px 95px;background:var(--selection);border-top:1px solid var(--border)}.end>span{font-size:55px;color:var(--muted-foreground)}.footer{max-width:1240px;display:flex;justify-content:space-between;gap:25px;align-items:center;margin:auto;padding:35px;font-size:11px;color:var(--muted-foreground)}.footer .logo{font-size:16px}.page a:focus-visible{outline:2px solid var(--ring);outline-offset:5px}@media(max-width:760px){.nav{padding:0 20px;height:72px;gap:14px}.nav>div{display:none}.nav .logo{flex:1}.nav .logo span{display:none}.nav .dark{padding:10px}.hero{padding:50px 18px 20px}.hero h1{letter-spacing:-1.5px}.intro{font-size:13px}.actions{gap:15px}.demo>aside{display:none}.demo{min-height:340px}.demoWorld>article{padding:12px}.demoWorld h3{font-size:16px}.demoWorld p{font-size:8px}.demoWorld small{font-size:6px}.demoWorld>.art{height:220px}.prompt{left:68%}.orbit{width:110px;height:110px}.productBar span:last-child{display:none}.toolGrid{grid-template-columns:1fr}.tools{padding:50px 20px}.workflow{grid-template-columns:1fr;gap:20px;padding:45px 20px}.footer{flex-wrap:wrap;padding:25px 20px}.footer>span{display:none}} +.page{background:var(--selection);color:var(--foreground-strong);font-family:Arial,'PingFang SC',sans-serif}.page *{box-sizing:border-box}.page a{text-decoration:none;color:inherit}.nav{max-width:1320px;margin:auto;height:92px;display:flex;align-items:center;gap:28px;padding:0 38px;border-bottom:1px solid var(--border)}.logo{display:flex;align-items:center;gap:10px;font-size:24px}.logo span{font-size:11px;letter-spacing:2px;color:var(--muted-foreground)}.nav>div{display:flex;gap:30px;flex:1;margin-left:60px;font-size:13px}.nav>a:not(.logo){font-size:13px}.dark{display:inline-flex;align-items:center;justify-content:center;gap:16px;background:var(--primary);color:var(--primary-foreground)!important;border-radius:7px;padding:14px 20px;font-size:14px}.dark:hover{background:var(--primary-hover)}.hero{text-align:center;padding:78px 32px 40px;max-width:1320px;margin:auto}.eyebrow{font-size:10px;letter-spacing:2.5px;color:var(--muted-foreground);font-weight:600}.hero h1{font-size:clamp(36px,4.6vw,68px);line-height:1.24;letter-spacing:-3px;font-weight:600;margin:28px 0}.hero h1>span{color:var(--muted-foreground)}.intro{font-size:16px;line-height:1.95;color:var(--muted-foreground)}.actions{display:flex;align-items:center;justify-content:center;gap:28px;margin:28px 0 55px;font-size:13px}.product{text-align:left;border:1px solid var(--border);border-radius:13px;overflow:hidden;box-shadow:0 20px 65px var(--shadow);background:var(--surface)}.productBar{display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 23px;border-bottom:1px solid var(--border);font-size:10px;color:var(--muted-foreground)}.productBar b{letter-spacing:2px;color:var(--foreground-strong)}.demo{display:flex;min-height:385px}.demo>aside{width:175px;flex-shrink:0;padding:26px 20px;border-right:1px solid var(--border);font-size:11px;color:var(--muted-foreground)}.demo hr{border:0;border-top:1px solid var(--border);margin:20px 0}.demo aside p{padding:8px 0}.demo aside small{display:block;margin-top:90px;font-size:9px}.demoWorld{position:relative;flex:1;min-width:0;background:radial-gradient(var(--grid) .7px,transparent .7px) 0 0/18px 18px,var(--background)}.demoWorld>svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:var(--connector);stroke-width:1.5}.demoWorld>article{position:absolute;border:1px solid var(--selection-border);box-shadow:0 8px 20px var(--shadow);border-radius:7px;padding:23px;width:23%;background:var(--background)}.demoWorld small{font-size:8px;letter-spacing:1px;color:var(--muted-foreground)}.demoWorld h3{font-size:22px;font-weight:500;line-height:1.5;margin:18px 0 12px}.demoWorld p{font-size:10px;line-height:1.9;color:var(--muted-foreground)}.note{left:5%;top:55px;transform:rotate(-3deg)}.demoWorld>.art{left:36%;top:35px;width:25%;height:260px;overflow:hidden;background:var(--background);color:var(--primary)}.art h3{position:absolute;bottom:24px;z-index:1}.art>span{position:absolute;bottom:23px;font-size:6px;letter-spacing:2px}.orbit{width:165px;height:165px;border-radius:50%;background:linear-gradient(135deg,#f7f2d8,#7f9469);box-shadow:20px 10px 30px #52613e44;position:absolute;right:-28px;top:25px}.prompt{left:69%;top:100px;transform:rotate(3deg)}.prompt>svg{display:block;margin-bottom:16px;color:var(--muted-foreground)}.dock{position:absolute;bottom:15px;left:50%;transform:translateX(-50%);display:flex;gap:23px;padding:12px 18px;border:1px solid var(--border);background:var(--surface);border-radius:10px}.dock svg{width:17px;color:var(--foreground)}.caption{font-size:10px;color:var(--muted-foreground);margin-top:23px}.tools{max-width:1160px;padding:75px 32px;margin:auto}.page h2{font-size:clamp(30px,3.5vw,47px);line-height:1.3;letter-spacing:-1.8px;font-weight:500;margin:20px 0 35px}.toolGrid{display:grid;grid-template-columns:1fr 1fr;gap:24px}.toolGrid>a{border:1px solid var(--border);padding:30px;border-radius:12px;background:var(--surface);transition:transform .2s}.toolGrid>a:hover{transform:translateY(-4px)}.toolGrid>a>svg{width:21px;vertical-align:middle;color:var(--muted-foreground)}.toolGrid>a>span{font-size:9px;letter-spacing:2px;margin-left:13px;color:var(--muted-foreground)}.toolGrid h3{font-size:27px;letter-spacing:-1px;margin:25px 0 14px;font-weight:500}.toolGrid p{font-size:13px;line-height:1.9;color:var(--muted-foreground);max-width:370px}.toolGrid strong{display:flex;justify-content:space-between;font-weight:400;font-size:12px;margin-top:24px}.poster,.miniCanvas{height:225px;margin-top:30px;overflow:hidden;background:#ebeedf;position:relative;border-radius:6px}.poster>span{display:block;font-size:48px;letter-spacing:-3px;font-weight:700;line-height:1;padding:28px;color:#434e32}.poster i{font-family:Georgia,serif;font-weight:400}.poster>b{position:absolute;right:20px;top:20px;font-size:175px;line-height:1;color:#9cae79;transform:rotate(15deg)}.miniCanvas{display:flex;align-items:center;justify-content:center;gap:10px;background:radial-gradient(var(--grid) .7px,transparent .7px) 0 0/15px 15px,var(--background)}.miniCanvas b{font-size:11px;font-weight:400;padding:20px 13px;border:1px solid var(--selection-border);background:var(--background);border-radius:6px}.miniCanvas i{color:var(--muted-foreground)}.workflow{max-width:1160px;margin:20px auto;padding:65px 32px 90px;display:grid;grid-template-columns:1fr 1fr;gap:100px;border-top:1px solid var(--border)}.workflow p{font-size:13px;line-height:1.9;color:var(--muted-foreground)}.workflow a{display:inline-flex;gap:10px;font-size:12px;margin-top:20px}.workflow ol{list-style:none;padding:0}.workflow li{display:flex;gap:25px;border-bottom:1px solid var(--border);padding:22px 0}.workflow li>b{font-size:12px;color:var(--muted-foreground);padding-top:6px}.workflow h3{font-size:18px;font-weight:500;margin:0}.end{text-align:center;padding:75px 25px 95px;background:var(--selection);border-top:1px solid var(--border)}.end>span{font-size:55px;color:var(--muted-foreground)}.footer{max-width:1240px;display:flex;justify-content:space-between;gap:25px;align-items:center;margin:auto;padding:35px;font-size:11px;color:var(--muted-foreground)}.footer .logo{font-size:16px}.page a:focus-visible{outline:2px solid var(--ring);outline-offset:5px}@media(max-width:760px){.nav{padding:0 20px;height:72px;gap:14px}.nav>div{display:none}.nav .logo{flex:1}.nav .dark{padding:10px}.hero{padding:50px 18px 20px}.hero h1{letter-spacing:-1.5px}.intro{font-size:13px}.actions{gap:15px}.demo>aside{display:none}.demo{min-height:340px}.demoWorld>article{padding:12px}.demoWorld h3{font-size:16px}.demoWorld p{font-size:8px}.demoWorld small{font-size:6px}.demoWorld>.art{height:220px}.prompt{left:68%}.orbit{width:110px;height:110px}.productBar span:last-child{display:none}.toolGrid{grid-template-columns:1fr}.tools{padding:50px 20px}.workflow{grid-template-columns:1fr;gap:20px;padding:45px 20px}.footer{flex-wrap:wrap;padding:25px 20px}.footer>span{display:none}} .nav .logo{flex-shrink:0}.nav>div{margin-left:24px}.nav .dark,.actions .dark,.end .dark{background:var(--primary);color:var(--primary-foreground)!important} .dark:hover{background:var(--primary-hover)} -- 2.52.0 From 628449735bce38009329cbd180a73fe27dad1ffc Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 02:43:55 +0800 Subject: [PATCH 7/8] fix(ui): fit the mark into compact library navigation --- apps/waterfall-wiki/src/styles/brand.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/waterfall-wiki/src/styles/brand.css b/apps/waterfall-wiki/src/styles/brand.css index e539c99..dd892b3 100644 --- a/apps/waterfall-wiki/src/styles/brand.css +++ b/apps/waterfall-wiki/src/styles/brand.css @@ -19,3 +19,5 @@ input,textarea,select{accent-color:var(--primary)} .primary-button,.auth-submit{background:var(--primary);color:var(--primary-foreground)} .primary-button:hover,.auth-submit:hover{background:var(--primary-hover)} +/* The legacy library rail collapses on tablets, while its mobile drawer expands. */ +@media(min-width:651px) and (max-width:950px){.sidebar .endoleap-wordmark{display:none!important}} -- 2.52.0 From 2a324c4d1925cfb06f778bcdad4a60b4f0a49894 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 03:09:33 +0800 Subject: [PATCH 8/8] fix(ui): wait for rendered browser state during CI navigation --- .../waterfall-wiki/tests/brand-ui-integration.ts | 2 ++ .../tests/canvas-ui-integration.ts | 6 +++--- .../waterfall-wiki/tests/guidance-integration.ts | 16 ++++++++-------- .../tests/guidance-workspace-ui.ts | 5 +++-- apps/waterfall-wiki/tests/integration.test.ts | 9 ++++++++- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apps/waterfall-wiki/tests/brand-ui-integration.ts b/apps/waterfall-wiki/tests/brand-ui-integration.ts index f1de465..d0c3bd1 100644 --- a/apps/waterfall-wiki/tests/brand-ui-integration.ts +++ b/apps/waterfall-wiki/tests/brand-ui-integration.ts @@ -17,10 +17,12 @@ export async function verifyBrandUI(base: string) { const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium', headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 960 } }); + page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000); const visit = async (path: string, theme: string) => { await page.goto(base + path, { waitUntil: 'commit' }); await page.locator('button[data-theme-ready="true"]').first().waitFor(); await page.waitForFunction(expected => document.documentElement.dataset.theme === expected, theme); + console.log('Brand UI route ready:', path, theme); }; const background = (selector: string) => page.locator(selector).first().evaluate(el => getComputedStyle(el).backgroundColor); await visit('/', 'home-light'); diff --git a/apps/waterfall-wiki/tests/canvas-ui-integration.ts b/apps/waterfall-wiki/tests/canvas-ui-integration.ts index 7f882cf..d868c70 100644 --- a/apps/waterfall-wiki/tests/canvas-ui-integration.ts +++ b/apps/waterfall-wiki/tests/canvas-ui-integration.ts @@ -21,8 +21,8 @@ export async function verifyCanvasAgentUI(base:string){ page.on('requestfailed',request=>{pending.delete(request);network.push(requestName(request)+': '+request.failure()?.errorText)}); page.on('response',response=>{const url=new URL(response.url());if(url.pathname.startsWith('/api/canvases'))canvasRequests.push(`${response.request().method()} ${url.pathname} ${response.status()}`)}); try{ - page.setDefaultTimeout(20000);page.on('pageerror',e=>errors.push(e.message)); - mark('open landing');await page.goto(base+'/',{waitUntil:'domcontentloaded'}); + page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000);page.on('pageerror',e=>errors.push(e.message)); + mark('open landing');await page.goto(base+'/',{waitUntil:'commit'}); await page.getByRole('heading',{name:/想法不该停在对话里/,level:1}).waitFor(); const accountSession=page.waitForResponse(response=>new URL(response.url()).pathname==='/api/account/session'&&response.request().method()==='GET'); const [,sessionResponse]=await Promise.all([page.getByRole('link',{name:'开始创作',exact:true}).click(),accountSession]); @@ -97,7 +97,7 @@ export async function verifyCanvasAgentUI(base:string){ assert.equal((await fetch(base+root,{headers:{authorization:second.authorization}})).status,404); await page.setViewportSize({width:390,height:844}); const bounds=await panel.boundingBox();assert.ok(bounds&&bounds.x>=0&&bounds.x+bounds.width<=390,'Agent panel stays within the mobile viewport'); - await page.reload({waitUntil:'domcontentloaded'}); + await page.reload({waitUntil:'commit'}); await page.getByRole('link',{name:email,exact:true,includeHidden:true}).waitFor({state:'attached'}); await page.getByRole('button',{name:'Agent',exact:true}).click(); const reopened=page.getByRole('region',{name:'Agent 连接',exact:true}); diff --git a/apps/waterfall-wiki/tests/guidance-integration.ts b/apps/waterfall-wiki/tests/guidance-integration.ts index 395210c..7a9cc76 100644 --- a/apps/waterfall-wiki/tests/guidance-integration.ts +++ b/apps/waterfall-wiki/tests/guidance-integration.ts @@ -35,7 +35,7 @@ export async function verifyGuidancePublishing(base:string){ // The document discovery list hydrates from the real API; the homepage is the product landing page. const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); try{ - const page=await browser.newPage(); + const page=await browser.newPage();page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000); const navigationDiagnostics:string[]=[],inflight=new Set(); const record=(value:string)=>{navigationDiagnostics.push(value.slice(0,1800));if(navigationDiagnostics.length>24)navigationDiagnostics.shift();}; page.on('pageerror',error=>record('pageerror: '+error.message)); @@ -46,30 +46,30 @@ export async function verifyGuidancePublishing(base:string){ page.on('response',response=>{const path=new URL(response.url()).pathname;if(path.startsWith('/api/')||path.includes('/_next/data/'))void response.text().then(body=>record(`data ${response.status()} ${path}: ${body.slice(0,1400)}`)).catch(()=>{});else if(response.status()>=400||response.request().resourceType()==='script')record(`resource ${response.status()} ${path}`);}); try{ const link=`a[href="/documents/${httpDoc.id}?revision=1"]`; - const navigation=await page.goto(base+'/documents',{waitUntil:'domcontentloaded'});record('directory HTTP '+navigation?.status()); + const navigation=await page.goto(base+'/documents',{waitUntil:'commit'});record('directory HTTP '+navigation?.status()); await page.locator(link).waitFor({state:'visible'});assert.equal(await page.locator(link).getByRole('heading').textContent(),httpDoc.title); // Published matches must not inherit the examples' empty state; submitted filters survive reload. const search=page.getByRole('textbox',{name:'搜索文档'}); await search.fill(httpDoc.title);await search.press('Enter'); - await page.waitForURL(url=>url.searchParams.get('q')===httpDoc.title); + await page.waitForURL(url=>url.searchParams.get('q')===httpDoc.title,{waitUntil:'commit'}); await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); assert.equal(await page.getByText('没有匹配文档',{exact:false}).count(),0); - await page.reload({waitUntil:'domcontentloaded'}); + await page.reload({waitUntil:'commit'}); await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); assert.equal(await search.inputValue(),httpDoc.title); await page.getByRole('tab',{name:/事实依据/}).click(); - await page.waitForURL(url=>url.searchParams.get('kind')==='evidence'); + await page.waitForURL(url=>url.searchParams.get('kind')==='evidence',{waitUntil:'commit'}); await page.getByRole('heading',{name:'没有匹配文档',exact:true}).waitFor(); await page.getByRole('button',{name:'清除所有筛选'}).click(); - await page.waitForURL(url=>url.search===''); + await page.waitForURL(url=>url.search==='',{waitUntil:'commit'}); await page.locator(link).waitFor({state:'visible'}); assert.equal(await search.inputValue(),''); await page.getByRole('navigation',{name:'主导航',exact:true}).getByRole('link',{name:'社区文档',exact:true}).click();await page.locator(link).waitFor({state:'visible'}); await page.locator(link).click(); - await page.waitForURL(url=>url.pathname==='/documents/'+httpDoc.id&&url.searchParams.get('revision')==='1'); + await page.waitForURL(url=>url.pathname==='/documents/'+httpDoc.id&&url.searchParams.get('revision')==='1',{waitUntil:'commit'}); await page.getByRole('heading',{name:httpDoc.title,exact:true,level:1}).waitFor(); assert.equal(await page.locator('h1').textContent(),httpDoc.title); - await page.goto(base+'/write',{waitUntil:'domcontentloaded'});assert.equal(await page.locator('h1').textContent(),'发布独立文档'); + await page.goto(base+'/write',{waitUntil:'commit'});await page.getByRole('heading',{name:'发布独立文档',exact:true,level:1}).waitFor();assert.equal(await page.locator('h1').textContent(),'发布独立文档'); }catch(error){console.error('Document navigation diagnostics',JSON.stringify({url:page.url(),headings:await page.locator('h1').allTextContents(),body:(await page.locator('body').innerText()).slice(0,2200),inflight:[...inflight].map(url=>new URL(url).pathname),events:navigationDiagnostics}));throw error;} await page.close(); await verifyGuidanceWorkspaceUI(base,browser); diff --git a/apps/waterfall-wiki/tests/guidance-workspace-ui.ts b/apps/waterfall-wiki/tests/guidance-workspace-ui.ts index b76792b..2ad56d8 100644 --- a/apps/waterfall-wiki/tests/guidance-workspace-ui.ts +++ b/apps/waterfall-wiki/tests/guidance-workspace-ui.ts @@ -89,7 +89,8 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser) route.fulfill({ json: { document: authored } }), ); const page = await context.newPage(); - page.setDefaultTimeout(10000); + page.setDefaultTimeout(60000); + page.setDefaultNavigationTimeout(60000); page.on("pageerror", (error) => errors.push(error.message)); page.on("crash", () => errors.push("Browser renderer crashed")); const pending = new Set(), network: string[] = []; @@ -99,7 +100,7 @@ export async function verifyGuidanceWorkspaceUI(base: string, browser: Browser) async function navigate(url: string, reload = false) { const previousSessionReplies = sessionReplies; try { - const navigation = await (reload ? page.reload({waitUntil:"domcontentloaded"}) : page.goto(url,{waitUntil:"domcontentloaded"})); + const navigation = await (reload ? page.reload({waitUntil:"commit"}) : page.goto(url,{waitUntil:"commit"})); assert.ok(navigation?.ok(), "Navigation returns a successful document"); await page.getByText(signedIn ? "editor@example.com" : "登录你的账户",{exact:true}).first().waitFor({state:"attached"}); assert.ok(sessionReplies > previousSessionReplies, "The new document requests its account fixture and renders the resulting account state"); diff --git a/apps/waterfall-wiki/tests/integration.test.ts b/apps/waterfall-wiki/tests/integration.test.ts index 1f9953a..478cd5c 100644 --- a/apps/waterfall-wiki/tests/integration.test.ts +++ b/apps/waterfall-wiki/tests/integration.test.ts @@ -23,6 +23,13 @@ import {verifyFlatNoteMigration} from './flat-note-migration'; import {staticEntries} from '../src/lib/explore'; import {publishLearningSeries} from '../src/lib/publish-learning-series'; +// Browser acceptance gets its own deadline; it must not consume the existing business-flow budget. +test('Endoleap icons, persisted themes and artwork isolation in the real browser', {timeout:300000,skip:!process.env.PROMPT_TEST_BASE_URL}, async()=>{ + assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1'); + const base=process.env.PROMPT_TEST_BASE_URL;assert.ok(base); + await verifyBrandUI(base); +}); + test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processing', {timeout:600000}, async()=>{ assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1','Run only against an explicitly isolated test database'); const socket=createServer();socket.listen(0,'127.0.0.1');await once(socket,'listening');const address=socket.address();assert.ok(address&&typeof address!=='string');const port=address.port;await new Promise(r=>socket.close(()=>r())); @@ -34,7 +41,7 @@ test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processi try{ let ready=false;for(let i=0;i<60;i++){try{if((await fetch(base+'/api/health')).ok){ready=true;break;}}catch{}await setTimeout(250);}assert.ok(ready); for(const route of ['/','/images','/videos','/audio','/articles','/motion','/library/import','/tools/doodle','/tools/skills','/tools/skills/street-people','/tools/skills/shadow-puppet'])assert.equal((await fetch(base+route)).status,200,route); - await verifyBrandUI(base); + if(!external)await verifyBrandUI(base); await verifyEmailAccounts(); await verifyCanvasWorkspace(base); if(process.env.VERIFY_CANVAS_UI==='1')await verifyCanvasAgentUI(base); -- 2.52.0