Merge commit '7667613116d143416649856ebf404aacff430c49'

This commit is contained in:
Alexa Amundson
2025-11-28 23:01:29 -06:00
20 changed files with 780 additions and 0 deletions

34
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,34 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:astro/recommended',
'plugin:svelte/recommended'
],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module'
},
overrides: [
{
files: ['*.astro'],
parser: 'astro-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
},
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

View File

@@ -9,6 +9,8 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
branches: [main, scaffold/archive-gen-0]
pull_request:
jobs: jobs:
build: build:
@@ -38,3 +40,33 @@ jobs:
- name: Test - name: Test
run: npm test --if-present run: npm test --if-present
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
- name: Build
run: pnpm build
- name: Upload dist
if: success()
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist

7
.gitignore vendored
View File

@@ -77,3 +77,10 @@ secrets/
DRAFT_* DRAFT_*
WIP_* WIP_*
TODO_* TODO_*
node_modules
dist
.cache
output
.DS_Store
.env
archive.env

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"plugins": ["prettier-plugin-astro", "prettier-plugin-svelte"],
"singleQuote": true,
"trailingComma": "none"
}

View File

@@ -168,3 +168,33 @@ We welcome contributions! Please see our [Contributing Guidelines](meta/CONTRIBU
--- ---
**Remember:** This archive is designed to last. Every addition should make the history clearer, not noisier. Quality over quantity. Clarity over completeness. 🕯️💚 **Remember:** This archive is designed to last. Every addition should make the history clearer, not noisier. Quality over quantity. Clarity over completeness. 🕯️💚
# Blackroad OS · System Archive (Gen-0)
Append-only artifact vault for deploy logs, beacon pings, and daily snapshots. Built with Astro 4 + Svelte islands and JSON-backed history.
## Quickstart
```bash
pnpm i
pnpm dev # http://localhost:4321
pnpm build # static output to /dist
```
Add records locally (appends into `/data/**`):
```bash
pnpm add:deploy --msg "Core 0.0.1 released"
pnpm add:beacon --env core --status ok
```
## Architecture
- **Data**: Plain JSON arrays committed under `/data`. They are append-only; scripts never mutate existing entries.
- **Viewer**: Astro static pages using Svelte islands for interactive tables and timelines.
- **Signature**: Build step writes `/public/sig.beacon.json` with the current timestamp and agent tag.
- **CI**: Lint → test → build → deploy (gh-pages).
- **Hooks**: `// TODO(archive-next): …` markers reserve space for IPFS mirrors and compression steps.
## Scripts
- `pnpm add:deploy --msg "..."` appends a deploy record to `/data/deploys/YYYY-MM-DD.json`.
- `pnpm add:beacon --env ENV --status STATUS` appends a beacon ping to `/data/beacons/YYYY-MM-DD.json`.

5
archive.env.example Normal file
View File

@@ -0,0 +1,5 @@
# Example environment file for Archive CI
GIT_AUTHOR_NAME=Archive-Gen-0
GIT_AUTHOR_EMAIL=archive@example.com
GIT_COMMITTER_NAME=Archive-Gen-0
GIT_COMMITTER_EMAIL=archive@example.com

9
astro.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
import { emitSignature } from './src/lib/sig.ts';
export default defineConfig({
integrations: [svelte(), emitSignature()],
site: 'https://example.com',
output: 'static'
});

View File

@@ -0,0 +1,8 @@
[
{
"ts": "2025-11-24T12:05:00Z",
"env": "genesis",
"status": "ok",
"note": "first beacon"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"ts": "2025-11-24T12:00:00Z",
"msg": "Bootstrap deploy for Archive-Gen-0",
"source": "manual"
}
]

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "blackroad-os-archive",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"lint": "eslint .",
"format": "prettier --write .",
"test": "echo 'Warning: No tests configured' && exit 0",
"add:deploy": "node scripts/add_deploy.mjs",
"add:beacon": "node scripts/add_beacon.mjs"
},
"dependencies": {
"astro": "^4.15.7",
"svelte": "^4.2.18"
},
"devDependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/svelte": "^5.7.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.0",
"eslint-plugin-astro": "^0.34.0",
"eslint-plugin-svelte": "^2.44.0",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.13.0",
"prettier-plugin-svelte": "^3.2.6",
"typescript": "^5.6.2"
}
}

