mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-04 09:08:09 +02:00
refactor(design): board JS uses relative paths; drop __GSTACK_SERVER_URL injection
Board JS in design/src/compare.ts now calls ./api/feedback and ./api/progress (relative to location.pathname) and feature-detects server mode via location.protocol instead of the injected window.__GSTACK_SERVER_URL global. The injection in design/src/serve.ts is removed (dead code now that nothing reads it). Tests updated to match the new contract: serve.test.ts asserts the relative-path JS is present and the global is gone; feedback-roundtrip asserts location.protocol detects HTTP mode. Why: prep for the multi-board daemon (design/src/daemon.ts upcoming) where the same generated HTML is served at /boards/<id>/ instead of /. Relative paths resolve against location.pathname in both cases, so one HTML, two hosts. The injection was the only thing tying board JS to a specific serving path; removing it unblocks the daemon work without forking the generator. file:// fallback preserved via the location.protocol feature-detect — board opened directly as a file still falls through to the DOM-only success path. The 6 feedback-roundtrip browser tests continue to fail with session.clearLoadedHtml undefined; that failure pre-exists this branch (verified against HEAD with these edits stashed) and lives in browse/src/write-commands.ts, not in the design code path. Tracking separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-6
@@ -391,6 +391,17 @@ export function generateCompareHtml(images: string[]): string {
|
|||||||
<div id="feedback-result"></div>
|
<div id="feedback-result"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Feature-detect: are we being served over HTTP (by serve.ts or the
|
||||||
|
// daemon), or opened directly as a file:// URL? In file:// mode the
|
||||||
|
// board JS falls through to a DOM-only success path with no server
|
||||||
|
// round-trips. Using location.protocol instead of an injected global
|
||||||
|
// means the same generated HTML works at both / (legacy --no-daemon)
|
||||||
|
// and /boards/<id>/ (daemon) — relative URLs resolve against
|
||||||
|
// location.pathname in both cases.
|
||||||
|
function hasServer() {
|
||||||
|
return location.protocol === 'http:' || location.protocol === 'https:';
|
||||||
|
}
|
||||||
|
|
||||||
// View toggle
|
// View toggle
|
||||||
document.querySelectorAll('.view-toggle button').forEach(function(btn) {
|
document.querySelectorAll('.view-toggle button').forEach(function(btn) {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
@@ -465,8 +476,8 @@ export function generateCompareHtml(images: string[]): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function postFeedback(feedback) {
|
function postFeedback(feedback) {
|
||||||
if (!window.__GSTACK_SERVER_URL) return Promise.resolve(null);
|
if (!hasServer()) return Promise.resolve(null);
|
||||||
return fetch(window.__GSTACK_SERVER_URL + '/api/feedback', {
|
return fetch('./api/feedback', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(feedback),
|
body: JSON.stringify(feedback),
|
||||||
@@ -509,7 +520,7 @@ export function generateCompareHtml(images: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startProgressPolling() {
|
function startProgressPolling() {
|
||||||
if (!window.__GSTACK_SERVER_URL) return;
|
if (!hasServer()) return;
|
||||||
var pollCount = 0;
|
var pollCount = 0;
|
||||||
var maxPolls = 150; // 5 min at 2s intervals
|
var maxPolls = 150; // 5 min at 2s intervals
|
||||||
var pollInterval = setInterval(function() {
|
var pollInterval = setInterval(function() {
|
||||||
@@ -523,7 +534,7 @@ export function generateCompareHtml(images: string[]): string {
|
|||||||
'</div>';
|
'</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch(window.__GSTACK_SERVER_URL + '/api/progress')
|
fetch('./api/progress')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.status === 'serving') {
|
if (data.status === 'serving') {
|
||||||
@@ -563,7 +574,7 @@ export function generateCompareHtml(images: string[]): string {
|
|||||||
postFeedback(feedback).then(function(result) {
|
postFeedback(feedback).then(function(result) {
|
||||||
if (result && result.received) {
|
if (result && result.received) {
|
||||||
showRegeneratingState();
|
showRegeneratingState();
|
||||||
} else if (window.__GSTACK_SERVER_URL) {
|
} else if (hasServer()) {
|
||||||
showPostFailure(feedback);
|
showPostFailure(feedback);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -578,7 +589,7 @@ export function generateCompareHtml(images: string[]): string {
|
|||||||
postFeedback(feedback).then(function(result) {
|
postFeedback(feedback).then(function(result) {
|
||||||
if (result && result.received) {
|
if (result && result.received) {
|
||||||
showPostSubmitState();
|
showPostSubmitState();
|
||||||
} else if (window.__GSTACK_SERVER_URL) {
|
} else if (hasServer()) {
|
||||||
showPostFailure(feedback);
|
showPostFailure(feedback);
|
||||||
} else {
|
} else {
|
||||||
// DOM-only mode (legacy / test)
|
// DOM-only mode (legacy / test)
|
||||||
|
|||||||
+16
-13
@@ -1,12 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* HTTP server for the design comparison board feedback loop.
|
* HTTP server for the design comparison board feedback loop.
|
||||||
*
|
*
|
||||||
* Replaces the broken file:// + DOM polling approach. The server:
|
* Legacy single-process path: spawned by `$D compare --serve --no-daemon`.
|
||||||
* 1. Serves the comparison board HTML over HTTP
|
* The daemon (`design/src/daemon.ts`) handles default invocations and hosts
|
||||||
* 2. Injects __GSTACK_SERVER_URL so the board POSTs feedback here
|
* multiple boards under `/boards/<id>/`; this file stays as the escape hatch
|
||||||
* 3. Prints feedback JSON to stdout (agent reads it)
|
* for tests and debugging. Board JS uses relative URLs and a
|
||||||
* 4. Stays alive across regeneration rounds (stateful)
|
* location.protocol feature-detect, so the same generated HTML works at
|
||||||
* 5. Auto-opens in the user's default browser
|
* both `/` (here) and `/boards/<id>/` (daemon).
|
||||||
|
*
|
||||||
|
* The server:
|
||||||
|
* 1. Serves the comparison board HTML over HTTP at `/`
|
||||||
|
* 2. Prints feedback JSON to stdout (agent reads it)
|
||||||
|
* 3. Stays alive across regeneration rounds (stateful)
|
||||||
|
* 4. Auto-opens in the user's default browser
|
||||||
*
|
*
|
||||||
* State machine:
|
* State machine:
|
||||||
*
|
*
|
||||||
@@ -69,17 +75,14 @@ export async function serve(options: ServeOptions): Promise<void> {
|
|||||||
fetch(req) {
|
fetch(req) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
||||||
// Serve the comparison board HTML
|
// Serve the comparison board HTML. The board JS uses relative paths
|
||||||
|
// (./api/feedback, ./api/progress) and a location.protocol
|
||||||
|
// feature-detect, so no per-request injection is needed.
|
||||||
if (
|
if (
|
||||||
req.method === "GET" &&
|
req.method === "GET" &&
|
||||||
(url.pathname === "/" || url.pathname === "/index.html")
|
(url.pathname === "/" || url.pathname === "/index.html")
|
||||||
) {
|
) {
|
||||||
// Inject the server URL so the board can POST feedback
|
return new Response(htmlContent, {
|
||||||
const injected = htmlContent.replace(
|
|
||||||
"</head>",
|
|
||||||
`<script>window.__GSTACK_SERVER_URL = ${JSON.stringify(url.origin)};</script>\n</head>`,
|
|
||||||
);
|
|
||||||
return new Response(injected, {
|
|
||||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ beforeAll(async () => {
|
|||||||
serverState = 'serving';
|
serverState = 'serving';
|
||||||
|
|
||||||
// This server mirrors the real serve.ts behavior:
|
// This server mirrors the real serve.ts behavior:
|
||||||
// - Injects __GSTACK_SERVER_URL into the HTML
|
// - Serves board HTML at / (board JS uses relative URLs)
|
||||||
// - Handles POST /api/feedback with file writes
|
// - Handles POST /api/feedback with file writes
|
||||||
// - Handles GET /api/progress for regeneration polling
|
// - Handles GET /api/progress for regeneration polling
|
||||||
// - Handles POST /api/reload for board swapping
|
// - Handles POST /api/reload for board swapping
|
||||||
@@ -67,11 +67,7 @@ beforeAll(async () => {
|
|||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
||||||
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
||||||
const injected = currentHtml.replace(
|
return new Response(currentHtml, {
|
||||||
'</head>',
|
|
||||||
`<script>window.__GSTACK_SERVER_URL = '${url.origin}';</script>\n</head>`
|
|
||||||
);
|
|
||||||
return new Response(injected, {
|
|
||||||
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -140,14 +136,15 @@ describe('Submit: browser click → feedback.json on disk', () => {
|
|||||||
if (fs.existsSync(feedbackPath)) fs.unlinkSync(feedbackPath);
|
if (fs.existsSync(feedbackPath)) fs.unlinkSync(feedbackPath);
|
||||||
serverState = 'serving';
|
serverState = 'serving';
|
||||||
|
|
||||||
// Navigate to the board (served with __GSTACK_SERVER_URL injected)
|
// Navigate to the board (board JS uses relative URLs + location.protocol detect)
|
||||||
await handleWriteCommand('goto', [baseUrl], bm);
|
await handleWriteCommand('goto', [baseUrl], bm);
|
||||||
|
|
||||||
// Verify __GSTACK_SERVER_URL was injected
|
// Verify the board detects HTTP mode (so postFeedback will actually fetch
|
||||||
const hasServerUrl = await handleReadCommand('js', [
|
// instead of falling into the file:// DOM-only path)
|
||||||
'!!window.__GSTACK_SERVER_URL'
|
const httpDetected = await handleReadCommand('js', [
|
||||||
|
"location.protocol === 'http:' || location.protocol === 'https:'"
|
||||||
], bm);
|
], bm);
|
||||||
expect(hasServerUrl).toBe('true');
|
expect(httpDetected).toBe('true');
|
||||||
|
|
||||||
// User picks variant A, rates it 5 stars
|
// User picks variant A, rates it 5 stars
|
||||||
await handleReadCommand('js', [
|
await handleReadCommand('js', [
|
||||||
|
|||||||
@@ -65,11 +65,9 @@ describe('Serve HTTP endpoints', () => {
|
|||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
||||||
if (req.method === 'GET' && url.pathname === '/') {
|
if (req.method === 'GET' && url.pathname === '/') {
|
||||||
const injected = htmlContent.replace(
|
// Board JS uses relative URLs (./api/feedback, ./api/progress)
|
||||||
'</head>',
|
// and a location.protocol feature-detect; no injection needed.
|
||||||
`<script>window.__GSTACK_SERVER_URL = '${url.origin}';</script>\n</head>`
|
return new Response(htmlContent, {
|
||||||
);
|
|
||||||
return new Response(injected, {
|
|
||||||
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -118,12 +116,17 @@ describe('Serve HTTP endpoints', () => {
|
|||||||
server.stop();
|
server.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET / serves HTML with injected __GSTACK_SERVER_URL', async () => {
|
test('GET / serves HTML with relative-path board JS (no injection)', async () => {
|
||||||
const res = await fetch(baseUrl);
|
const res = await fetch(baseUrl);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
expect(html).toContain('__GSTACK_SERVER_URL');
|
// No more per-origin URL injection; board JS uses relative paths.
|
||||||
expect(html).toContain(baseUrl);
|
expect(html).not.toContain('__GSTACK_SERVER_URL');
|
||||||
|
expect(html).not.toContain(baseUrl);
|
||||||
|
// Board JS calls relative endpoints so the same HTML works at / and at
|
||||||
|
// /boards/<id>/ (daemon mode).
|
||||||
|
expect(html).toContain("fetch('./api/feedback'");
|
||||||
|
expect(html).toContain("fetch('./api/progress')");
|
||||||
expect(html).toContain('Design Exploration');
|
expect(html).toContain('Design Exploration');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user