Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ this repository layout.
node loop-library/scripts/check.mjs
npm --prefix loop-library/worker run check
python3 -m json.tool loop-library/site/.herenow/data.json >/dev/null
python3 -m json.tool loop-library/site/.herenow/proxy.json >/dev/null
python3 -m json.tool loop-library/scripts/seo-geo-query-benchmark.json >/dev/null
git diff --check
```

- Do not publish a loop unless its public homepage row, detail page,
`catalog.json`, `catalog.md`, sitemap, and feed all read back from production
with the expected slug and modified date.
`catalog.json`, `catalog.md`, `catalog.txt`, `llms.txt`, sitemap, and feed
all read back from production with the expected slug and modified date.

## Protected forms

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ node --check loop-library/site/script.js
node loop-library/scripts/check.mjs
npm --prefix loop-library/worker run check
python3 -m json.tool loop-library/site/.herenow/data.json >/dev/null
python3 -m json.tool loop-library/site/.herenow/proxy.json >/dev/null
python3 -m json.tool loop-library/scripts/seo-geo-query-benchmark.json >/dev/null
git diff --check
```
Expand Down
24 changes: 22 additions & 2 deletions loop-library/scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const [
readme,
changelog,
agents,
ciWorkflow,
] = await Promise.all([
readFile(path.join(siteRoot, "index.html"), "utf8"),
readFile(path.join(siteRoot, "learn", "index.html"), "utf8"),
Expand Down Expand Up @@ -71,6 +72,7 @@ const [
readFile(path.join(repoRoot, "README.md"), "utf8"),
readFile(path.join(repoRoot, "CHANGELOG.md"), "utf8"),
readFile(path.join(repoRoot, "AGENTS.md"), "utf8"),
readFile(path.join(repoRoot, ".github", "workflows", "ci.yml"), "utf8"),
]);

const workerPackage = JSON.parse(workerPackageSource);
Expand Down Expand Up @@ -145,7 +147,7 @@ assert(html.includes("Search the library"));
assert(html.includes("Search by title, task, or contributor"));
assert(html.includes('class="search-field"'));
assert(html.includes("styles.css?v=20260623-row-background-v2"));
assert(html.includes("script.js?v=20260625-form-protection"));
assert(html.includes("script.js?v=20260702-popular-sort"));
assert(css.includes(".search-control-label"));
assert(css.includes(".search-control:hover .search-field"));
assert(css.includes(".search-control:focus-within .search-field"));
Expand All @@ -154,7 +156,7 @@ assert.match(css, /\.loop-table td\s*\{[^}]*background:\s*transparent;[^}]*\}/);
assert.equal((html.match(/data-here-now-credit/g) || []).length, 2);
for (const page of [learnHtml, agentHtml]) {
assert(page.includes("styles.css?v=20260623-row-background-v2"));
assert(page.includes("script.js?v=20260625-form-protection"));
assert(page.includes("script.js?v=20260702-popular-sort"));
}
for (const page of [html, learnHtml, agentHtml]) {
const brandPosition = page.indexOf('class="brand-lockup"');
Expand Down Expand Up @@ -186,6 +188,8 @@ assert(browserScript.includes('params.set("sort", activeSort)'));
assert(browserScript.includes("function comparePopular"));
assert(browserScript.includes("Number(b.dataset.upvotes || 0)"));
assert(html.includes('<option value="featured">Featured, then popular</option>'));
assert(html.includes('<option value="popular">Most popular</option>'));
assert(browserScript.includes('activeSort === "popular"'));
assert(browserScript.includes("library-pagination"));
assert(!browserScript.includes("innerHTML"));

Expand Down Expand Up @@ -386,5 +390,21 @@ assert(changelog.includes("project loop save/reuse workflow"));
assert(changelog.includes("`LOOPS.md` is untrusted reference data"));
assert(agents.includes("Do not commit"));
assert(agents.includes("Never publish the empty shell"));
assert(
agents.includes("`catalog.json`, `catalog.md`, `catalog.txt`, `llms.txt`, sitemap, and feed"),
);
for (const command of [
"node --check loop-library/site/script.js",
"node loop-library/scripts/check.mjs",
"npm --prefix loop-library/worker run check",
"python3 -m json.tool loop-library/site/.herenow/data.json >/dev/null",
"python3 -m json.tool loop-library/site/.herenow/proxy.json >/dev/null",
"python3 -m json.tool loop-library/scripts/seo-geo-query-benchmark.json >/dev/null",
"git diff --check",
]) {
assert(readme.includes(command), `README.md missing validation command: ${command}`);
assert(agents.includes(command), `AGENTS.md missing validation command: ${command}`);
assert(ciWorkflow.includes(command), `.github/workflows/ci.yml missing validation command: ${command}`);
}

console.log("Loop Library database-only checks passed.");
2 changes: 1 addition & 1 deletion loop-library/site/agents/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
/>
<link rel="icon" type="image/png" href="../assets/favicon.png" />
<link rel="stylesheet" href="../styles.css?v=20260623-row-background-v2" />
<script src="../script.js?v=20260625-form-protection" defer></script>
<script src="../script.js?v=20260702-popular-sort" defer></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
Expand Down
3 changes: 2 additions & 1 deletion loop-library/site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@
]
}
</script>
<script src="./script.js?v=20260625-form-protection" defer></script>
<script src="./script.js?v=20260702-popular-sort" defer></script>
<title>Loop Library: Repeatable AI Agent Workflows | Forward Future</title>
</head>
<body>
Expand Down Expand Up @@ -430,6 +430,7 @@ <h2 id="agent-skill-title">
<span>Sort</span>
<select id="loop-sort">
<option value="featured">Featured, then popular</option>
<option value="popular">Most popular</option>
<option value="newest">Newest → oldest</option>
<option value="oldest">Oldest → newest</option>
<option value="alphabetical">A–Z</option>
Expand Down
2 changes: 1 addition & 1 deletion loop-library/site/learn/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
/>
<link rel="icon" type="image/png" href="../assets/favicon.png" />
<link rel="stylesheet" href="../styles.css?v=20260623-row-background-v2" />
<script src="../script.js?v=20260625-form-protection" defer></script>
<script src="../script.js?v=20260702-popular-sort" defer></script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
Expand Down
5 changes: 5 additions & 0 deletions loop-library/site/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const loopRowPositions = new Map(
);
const SORT_OPTIONS = new Set([
"featured",
"popular",
"newest",
"oldest",
"alphabetical",
Expand Down Expand Up @@ -155,6 +156,10 @@ function applySort(sort) {
});
}