4
public/sig.beacon.json Normal file
View File

@@ -0,0 +1,4 @@
{
"ts": "2025-11-24T12:00:00Z",
"agent": "Archive-Gen-0"
}

48
scripts/add_beacon.mjs Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env node
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
const args = Object.fromEntries(
process.argv.slice(2).map((arg) => {
const [key, ...rest] = arg.replace(/^--/, '').split('=');
return [key, rest.join('=') || ''];
})
);
const env = args.env;
const status = args.status;
const note = args.note || '';
if (!env || !env.trim() || !status || !status.trim()) {
console.error('Usage: pnpm add:beacon --env ENV --status STATUS [--note "..." ]');
process.exit(1);
}
const now = new Date();
const isoDate = now.toISOString();
const dateSlug = isoDate.slice(0, 10);
const targetPath = resolve('data/beacons', `${dateSlug}.json`);
async function appendBeacon() {
await mkdir(resolve('data/beacons'), { recursive: true });
let existing = [];
try {
const raw = await readFile(targetPath, 'utf-8');
existing = JSON.parse(raw);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
}
const record = {
ts: isoDate,
env,
status,
...(note ? { note } : {})
};
await writeFile(targetPath, JSON.stringify([...existing, record], null, 2));
console.log(`Appended beacon to ${targetPath}`);
}
appendBeacon();

