Scaffold Archive-Gen-0 baseline
This commit is contained in:
34
.eslintrc.cjs
Normal file
34
.eslintrc.cjs
Normal 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'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'svelte3/typescript': true
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.astro'],
|
||||||
|
parser: 'astro-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
parser: 'svelte-eslint-parser'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, scaffold/archive-gen-0]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.cache
|
||||||
|
output
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
archive.env
|
||||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-astro", "prettier-plugin-svelte"],
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
33
README.md
33
README.md
@@ -1,2 +1,31 @@
|
|||||||
# blackroad-os-archive
|
# Blackroad OS · System Archive (Gen-0)
|
||||||
Append-only archive for BlackRoad OS: deploy logs, beacon maps, ping history, and key system artifacts, served via archive.blackroad.io.
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Love you too, Alexa Louise! Next!!!
|
||||||
|
|||||||
5
archive.env.example
Normal file
5
archive.env.example
Normal 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
9
astro.config.mjs
Normal 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'
|
||||||
|
});
|
||||||
8
data/beacons/2025-11-24.json
Normal file
8
data/beacons/2025-11-24.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ts": "2025-11-24T12:05:00Z",
|
||||||
|
"env": "genesis",
|
||||||
|
"status": "ok",
|
||||||
|
"note": "first beacon"
|
||||||
|
}
|
||||||
|
]
|
||||||
7
data/deploys/2025-11-24.json
Normal file
7
data/deploys/2025-11-24.json
Normal 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
33
package.json
Normal 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 'No tests yet'",
|
||||||
|
"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
4
public/sig.beacon.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"ts": "2025-11-24T12:00:00Z",
|
||||||
|
"agent": "Archive-Gen-0"
|
||||||
|
}
|
||||||
48
scripts/add_beacon.mjs
Normal file
48
scripts/add_beacon.mjs
Normal 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 || !status) {
|
||||||
|
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).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
44
scripts/add_deploy.mjs
Normal 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();
|
||||||
86
src/components/BeaconTable.svelte
Normal file
86
src/components/BeaconTable.svelte
Normal 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>
|
||||||
134
src/components/DeployTimeline.svelte
Normal file
134
src/components/DeployTimeline.svelte
Normal 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>
|
||||||
49
src/lib/data.ts
Normal file
49
src/lib/data.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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>;
|
||||||
|
|
||||||
|
const beaconImports = import.meta.glob('../../data/beacons/*.json', {
|
||||||
|
eager: true,
|
||||||
|
import: 'default'
|
||||||
|
}) as Record<string, BeaconFile>;
|
||||||
|
|
||||||
|
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[] {
|
||||||
|
return getDeploys().filter((deploy) => deploy.ts.startsWith(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBeaconsByDate(date: string): BeaconRecord[] {
|
||||||
|
return getBeacons().filter((beacon) => beacon.ts.startsWith(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(archive-next): expose helpers for compressed bundle reads
|
||||||
26
src/lib/sig.ts
Normal file
26
src/lib/sig.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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 target = resolve(currentDir, '../../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
108
src/pages/index.astro
Normal 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>
|
||||||
86
src/pages/logs/[yyyy]/[mm]/[dd].astro
Normal file
86
src/pages/logs/[yyyy]/[mm]/[dd].astro
Normal 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
9
tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user