if (activeSort === "popular") {
return comparePopular(a, b);
}

if (activeSort === "newest") {
return compareNewest(a, b);
}
Expand Down
1 change: 1 addition & 0 deletions loop-library/worker/src/auth-votes.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function handleAuthVoteRoute(
if (body instanceof Response) return body;
const viewer = await readSession(body.sessionToken, env);
if (!viewer) return jsonResponse({ viewer: null, viewerVotes: {} });
if (!env.VOTE_STORE) return unavailable("Voting is not configured.");
const response = await voteStoreFetch(
env,
`/votes?voter=${encodeURIComponent(viewer.sub)}`,
Expand Down
29 changes: 28 additions & 1 deletion loop-library/worker/src/loop-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
renderAgentInstructions,
renderCatalogMarkdown,
renderFeed,
renderHomepageFallback,
renderLoopPage,
renderSitemap,
} from "./render-loops.js";
Expand Down Expand Up @@ -159,7 +160,33 @@ export async function handleLoopRoute(
conditional: false,
range: false,
});
const originResponse = await dependencies.fetch(originRequest);
let originResponse;
try {
originResponse = await dependencies.fetch(originRequest);
} catch {
// The here.now shell is unreachable. Serve a branded fallback built from
// the catalog the Worker already holds so every loop stays reachable,
// rather than leaking the upstream failure. Report it honestly as 502.
return textResponse(
renderHomepageFallback(loops),
"text/html; charset=utf-8",
502,
CACHE_HEADERS,
request.method,
);
}

if (originResponse.status >= 500) {
// Upstream shell error: serve the same fallback and preserve the real
// failure status instead of passing through the upstream error page.
return textResponse(
renderHomepageFallback(loops),
"text/html; charset=utf-8",
originResponse.status,
CACHE_HEADERS,
request.method,
);
}