44
scripts/add_deploy.mjs Normal file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
const args = Object.fromEntries(
process.argv.slice(2).map((arg) => {
const [key, ...rest] = arg.replace(/^--/, '').split('=');
return [key, rest.join('=') || ''];
})
);
const message = args.msg || args.message;
if (!message) {
console.error('Usage: pnpm add:deploy --msg "Deploy message"');
process.exit(1);
}
const now = new Date();
const isoDate = now.toISOString();
const dateSlug = isoDate.slice(0, 10);
const targetPath = resolve('data/deploys', `${dateSlug}.json`);
async function appendDeploy() {
await mkdir(resolve('data/deploys'), { recursive: true });
let existing = [];
try {
const raw = await readFile(targetPath, 'utf-8');
existing = JSON.parse(raw);
} catch (err) {
if ((err).code !== 'ENOENT') throw err;
}
const record = {
ts: isoDate,
msg: message,
source: 'cli'
};
await writeFile(targetPath, JSON.stringify([...existing, record], null, 2));
console.log(`Appended deploy to ${targetPath}`);
}
appendDeploy();

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import type { BeaconRecord } from '~/lib/data';
export let beacons: BeaconRecord[] = [];
</script>
<section class="beacon-card">
<div class="beacon-card__header">
<h2>Beacon Pings</h2>
<p>Append-only status pulses across environments.</p>
</div>
<table>
<thead>
<tr>
<th>Time</th>
<th>Env</th>
<th>Status</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{#if beacons.length === 0}
<tr>
<td colspan="4">No beacons yet.</td>
</tr>
{:else}
{#each beacons as beacon}
<tr>
<td>{new Date(beacon.ts).toLocaleString()}</td>
<td>{beacon.env}</td>
<td>{beacon.status}</td>
<td>{beacon.note ?? '—'}</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</section>
<style>
.beacon-card {
border: 1px solid var(--theme-border, #222);
background: var(--theme-surface, #0e0e10);
color: var(--theme-text, #f5f5f5);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.beacon-card__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
h2 {
margin: 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
th,
td {
text-align: left;
padding: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
th {
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.8rem;
color: #a1a1aa;
}
tr:hover td {
background: rgba(255, 255, 255, 0.03);
}
</style>

View File

@@ -0,0 +1,134 @@
<script lang="ts">
import type { DeployRecord } from '~/lib/data';
export let deploys: DeployRecord[] = [];
</script>
<section class="timeline">
<header>
<div>
<p class="eyebrow">Deploy Log</p>
<h1>Immutable ship log</h1>
</div>
<p class="hint">Records are sorted newest first and committed to git.</p>
</header>
<ol>
{#if deploys.length === 0}
<li class="empty">No deploys recorded yet.</li>
{:else}
{#each deploys as deploy}
<li>
<div class="dot" aria-hidden="true"></div>
<div class="meta">
<p class="time">{new Date(deploy.ts).toLocaleString()}</p>
<p class="msg">{deploy.msg}</p>
{#if deploy.source}
<p class="source">via {deploy.source}</p>
{/if}
</div>
</li>
{/each}
{/if}
</ol>
</section>
<style>
.timeline {
background: linear-gradient(135deg, #0b0b0e, #0f1119);
border: 1px solid var(--theme-border, #222);
border-radius: 12px;
padding: 1rem 1.5rem;
color: #f5f5f5;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.eyebrow {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #a1a1aa;
font-size: 0.8rem;
}
h1 {
margin: 0.2rem 0 0;
font-size: 1.4rem;
}
.hint {
margin: 0;
color: #94a3b8;
max-width: 18rem;
}
ol {
list-style: none;
padding: 0;
margin: 1rem 0 0;
position: relative;
}
ol::before {
content: '';
position: absolute;
top: 0;
left: 12px;
width: 2px;
height: 100%;
background: rgba(255, 255, 255, 0.1);
}
li {
display: grid;
grid-template-columns: 24px 1fr;
gap: 0.75rem;
position: relative;
padding-bottom: 1rem;
}
.dot {
width: 12px;
height: 12px;
background: #22d3ee;
border-radius: 999px;
margin-top: 0.2rem;
box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.15);
}
.meta {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 0.75rem 1rem;
}
.time {
margin: 0;
color: #a1a1aa;
font-size: 0.9rem;
}
.msg {
margin: 0.1rem 0 0;
font-size: 1rem;
}
.source {
margin: 0.35rem 0 0;
color: #cbd5e1;
font-size: 0.9rem;
}
.empty {
color: #94a3b8;
padding-left: 0.6rem;
}
</style>

64
src/lib/data.ts Normal file
View File

@@ -0,0 +1,64 @@
export type DeployRecord = {
ts: string;
msg: string;
source?: string;
};
export type BeaconRecord = {
ts: string;
env: string;
status: string;
note?: string;
};
type DeployFile = DeployRecord[];
type BeaconFile = BeaconRecord[];
const deployImports = import.meta.glob('../../data/deploys/*.json', {
eager: true,
import: 'default'
}) as Record<string, DeployFile>;
// Validate that deployImports succeeded
if (!deployImports || Object.keys(deployImports).length === 0) {
console.warn('No deploy data files found');
}
const beaconImports = import.meta.glob('../../data/beacons/*.json', {
eager: true,
import: 'default'
}) as Record<string, BeaconFile>;
// Validate that beaconImports succeeded
if (!beaconImports || Object.keys(beaconImports).length === 0) {
console.warn('No beacon data files found');
}
function flatten<T extends { ts: string }>(files: Record<string, T[]>): T[] {
return Object.values(files)
.flat()
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
}
export function getDeploys(): DeployRecord[] {
return flatten(deployImports);
}
export function getBeacons(): BeaconRecord[] {
return flatten(beaconImports);
}
export function getDeploysByDate(date: string): DeployRecord[] {
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new Error(`Invalid date format: ${date}`);
}
return getDeploys().filter((deploy) => deploy.ts.slice(0, 10) === date);
}
export function getBeaconsByDate(date: string): BeaconRecord[] {
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new Error(`Invalid date format: ${date}`);
}
return getBeacons().filter((beacon) => beacon.ts.slice(0, 10) === date);
}
// TODO(archive-next): expose helpers for compressed bundle reads

27
src/lib/sig.ts Normal file
View File

@@ -0,0 +1,27 @@
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const currentDir = dirname(fileURLToPath(import.meta.url));
export async function writeSignature() {
const payload = {
ts: new Date().toISOString(),
agent: 'Archive-Gen-0'
};
const projectRoot = resolve(currentDir, '../..');
const target = resolve(projectRoot, 'public/sig.beacon.json');
await writeFile(target, JSON.stringify(payload, null, 2));
}
export function emitSignature() {
return {
name: 'archive-signature',
hooks: {
'astro:build:done': async () => {
await writeSignature();
// TODO(archive-next): mirror signature to IPFS
}
}
} as const;
}

108
src/pages/index.astro Normal file
View File

@@ -0,0 +1,108 @@
---
import DeployTimeline from '../components/DeployTimeline.svelte';
import BeaconTable from '../components/BeaconTable.svelte';
import { getBeacons, getDeploys } from '../lib/data';
const deploys = getDeploys();
const beacons = getBeacons();
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Blackroad OS · System Archive</title>
</head>
<body>
<main>
<header class="hero">
<p class="eyebrow">Archive-Gen-0</p>
<h1>Blackroad OS · System Archive</h1>
<p class="lede">Append-only ledger for deploys, beacon pings, and day-by-day snapshots.</p>
</header>
<section class="grid">
<DeployTimeline client:load deploys={deploys} />
<BeaconTable client:load beacons={beacons} />
</section>
<section class="cta">
<h2>Daily log views</h2>
<p>Jump into `/logs/YYYY/MM/DD` for date-scoped records.</p>
<p class="todo">// TODO(archive-next): surface compressed log bundles + IPFS gateways</p>
</section>
</main>
</body>
</html>
<style>
:root {
color-scheme: dark;
--theme-surface: #0f1117;
--theme-border: #1f2937;
--theme-text: #e5e7eb;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at 20% 20%, rgba(34, 211, 238, 0.08), transparent 25%),
radial-gradient(circle at 80% 0%, rgba(236, 72, 153, 0.07), transparent 22%),
#06070b;
color: var(--theme-text);
margin: 0;
min-height: 100vh;
}
main {
max-width: 1100px;
padding: 2rem clamp(1rem, 3vw, 2rem);
margin: 0 auto;
}
.hero {
margin-bottom: 1.5rem;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
margin: 0;
}
h1 {
margin: 0.25rem 0 0.35rem;
font-size: clamp(1.8rem, 3vw, 2.4rem);
}
.lede {
margin: 0;
color: #cbd5e1;
max-width: 640px;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 880px) {
.grid {
grid-template-columns: 1.2fr 0.8fr;
}
}
.cta {
margin-top: 1.5rem;
padding: 1rem 1.25rem;
border: 1px dashed #1f2937;
background: rgba(255, 255, 255, 0.02);
border-radius: 10px;
}
.todo {
font-family: 'JetBrains Mono', SFMono-Regular, ui-monospace, monospace;
color: #94a3b8;
}
</style>

View File

@@ -0,0 +1,86 @@
---
import DeployTimeline from '../../../components/DeployTimeline.svelte';
import BeaconTable from '../../../components/BeaconTable.svelte';
import { getBeaconsByDate, getDeploysByDate } from '../../../lib/data';
const { yyyy, mm, dd } = Astro.params;
const slug = `${yyyy}-${mm}-${dd}`;
const deploys = getDeploysByDate(slug);
const beacons = getBeaconsByDate(slug);
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Log for {slug}</title>
</head>
<body>
<main>
<header class="hero">
<p class="eyebrow">Daily log</p>
<h1>{slug}</h1>
<p class="lede">Deploys and beacons captured on this date.</p>
</header>
<section class="grid">
<DeployTimeline client:load deploys={deploys} />
<BeaconTable client:load beacons={beacons} />
</section>
</main>
</body>
</html>
<style>
:root {
color-scheme: dark;
--theme-text: #e5e7eb;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: #06070b;
color: var(--theme-text);
margin: 0;
min-height: 100vh;
}
main {
max-width: 1000px;
padding: 2rem clamp(1rem, 3vw, 2rem);
margin: 0 auto;
}
.hero {
margin-bottom: 1.5rem;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
margin: 0;
}
h1 {
margin: 0.25rem 0 0.35rem;
font-size: clamp(1.6rem, 3vw, 2.2rem);
}
.lede {
margin: 0;
color: #cbd5e1;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 880px) {
.grid {
grid-template-columns: 1.2fr 0.8fr;
}
}
</style>

9
tsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
}
}
}