Web Frontend
The frontend is an Astro 6 static site that generates a comprehensive, self-contained view of the autonomous rewrite journey. It leverages Tailwind CSS v4 via PostCSS for a responsive, theme-aware UI and uses Astro’s content collections to render pages from pre-generated Markdown files. The architecture prioritizes performance and offline capability, producing a static dist/ directory that is served by the journey server, with no runtime data fetching required.
Build Configuration and Output
Section titled “Build Configuration and Output”The project is configured as a static site, ensuring all assets and pages are generated at build time. The build output is directed to ./dist by default, but this is configurable via the JOURNEY_OUTDIR environment variable to support isolated snapshots 1. Similarly, the URL base path can be adjusted via JOURNEY_BASE to allow the site to be served from sub-paths like /snapshots/<id>/ without breaking links.
Styling is handled by Tailwind CSS v4, which is integrated via PostCSS rather than the Vite plugin. This specific configuration avoids compatibility issues with Astro 6’s rolldown-based Vite implementation. The PostCSS configuration is defined in postcss.config.mjs and processes the global styles in src/styles/global.css 2.
Content Data Model
Section titled “Content Data Model”Journey data is stored as Markdown files within src/content/iters/. Each file represents a single iteration of the autonomous loop, named in the format epoch-{E}-iter-{NNNN}.md 3. These files are pre-generated by the journey_synth Python script and contain redacted data with pre-formatted dates.
The content schema is defined in src/content.config.ts using Zod. It captures key metrics for each iteration, including:
- Identity:
iter,epoch,slug,module. - Status:
outcome(e.g., ACCEPT, REVERT, WEDGED_NO_VERDICT). - Timing:
startedAtIso,endedAtIso, and their display equivalents. - Resource Usage:
qwenTokens,codexCalls,codexTokens,wedges,revisionCount, andfiles.
The build process automatically generates a page for every iteration found in the content directory, eliminating the need for hardcoded lists or runtime API calls.
Layout and Styling
Section titled “Layout and Styling”The site uses a global layout defined in src/layouts/BaseLayout.astro. This shell includes the navigation, main content slot, and footer 4. It implements a “no-flash” theme toggle by injecting an inline script that sets the dark class on the <html> element before the first paint, based on localStorage or the OS preference.
Styling is managed through CSS variables defined in src/styles/global.css. The theme system defines light and dark palettes using custom properties (e.g., --color-bg, --color-text) 5. Components use these variables via Tailwind’s arbitrary value syntax (e.g., bg-(--color-bg)) to ensure they automatically adapt to the active theme.
The global styles also include custom typography rules for markdown content within the .prose-md class, handling headings, code blocks, tables, and blockquotes with theme-aware colors. A subtle grain texture is applied via a CSS pseudo-element to add visual warmth.
Components
Section titled “Components”The UI is built from reusable Astro components:
- Nav: A responsive navigation bar that includes links to all site sections (Start, How it works, Journey, etc.) 6. It features a mobile menu drawer and a theme toggle button that syncs the UI icon and persists the preference to
localStorage. - OutcomeBadge: A small component that displays the status of an iteration (e.g., “accepted”, “reverted”, “wedged”) with color-coded styling based on the outcome 7.
- StatCard: A card component for displaying key metrics. It supports a value, label, optional hint, and a tone (default, good, warn, danger, brand) to color the value 8.
- Section: A wrapper component for content sections that can include a kicker, title, and optional narrow layout. It uses a
.revealclass for potential future animation hooks 9. - Quote: A styled blockquote component with support for attribution and tone-based left border colors (neutral, warn, danger) 10.
Testing and Ignored Files
Section titled “Testing and Ignored Files”End-to-end testing is handled by Playwright, configured in playwright.config.ts. Tests run in parallel and include both desktop (1440x900) and mobile (iPhone 17 Pro Max viewport) projects 11. The tests default to a local server but can be pointed at a remote instance via the BASE_URL environment variable.
The .gitignore file excludes build artifacts (dist/, .astro/), dependencies (node_modules/), and test reports (playwright-report/, test-results/) 12.
import { defineConfig } from 'astro/config';
// Static build: output goes to dist/ and is served by the journey server.
// Tailwind v4 is wired via PostCSS (postcss.config.mjs), NOT @tailwindcss/vite:
// under Astro 6's rolldown-based vite the vite plugin trips "Missing field
// `tsconfigPaths`". The PostCSS path keeps both latest Astro and Tailwind v4
// working (same approach as wiki-site).
//
// `base` is configurable so a snapshot can be rebuilt self-contained under its
// own prefix (JOURNEY_BASE=/snapshots/<id>/); defaults to root for the live site.
export default defineConfig({
output: 'static',
// outDir + base are env-configurable so a snapshot can be built self-contained
// under its own prefix (JOURNEY_BASE=/snapshots/<id>/ JOURNEY_OUTDIR=<dir>)
// without clobbering the live dist/.
outDir: process.env.JOURNEY_OUTDIR || './dist',
base: process.env.JOURNEY_BASE || '/',
site: process.env.JOURNEY_SITE || undefined,
trailingSlash: 'ignore',
build: {
inlineStylesheets: 'auto',
assets: 'assets',
},
});
// Tailwind v4 via PostCSS. Astro picks this up automatically and runs it over
// src/styles/global.css. Used instead of @tailwindcss/vite because that plugin
// is incompatible with Astro 6's rolldown-based vite ("Missing field
// `tsconfigPaths`"). Pinned versions live in package.json. Mirrors wiki-site.
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
// One markdown entry per iteration, written by journey_synth into
// src/content/iters/epoch-{E}-iter-{NNNN}.md (already redacted; dates
// pre-formatted in Python). The build generates a page for EVERY iter across
// EVERY epoch from this collection - no runtime fetch, no hardcoded list.
const iters = defineCollection({
loader: glob({ pattern: "*.md", base: "./src/content/iters" }),
schema: z.object({
iter: z.number(),
epoch: z.number(),
slug: z.string(),
module: z.string().nullable(),
outcome: z.string().nullable(),
startedAtIso: z.string().optional().default(""),
startedAtDisplay: z.string().optional().default(""),
endedAtIso: z.string().optional().default(""),
endedAtDisplay: z.string().optional().default(""),
wallDisplay: z.string().optional().default(""),
qwenTokens: z.number().optional().default(0),
codexCalls: z.number().optional().default(0),
codexTokens: z.number().optional().default(0),
wedges: z.number().optional().default(0),
revisionCount: z.number().optional().default(0),
files: z.number().optional().default(0),
privacy: z.string().optional().default("redacted"),
generatedIso: z.string().optional().default(""),
}),
});
export const collections = { iters };
---
import "../styles/global.css";
import Nav from "../components/Nav.astro";
interface Props {
title: string;
description?: string;
section?: string;
}
const { title, description = "An autonomous AI loop rewriting a Python codebase in Go - every step, every wedge, every receipt.", section = "" } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} - autoswe journey</title>
<meta name="description" content={description} />
<meta name="color-scheme" content="light dark" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2376b900'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='18' font-weight='700' fill='%230a0e14' text-anchor='middle'%3E%E2%96%B6%3C/text%3E%3C/svg%3E" />
<!-- No-flash theme: set the class BEFORE first paint from stored pref, else OS. -->
<script is:inline>
(function () {
try {
var t = localStorage.getItem("theme");
if (t !== "light" && t !== "dark") {
t = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
var r = document.documentElement;
r.classList.toggle("dark", t === "dark");
r.dataset.theme = t;
} catch (e) {}
})();
</script>
</head>
<body class="min-h-screen flex flex-col">
@import "tailwindcss";
/* Tailwind v4 - CSS-first config. No tailwind.config.js exists.
Tokens are defined LIGHT-first in @theme; the .dark / [data-theme="dark"]
block below redefines the same custom properties. Every component uses the
var-based utility syntax (text-(--color-x) / bg-(--color-x)), so the theme
switch cascades with zero component changes. */
@theme {
/* ---- light defaults ---- */
--color-bg: #ffffff;
--color-surface: #f6f8fa;
--color-surface-2: #eaeef2;
--color-border: #d0d7de;
--color-border-strong: #afb8c1;
--color-text: #1f2328;
--color-muted: #59636e;
--color-brand: #2f8a16; /* signature green (deeper for contrast on light) */
--color-brand-dim: #2f8a161f;
--color-accent: #0969da;
--color-warn: #9a6700;
--color-danger: #cf222e;
--color-success: #1a7f37;
--color-grain-dot: rgba(0,0,0,0.02);
/* ---- theme-invariant ---- */
--font-sans: "Inter", "InterVariable", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
--radius-card: 12px;
--shadow-card: 0 1px 2px rgba(31,35,40,0.06), 0 8px 24px rgba(31,35,40,0.08);
}
/* ---- dark override (the original palette) ---- */
:root.dark,
[data-theme="dark"] {
--color-bg: #0a0e14;
--color-surface: #11161d;
--color-surface-2: #161b22;
---
interface Props {
current?: string;
}
const { current = "" } = Astro.props;
// Base-aware links so a snapshot rebuilt under /snapshots/<id>/ stays self-contained.
const base = import.meta.env.BASE_URL; // always ends with "/"
const links = [
{ href: base, label: "Start", id: "start" },
{ href: `${base}how`, label: "How it works", id: "how" },
{ href: `${base}journey`, label: "Journey", id: "journey" },
{ href: `${base}parity`, label: "Parity", id: "parity" },
{ href: `${base}health`, label: "Health", id: "health" },
{ href: `${base}lessons`, label: "Lessons", id: "lessons" },
{ href: `${base}receipts`, label: "Receipts", id: "receipts" },
{ href: `${base}about`, label: "About", id: "about" },
];
---
<header class="sticky top-0 z-30 backdrop-blur-md bg-(--color-bg)/85 border-b border-(--color-border)">
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between gap-4">
<a href={base} class="flex items-center gap-2 group shrink-0">
<span class="w-7 h-7 grid place-items-center rounded-md bg-(--color-brand) text-(--color-bg) font-mono font-bold text-sm group-hover:scale-105 transition">▶</span>
<span class="font-semibold tracking-tight">autoswe</span>
<span class="text-(--color-muted) text-sm hidden md:inline">- journey</span>
</a>
<!-- Desktop nav -->
<nav class="hidden md:flex items-center gap-1 text-sm">
{links.map((l) => (
<a
href={l.href}
class:list={[
"px-3 py-1.5 rounded-md transition",
current === l.id
? "bg-(--color-surface-2) text-(--color-brand) font-medium"
: "text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface)",
]}
>{l.label}</a>
))}
---
interface Props {
outcome: string;
}
const { outcome } = Astro.props;
const map: Record<string, { label: string; class: string }> = {
ACCEPT: { label: "accepted", class: "text-(--color-success) bg-(--color-success)/10 border-(--color-success)/30" },
REVERT: { label: "reverted", class: "text-(--color-danger) bg-(--color-danger)/10 border-(--color-danger)/30" },
WEDGED_NO_VERDICT: { label: "wedged", class: "text-(--color-warn) bg-(--color-warn)/10 border-(--color-warn)/30" },
IN_PROGRESS: { label: "in progress", class: "text-(--color-accent) bg-(--color-accent)/10 border-(--color-accent)/30" },
UNKNOWN: { label: "unknown", class: "text-(--color-muted) bg-(--color-surface-2) border-(--color-border)" },
};
const m = map[outcome] ?? map.UNKNOWN;
---
<span class:list={["inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-mono border", m.class]}>
{m.label}
</span>
---
interface Props {
value: string | number;
label: string;
hint?: string;
tone?: "default" | "good" | "warn" | "danger" | "brand";
}
const { value, label, hint, tone = "default" } = Astro.props;
const toneClass = {
default: "text-(--color-text)",
good: "text-(--color-success)",
warn: "text-(--color-warn)",
danger: "text-(--color-danger)",
brand: "text-(--color-brand)",
}[tone];
---
<div class="rounded-(--radius-card) border border-(--color-border) bg-(--color-surface) p-4 shadow-(--shadow-card)">
<div class={`text-2xl font-bold tracking-tight font-mono ${toneClass}`}>{value}</div>
<div class="text-sm text-(--color-muted) mt-1">{label}</div>
{hint && <div class="text-xs text-(--color-muted)/80 mt-1.5 leading-snug">{hint}</div>}
</div>
---
interface Props {
kicker?: string;
title?: string;
narrow?: boolean;
reveal?: boolean;
id?: string;
}
const { kicker, title, narrow = false, reveal = true, id } = Astro.props;
const maxW = narrow ? "max-w-4xl" : "max-w-6xl";
---
<section id={id} class:list={[`${maxW} mx-auto px-5 sm:px-6 py-7 sm:py-9`, reveal && "reveal"]}>
{kicker && <div class="text-xs font-mono uppercase tracking-[0.2em] text-(--color-brand) mb-2">{kicker}</div>}
{title && <h2 class="text-2xl sm:text-3xl font-bold tracking-tight mb-4">{title}</h2>}
<slot />
</section>
---
interface Props {
attribution?: string;
tone?: "neutral" | "warn" | "danger";
}
const { attribution, tone = "neutral" } = Astro.props;
const borderTone = {
neutral: "border-(--color-brand)",
warn: "border-(--color-warn)",
danger: "border-(--color-danger)",
}[tone];
---
<figure class={`my-6 border-l-4 ${borderTone} pl-5 py-1`}>
<blockquote class="text-lg sm:text-xl text-(--color-text) leading-relaxed italic">
<slot />
</blockquote>
{attribution && <figcaption class="text-sm text-(--color-muted) mt-3 font-mono">- {attribution}</figcaption>}
</figure>
import { defineConfig, devices } from "@playwright/test";
// Defaults to a local server. Override with BASE_URL=http://<host>:8765
// to run against a remote instance.
const BASE_URL = process.env.BASE_URL ?? "http://localhost:8765";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
reporter: [["list"], ["html", { open: "never" }]],
use: {
baseURL: BASE_URL,
trace: "retain-on-failure",
screenshot: "only-on-failure",
// Point at an already-installed Chromium when the bundled revision isn't
// downloaded (offline / pinned environments): set PLAYWRIGHT_CHROME to the
// chrome binary. Falls back to Playwright's managed browser otherwise.
launchOptions: process.env.PLAYWRIGHT_CHROME
? { executablePath: process.env.PLAYWRIGHT_CHROME }
: undefined,
},
projects: [
{
name: "chromium-desktop",
use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } },
},
{
// Mobile responsive check on chromium (no extra browser install).
// Viewport matches iPhone 17 Pro Max - see user device-viewports memory.
name: "chromium-mobile",
use: {
...devices["Desktop Chrome"],
viewport: { width: 430, height: 932 },
isMobile: true,
hasTouch: true,
},
},
],
node_modules/
dist/
snapshots/
.astro/
.DS_Store
playwright-report/
test-results/