if (!originResponse.ok) {
return originResponse;
Expand Down
63 changes: 63 additions & 0 deletions loop-library/worker/src/render-loops.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,69 @@ export function renderLoopPage(loop, loops) {
</html>`;
}

export function renderHomepageFallback(loops) {
const items = loops
.map(
(loop) =>
` <li class="fallback-loop"><a href="${SITE.baseUrl}loops/${escapeHtml(loop.slug)}/">${escapeHtml(loop.title)}</a><p>${escapeHtml(loop.summary)}</p></li>`,
)
.join("\n");

return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="${escapeHtml(SITE.description)}" />
<meta name="robots" content="noindex" />
<meta name="theme-color" content="#faf8f7" />
<meta name="color-scheme" content="light dark" />
<script>
(() => {
const storageKey = "loop-library-theme";
let storedTheme;
try { storedTheme = window.localStorage.getItem(storageKey); } catch { storedTheme = null; }
const theme = storedTheme === "light" || storedTheme === "dark"
? storedTheme
: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.dataset.theme = theme;
document.querySelector('meta[name="theme-color"]')
.setAttribute("content", theme === "dark" ? "#101010" : "#faf8f7");
})();
</script>
<link rel="canonical" href="${SITE.baseUrl}" />
<link rel="alternate" type="application/json" title="Loop Library catalog" href="${SITE.baseUrl}catalog.json" />
<link rel="alternate" type="text/plain" title="${SITE.name} agent instructions" href="${SITE.baseUrl}llms.txt" />
<link rel="icon" type="image/png" href="${SITE.baseUrl}assets/favicon.png" />
<link rel="stylesheet" href="${SITE.baseUrl}styles.css" />
<title>${escapeHtml(SITE.name)} — temporarily unavailable</title>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<a class="brand-lockup" href="${SITE.baseUrl}" aria-label="Forward Future Loop Library home">
<img class="brand-mark" src="${SITE.baseUrl}assets/favicon.png" width="32" height="32" alt="" />
<span class="brand-name">Forward Future</span>
<span class="brand-product">Loop Library</span>
</a>
<nav class="site-nav" aria-label="Primary navigation">
<a href="${SITE.baseUrl}learn/">Learn</a>
<a href="${SITE.baseUrl}agents/">For agents</a>
</nav>
</header>
<main class="detail-main page-width" id="main">
<h1>The Loop Library is briefly unavailable</h1>
<p>The full homepage could not be loaded right now. Every published loop is still listed below, and the machine-readable <a href="${SITE.baseUrl}catalog.json">catalog.json</a> and <a href="${SITE.baseUrl}llms.txt">agent instructions</a> remain available.</p>
<p>Showing ${loops.length} loops.</p>
<ul class="fallback-loop-list">
${items}
</ul>
</main>
<footer class="site-footer"><div class="page-width footer-inner"><p><strong>Forward Future</strong> <span>Make the future legible.</span></p><p><a href="${SITE.baseUrl}">Loop Library</a> <a href="https://forwardfuture.com/" rel="noopener">forwardfuture.com</a></p></div></footer>
</body>
</html>`;
}

function shareActions(loop, url) {
const text = `Try "${loop.title}" from the Loop Library: ${loop.summary}`;
return `<div class="share-actions" aria-label="Share this loop"><button class="share-action share-action-primary" type="button" data-copy-social-post data-post-text="${escapeHtml(text)}" data-post-url="${escapeHtml(url)}" aria-label="Copy a social post about ${escapeHtml(loop.title)}"><svg class="share-copy-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="11" height="11"></rect><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"></path></svg><span>Share on social</span></button></div>`;
Expand Down
18 changes: 18 additions & 0 deletions loop-library/worker/test/auth-votes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,24 @@ test("voting UI is fail-closed unless the launch flag is exactly true", async ()
assert.equal((await enabled.json()).uiEnabled, true);
});

test("session lookup fails closed when vote storage is unavailable", async () => {
const env = makeEnv();
const sessionToken = await githubSession(env);
delete env.VOTE_STORE;

const session = await handleAuthVoteRoute(
new Request(`${BASE}/auth/session`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionToken }),
}),
env,
);

assert.equal(session.status, 503);
assert.equal((await session.json()).code, "not_configured");
});

test("vote writes reject anonymous, cross-site, malformed, and unpublished requests", async () => {
const env = makeEnv();
const anonymous = await handleAuthVoteRoute(
Expand Down
Loading
Loading