This commit is contained in:
2026-08-03 13:59:18 +02:00
commit 8798c74d4b
11 changed files with 3590 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
colony.json
colony.json.tmp
+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 92" role="img" aria-label="Technical orbital divider">
<g fill="none" stroke="#b88a43" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 46H435"/>
<path d="M765 46H1180"/>
<path d="M42 36v20M65 39v14M89 36v20M1110 36v20M1135 39v14M1158 36v20" stroke-width="2"/>
<rect x="436" y="33" width="62" height="26" rx="3"/>
<rect x="702" y="33" width="62" height="26" rx="3"/>
<circle cx="600" cy="46" r="31"/>
<ellipse cx="600" cy="46" rx="66" ry="18" transform="rotate(-12 600 46)"/>
<path d="M499 46h52M649 46h52"/>
<path d="M510 37v18M530 40v12M670 40v12M690 37v18" stroke-width="2"/>
</g>
<g fill="#b88a43">
<circle cx="20" cy="46" r="5"/><circle cx="1180" cy="46" r="5"/>
<circle cx="467" cy="46" r="4"/><circle cx="733" cy="46" r="4"/>
<circle cx="600" cy="46" r="8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

+1728
View File
File diff suppressed because it is too large Load Diff
+428
View File
@@ -0,0 +1,428 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Game Sessions — Orison Reach</title>
<style>
:root {
--void: #171513;
--void-soft: #211e1a;
--panel: #2a2520;
--paper: #e8dcc4;
--paper-deep: #cbbd9e;
--ink: #2c231c;
--text: #e9dfca;
--muted: #a89d89;
--brass: #b88a43;
--rust: #94452f;
--moss: #70794e;
--oxygen: #86a5a4;
--line: rgba(232, 220, 196, .18);
--shadow: 0 22px 70px rgba(0,0,0,.42);
--serif: Georgia, "Times New Roman", serif;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--text);
background:
radial-gradient(circle at 12% 8%, rgba(134,165,164,.08), transparent 26rem),
linear-gradient(rgba(255,255,255,.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.014) 1px, transparent 1px),
var(--void);
background-size: auto, 28px 28px, 28px 28px, auto;
font-family: var(--sans);
line-height: 1.7;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: .16;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
mix-blend-mode: soft-light;
z-index: 10;
}
a { color: inherit; }
.shell { width: min(1180px, calc(100% - 36px)); margin: 0 auto; }
nav {
position: sticky;
top: 0;
z-index: 5;
border-bottom: 1px solid var(--line);
background: rgba(23,21,19,.86);
backdrop-filter: blur(14px);
}
nav .shell { display: flex; gap: 1.6rem; overflow-x: auto; align-items: center; }
nav a {
padding: 1rem 0;
text-decoration: none;
white-space: nowrap;
font: 650 .72rem/1 var(--mono);
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
nav a:hover, nav a.active { color: var(--paper); }
h1, h2, h3 { font-family: var(--serif); font-weight: 600; letter-spacing: -.02em; margin: 0; }
h2 { font-size: clamp(2.2rem, 5vw, 4.2rem); line-height: 1.05; color: var(--paper); }
.eyebrow {
color: var(--brass);
font: 700 .74rem/1.2 var(--mono);
letter-spacing: .22em;
text-transform: uppercase;
}
main { padding: 4rem 0 7rem; }
.page-header { margin-bottom: 3rem; }
.page-header-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 2rem;
flex-wrap: wrap;
margin-top: .8rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: .5rem;
padding: .7rem 1.4rem;
border: 1px solid var(--line);
background: transparent;
color: var(--brass);
font: 700 .72rem/1 var(--mono);
letter-spacing: .14em;
text-transform: uppercase;
cursor: pointer;
text-decoration: none;
transition: background .15s, border-color .15s, color .15s;
white-space: nowrap;
}
.btn:hover { background: rgba(184,138,67,.12); border-color: var(--brass); }
.btn.primary { background: var(--brass); color: var(--void); border-color: var(--brass); }
.btn.primary:hover { background: #cca055; }
.btn.danger { color: var(--rust); }
.btn.danger:hover { background: rgba(148,69,47,.15); border-color: var(--rust); }
.btn.sm { padding: .45rem .9rem; font-size: .66rem; }
/* Game list — open, editorial */
.games-list { display: flex; flex-direction: column; }
.game-row {
padding: 2rem 0;
border-bottom: 1px solid var(--line);
text-decoration: none;
display: block;
transition: color .15s;
}
.game-row:first-child { border-top: 1px solid var(--line); }
.game-row-eyebrow {
font: 700 .7rem/1.4 var(--mono);
letter-spacing: .16em;
text-transform: uppercase;
color: var(--brass);
margin-bottom: .5rem;
}
.game-row-eyebrow .sep { color: var(--muted); opacity: .6; margin: 0 .25em; }
.game-row-eyebrow .e-dark { color: var(--rust); }
.game-row-title {
font-family: var(--serif);
font-size: clamp(1.5rem, 3vw, 2.4rem);
font-weight: 600;
color: var(--paper);
letter-spacing: -.02em;
line-height: 1.05;
margin: 0 0 .6rem;
transition: color .15s;
}
.game-row:hover .game-row-title { color: var(--brass); }
.game-row-desc { color: #c2b7a4; font-size: 1rem; line-height: 1.6; max-width: 72ch; }
.empty-state {
padding: 3rem 0;
color: var(--muted);
font: italic 1rem/1.6 var(--serif);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(23,21,19,.88);
backdrop-filter: blur(6px);
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.modal {
background: var(--void-soft);
border: 1px solid rgba(232,220,196,.3);
padding: 2rem 2rem 1.6rem;
width: min(560px, 100%);
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.modal-title {
font-family: var(--serif);
font-size: 1.5rem;
font-weight: 600;
color: var(--paper);
margin: 0 0 1.6rem;
}
.form-group { margin-bottom: 1.2rem; }
.form-label {
display: block;
font: 700 .68rem/1 var(--mono);
letter-spacing: .1em;
text-transform: uppercase;
color: var(--brass);
margin-bottom: .45rem;
}
.form-label span { color: var(--rust); margin-left: .2rem; }
input[type="text"], input[type="password"], input[type="date"], textarea, select {
width: 100%;
background: var(--panel);
border: 1px solid rgba(232,220,196,.2);
color: var(--text);
padding: .65rem .85rem;
font-family: var(--sans);
font-size: .94rem;
line-height: 1.5;
appearance: none;
-webkit-appearance: none;
}
textarea { resize: vertical; min-height: 80px; }
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--brass);
}
.mood-row { display: flex; gap: 1rem; }
.mood-radio { display: flex; align-items: center; gap: .5rem; cursor: pointer; }
.mood-radio input { width: auto; }
.mood-radio span { font: .82rem/1 var(--mono); color: var(--text); }
.form-hint { font: .72rem/1.4 var(--mono); color: var(--muted); margin-top: .35rem; }
.form-error { font: .78rem/1.4 var(--mono); color: var(--rust); margin-top: .4rem; }
.date-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.modal-actions { display: flex; justify-content: flex-end; gap: .8rem; margin-top: 1.6rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
footer { border-top: 1px solid var(--line); padding: 2.5rem 0 4rem; color: var(--muted); }
footer .shell { display: flex; justify-content: space-between; gap: 2rem; flex-wrap: wrap; }
footer p { margin: 0; font: .72rem/1.7 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
[x-cloak] { display: none !important; }
</style>
</head>
<body x-data="gamesApp()" x-cloak>
<nav aria-label="Archive sections">
<div class="shell">
<a href="index.html">Archive</a>
<a href="games.html" class="active">Game Sessions</a>
</div>
</nav>
<main class="shell">
<div class="page-header" style="margin-bottom:4rem">
<div style="display:flex;align-items:flex-end;justify-content:space-between;gap:2rem;flex-wrap:wrap">
<div>
<div class="eyebrow" style="margin-bottom:.4rem">Colony Chronicle // Active Sessions</div>
<h2 style="font-family:var(--serif);font-size:clamp(2.2rem,5vw,4.2rem);font-weight:600;color:var(--paper);letter-spacing:-.02em;line-height:1.05;margin:0">Game Sessions</h2>
</div>
<button class="btn primary" @click="openCreate()">+ New Session</button>
</div>
</div>
<div class="games-list">
<div x-show="loading" class="empty-state" style="border-top:1px solid var(--line)">Loading sessions…</div>
<div x-show="!loading && games.length === 0" class="empty-state" style="border-top:1px solid var(--line)">
No sessions recorded. Start a new one to begin the chronicle.
</div>
<template x-for="game in games" :key="game.id">
<a class="game-row" :href="`game.html?slug=${game.slug}`">
<div class="game-row-eyebrow">
<span x-text="game.timeline_start"></span> → <span x-text="game.timeline_end"></span>
<span class="sep"> // </span>
<span :class="game.mood === 'dark' ? 'e-dark' : ''" x-text="game.mood"></span>
</div>
<div class="game-row-title" x-text="game.title"></div>
<div class="game-row-desc" x-show="game.description" x-text="game.description"
style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden"></div>
</a>
</template>
</div>
</main>
<footer>
<div class="shell">
<p>Orison Reach Public Archive<br />Civilian access edition</p>
<p>Knowledge builds tomorrow<br />Work keeps it standing</p>
</div>
</footer>
<!-- Create Game Modal -->
<div class="modal-overlay" x-show="showModal" @keydown.escape.window="closeModal()">
<div class="modal" @click.stop>
<div class="modal-title">New Session</div>
<div class="form-group">
<label class="form-label">Title <span>*</span></label>
<input type="text" x-model="form.title" placeholder="The Long Winter" @keydown.enter.prevent />
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea x-model="form.description" placeholder="What is this session about?"></textarea>
</div>
<div class="form-group">
<label class="form-label">Mood</label>
<div class="mood-row">
<label class="mood-radio"><input type="radio" x-model="form.mood" value="light" /><span>Light</span></label>
<label class="mood-radio"><input type="radio" x-model="form.mood" value="dark" /><span>Dark</span></label>
</div>
</div>
<div class="date-row">
<div class="form-group">
<label class="form-label">Timeline Start <span>*</span></label>
<input type="date" x-model="form.timeline_start" />
</div>
<div class="form-group">
<label class="form-label">Timeline End <span>*</span></label>
<input type="date" x-model="form.timeline_end" />
</div>
</div>
<div class="form-group">
<label class="form-label">Password <span>*</span></label>
<input type="password" x-model="form.password" placeholder="Required to edit this session" />
<div class="form-hint">Anyone with the URL can read. Password required to add or edit entries.</div>
</div>
<div style="border-top:1px solid rgba(232,220,196,.18);margin-top:1.4rem;padding-top:1.2rem;">
<div style="font:700 .68rem/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem;">Opening Event</div>
<div class="form-group">
<label class="form-label">Title</label>
<input type="text" x-model="form.start_title" placeholder="Where does this story begin?" />
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea x-model="form.start_description" placeholder="The circumstances of the opening moment…" style="min-height:60px"></textarea>
</div>
<div class="form-group">
<label class="form-label">Mood</label>
<div class="mood-row">
<label class="mood-radio"><input type="radio" x-model="form.start_mood" value="" /><span>Inherit</span></label>
<label class="mood-radio"><input type="radio" x-model="form.start_mood" value="light" /><span>Light</span></label>
<label class="mood-radio"><input type="radio" x-model="form.start_mood" value="dark" /><span>Dark</span></label>
</div>
</div>
</div>
<div style="border-top:1px solid rgba(232,220,196,.18);margin-top:1.4rem;padding-top:1.2rem;">
<div style="font:700 .68rem/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem;">Closing Event</div>
<div class="form-group">
<label class="form-label">Title</label>
<input type="text" x-model="form.end_title" placeholder="Where does this story end?" />
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea x-model="form.end_description" placeholder="The circumstances of the closing moment…" style="min-height:60px"></textarea>
</div>
<div class="form-group">
<label class="form-label">Mood</label>
<div class="mood-row">
<label class="mood-radio"><input type="radio" x-model="form.end_mood" value="" /><span>Inherit</span></label>
<label class="mood-radio"><input type="radio" x-model="form.end_mood" value="light" /><span>Light</span></label>
<label class="mood-radio"><input type="radio" x-model="form.end_mood" value="dark" /><span>Dark</span></label>
</div>
</div>
</div>
<div class="form-error" x-show="formError" x-text="formError"></div>
<div class="modal-actions">
<button class="btn" @click="closeModal()">Cancel</button>
<button class="btn primary" @click="createGame()" :disabled="saving">
<span x-text="saving ? 'Creating…' : 'Create Session'"></span>
</button>
</div>
</div>
</div>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script>
<script>
function gamesApp() {
return {
games: [],
loading: true,
showModal: false,
saving: false,
formError: null,
form: {},
async init() {
await this.loadGames()
},
async loadGames() {
this.loading = true
try {
const r = await fetch('/api/games')
this.games = r.ok ? await r.json() : []
} catch {
this.games = []
}
this.loading = false
},
openCreate() {
this.form = {
title: '', description: '', mood: 'light',
timeline_start: '', timeline_end: '', password: '',
start_title: '', start_description: '', start_mood: '',
end_title: '', end_description: '', end_mood: ''
}
this.formError = null
this.showModal = true
},
closeModal() {
this.showModal = false
this.form = {}
this.formError = null
},
async createGame() {
if (!this.form.title) { this.formError = 'Title is required'; return }
if (!this.form.timeline_start) { this.formError = 'Start date is required'; return }
if (!this.form.timeline_end) { this.formError = 'End date is required'; return }
if (this.form.timeline_end <= this.form.timeline_start) { this.formError = 'End date must be after start date'; return }
if (!this.form.password) { this.formError = 'Password is required'; return }
this.saving = true
this.formError = null
try {
const r = await fetch('/api/games', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
})
const data = await r.json()
if (!r.ok) { this.formError = data.error || 'Failed to create session'; return }
window.location.href = `game.html?slug=${data.slug}`
} catch {
this.formError = 'Network error. Is the server running?'
} finally {
this.saving = false
}
}
}
}
</script>
</body>
</html>
+335
View File
@@ -0,0 +1,335 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="A fictional archive from the frontier colony of Orison Reach." />
<title>Orison Reach — Colony Archive</title>
<style>
:root {
--void: #171513;
--void-soft: #211e1a;
--panel: #2a2520;
--paper: #e8dcc4;
--paper-deep: #cbbd9e;
--ink: #2c231c;
--text: #e9dfca;
--muted: #a89d89;
--brass: #b88a43;
--rust: #94452f;
--moss: #70794e;
--oxygen: #86a5a4;
--line: rgba(232, 220, 196, .18);
--shadow: 0 22px 70px rgba(0,0,0,.42);
--serif: Georgia, "Times New Roman", serif;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--text);
background:
radial-gradient(circle at 12% 8%, rgba(134,165,164,.08), transparent 26rem),
linear-gradient(rgba(255,255,255,.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.014) 1px, transparent 1px),
var(--void);
background-size: auto, 28px 28px, 28px 28px, auto;
font-family: var(--sans);
line-height: 1.7;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: .16;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
mix-blend-mode: soft-light;
z-index: 10;
}
a { color: inherit; }
img { display: block; max-width: 100%; }
.shell { width: min(1180px, calc(100% - 36px)); margin: 0 auto; }
.masthead {
min-height: 74vh;
display: grid;
align-items: end;
position: relative;
overflow: hidden;
border-bottom: 1px solid var(--line);
background:
linear-gradient(to top, rgba(23,21,19,.98) 0%, rgba(23,21,19,.54) 45%, rgba(23,21,19,.12) 100%),
url("assets/engineering.png") center 30% / cover no-repeat;
}
.masthead::after {
content: "ARCHIVE // ORISON REACH // CYCLE 0187";
position: absolute;
top: 26px;
right: 32px;
color: var(--paper);
font: 600 .72rem/1 var(--mono);
letter-spacing: .18em;
opacity: .72;
}
.hero-copy { padding: 7rem 0 4.5rem; max-width: 830px; }
.eyebrow {
color: var(--brass);
font: 700 .74rem/1.2 var(--mono);
letter-spacing: .22em;
text-transform: uppercase;
}
h1, h2, h3 {
font-family: var(--serif);
font-weight: 600;
letter-spacing: -.02em;
margin: 0;
}
h1 { font-size: clamp(3.2rem, 9vw, 7.6rem); line-height: .92; max-width: 10ch; }
h2 { font-size: clamp(2.2rem, 5vw, 4.2rem); line-height: 1.05; color: var(--paper); }
h3 { font-size: clamp(1.35rem, 2.4vw, 2rem); line-height: 1.25; color: var(--paper); }
.hero-copy p { max-width: 62ch; color: #d0c6b3; font-size: 1.08rem; }
nav {
position: sticky;
top: 0;
z-index: 5;
border-bottom: 1px solid var(--line);
background: rgba(23,21,19,.86);
backdrop-filter: blur(14px);
}
nav .shell { display: flex; gap: 1.6rem; overflow-x: auto; }
nav a {
padding: 1rem 0;
text-decoration: none;
white-space: nowrap;
font: 650 .72rem/1 var(--mono);
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
nav a:hover { color: var(--paper); }
main { padding: 5rem 0 7rem; }
.intro { max-width: 770px; }
.intro .lead { font: 1.35rem/1.65 var(--serif); color: var(--paper); }
.divider {
width: min(100%, 980px);
margin: 4.2rem auto;
opacity: .9;
}
.project {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(280px, .75fr);
gap: clamp(2rem, 5vw, 5rem);
align-items: start;
margin: 0 0 7rem;
}
.project:nth-of-type(even) .project-image { order: 2; }
.project-image {
position: sticky;
top: 5.3rem;
border: 1px solid rgba(232,220,196,.28);
box-shadow: var(--shadow);
background: var(--panel);
padding: 8px;
}
.project-image img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.project-copy { padding-top: 1rem; }
.project-copy p { color: #c2b7a4; }
.meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
margin-top: 2rem;
}
.meta div { background: var(--void-soft); padding: 1rem; }
.meta dt { color: var(--brass); font: 700 .68rem/1.2 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.meta dd { margin: .38rem 0 0; color: var(--paper); }
.document {
margin-top: 2rem;
background: var(--paper);
color: var(--ink);
padding: clamp(1.4rem, 4vw, 3.5rem);
border: 1px solid #9d8d70;
box-shadow: var(--shadow);
position: relative;
}
.document::before,
.document::after {
content: "";
position: absolute;
width: 22px;
height: 22px;
border-color: var(--rust);
opacity: .6;
}
.document::before { left: 12px; top: 12px; border-left: 2px solid; border-top: 2px solid; }
.document::after { right: 12px; bottom: 12px; border-right: 2px solid; border-bottom: 2px solid; }
.document h3 { color: var(--ink); }
.document p { color: #514538; }
.document code { font-family: var(--mono); color: var(--rust); }
.table-wrap { overflow-x: auto; margin-top: 1.5rem; }
table { width: 100%; border-collapse: collapse; min-width: 680px; font-size: .94rem; }
th, td { padding: .9rem 1rem; border-bottom: 1px solid rgba(44,35,28,.2); text-align: left; }
th { font: 700 .7rem/1.2 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--rust); }
tbody tr:hover { background: rgba(148,69,47,.07); }
.status { font: 700 .7rem/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.status.stable { color: #566238; }
.status.watch { color: #9a6b1d; }
.status.critical { color: #91372a; }
blockquote {
margin: 2rem 0 0;
padding: 1.3rem 1.5rem;
border-left: 4px solid var(--brass);
background: rgba(184,138,67,.08);
color: var(--paper);
font-family: var(--serif);
font-size: 1.25rem;
}
footer { border-top: 1px solid var(--line); padding: 2.5rem 0 4rem; color: var(--muted); }
footer .shell { display: flex; justify-content: space-between; gap: 2rem; flex-wrap: wrap; }
footer p { margin: 0; font: .72rem/1.7 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
@media (max-width: 840px) {
.masthead { min-height: 68vh; }
.masthead::after { left: 20px; right: auto; }
.project { grid-template-columns: 1fr; }
.project:nth-of-type(even) .project-image { order: initial; }
.project-image { position: relative; top: auto; }
}
</style>
</head>
<body>
<header class="masthead">
<div class="shell hero-copy">
<div class="eyebrow">Frontier Directorate Field Archive</div>
<h1>Orison Reach</h1>
<p>A record of the people trying to turn an unfinished station into a permanent human settlement—before the cold, the dust, or simple arithmetic proves them wrong.</p>
</div>
</header>
<nav aria-label="Archive sections">
<div class="shell">
<a href="#charter">Charter</a>
<a href="#science">Scientific Project</a>
<a href="#engineering">Engineering Project</a>
<a href="#labour">Labour Project</a>
<a href="#ledger">Survival Ledger</a>
<a href="games.html" style="color:var(--brass);margin-left:auto">Game Sessions</a>
</div>
</nav>
<main class="shell">
<section class="intro" id="charter">
<div class="eyebrow">Colony Charter // Revision 07</div>
<h2>Survival is a shared system</h2>
<p class="lead">No discipline can preserve the colony alone. Science determines what is possible. Engineering decides how possibility becomes infrastructure. Labour turns plans into weight-bearing reality.</p>
<p>Orison Reach began as a temporary logistics station on the edge of the Tycho Drift. Eighteen years later, its inhabitants have stopped speaking about evacuation windows. They speak instead about harvests, pressure reserves, foundation depth and children who have never seen an open sky.</p>
</section>
<img class="divider" src="assets/divider.svg" alt="" />
<section class="project" id="science">
<figure class="project-image"><img src="assets/science.png" alt="Scientists working in a dense frontier-colony laboratory" /></figure>
<div class="project-copy">
<div class="eyebrow">Project S-14 // Scientific Directorate</div>
<h2>The Green Vault</h2>
<h3>A closed biosphere that must outlive its designers</h3>
<p>The colony's most physically comfortable work takes place behind sealed laboratory doors. It is also the work with the smallest margin for failure. The Green Vault team must create soil from mineral dust, maintain viable seed lines and prevent a single aggressive fungus from collapsing the food chain.</p>
<p>Every successful crop reduces dependence on supply ships. Every failed cycle consumes water, nutrients and time that the colony may not have.</p>
<dl class="meta">
<div><dt>Lead discipline</dt><dd>Ecological systems</dd></div>
<div><dt>Current cycle</dt><dd>18 of 24</dd></div>
<div><dt>Primary risk</dt><dd>Microbial collapse</dd></div>
<div><dt>Outcome</dt><dd>Food independence</dd></div>
</dl>
</div>
</section>
<img class="divider" src="assets/divider.svg" alt="" />
<section class="project" id="engineering">
<figure class="project-image"><img src="assets/engineering.png" alt="Civil engineers planning and surveying a colony expansion" /></figure>
<div class="project-copy">
<div class="eyebrow">Project E-07 // Civil Works</div>
<h2>The Meridian Ring</h2>
<h3>Designing a district before the ground beneath it is understood</h3>
<p>The civil engineers of Orison Reach work between theory and consequence. They calculate pressure loads, survey unstable regolith, route heat and water, and decide which systems may share a wall without turning one failure into five.</p>
<p>They rarely lift the beams themselves. Their labour is measured in tolerances, redundancies, revisions and the uncomfortable knowledge that every line on a drawing may eventually carry somebody's home.</p>
<dl class="meta">
<div><dt>Lead discipline</dt><dd>Habitat infrastructure</dd></div>
<div><dt>Design life</dt><dd>120 years</dd></div>
<div><dt>Primary risk</dt><dd>Thermal subsidence</dd></div>
<div><dt>Outcome</dt><dd>4,000 new residents</dd></div>
</dl>
</div>
</section>
<img class="divider" src="assets/divider.svg" alt="" />
<section class="project" id="labour">
<figure class="project-image"><img src="assets/labour.png" alt="Colony labourers maintaining machinery and gathering after work" /></figure>
<div class="project-copy">
<div class="eyebrow">Project L-21 // Works Cooperative</div>
<h2>The Deep Cut</h2>
<h3>Extracting tomorrow from stone that does not care who survives</h3>
<p>Beyond the pressurised districts, crews cut foundation trenches, move shielding rock and maintain the machines that keep the work moving. The shifts are long, the suits are heavy and the dust finds every weakness in a seal.</p>
<p>The Deep Cut is not glamorous. It is the slow accumulation of tonnes, metres and repaired parts. Yet each completed section becomes shelter, radiation shielding, transport tunnels and room for the colony to grow.</p>
<blockquote>“Nobody remembers the first tonne. They remember the day the new district held pressure.”</blockquote>
</div>
</section>
<img class="divider" src="assets/divider.svg" alt="" />
<section class="document" id="ledger">
<div class="eyebrow">Operational extract // Cycle 0187.44</div>
<h2 style="color:var(--ink)">Colony survival ledger</h2>
<h3>Priority systems and current margins</h3>
<p>The ledger is reviewed at the beginning of every station day. Any reserve below <code>12 days</code> automatically suspends non-essential expansion work.</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>System</th><th>Directorate</th><th>Reserve</th><th>Trend</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td>Potable water</td><td>Engineering</td><td>31 days</td><td>+2.1%</td><td><span class="status stable">Stable</span></td></tr>
<tr><td>Staple calories</td><td>Science</td><td>19 days</td><td>+0.4%</td><td><span class="status watch">Watch</span></td></tr>
<tr><td>Pressure seal stock</td><td>Labour</td><td>14 days</td><td>−3.8%</td><td><span class="status watch">Watch</span></td></tr>
<tr><td>Reactor coolant</td><td>Engineering</td><td>46 days</td><td>0.0%</td><td><span class="status stable">Stable</span></td></tr>
<tr><td>Antifungal cultures</td><td>Science</td><td>8 days</td><td>−6.2%</td><td><span class="status critical">Critical</span></td></tr>
</tbody>
</table>
</div>
</section>
</main>
<footer>
<div class="shell">
<p>Orison Reach Public Archive<br />Civilian access edition</p>
<p>Knowledge builds tomorrow<br />Work keeps it standing</p>
</div>
</footer>
</body>
</html>
+835
View File
@@ -0,0 +1,835 @@
{
"name": "colony-archive-site",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "colony-archive-site",
"version": "1.0.0",
"dependencies": {
"bcryptjs": "^2.4.3",
"express": "^4.18.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
"integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4",
"side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "colony-archive-site",
"version": "1.0.0",
"description": "Orison Reach Colony Archive",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"express": "^4.18.2"
}
}
+229
View File
@@ -0,0 +1,229 @@
const express = require('express')
const bcrypt = require('bcryptjs')
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const app = express()
const PORT = process.env.PORT || 3000
const DB_PATH = path.join(__dirname, 'colony.json')
function loadDB() {
try { return JSON.parse(fs.readFileSync(DB_PATH, 'utf8')) }
catch { return { games: [] } }
}
function saveDB(data) {
const tmp = DB_PATH + '.tmp'
fs.writeFileSync(tmp, JSON.stringify(data, null, 2))
fs.renameSync(tmp, DB_PATH)
}
function toSlug(title) {
return title.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
}
function newId() {
return crypto.randomUUID()
}
function now() {
return new Date().toISOString()
}
app.use(express.json())
app.use(express.static(path.join(__dirname)))
function requirePassword(req, res, next) {
const password = req.headers['x-game-password']
if (!password) return res.status(401).json({ error: 'Password required' })
const db = loadDB()
const game = db.games.find(g => g.slug === req.params.slug)
if (!game) return res.status(404).json({ error: 'Game not found' })
if (!bcrypt.compareSync(password, game.password_hash)) {
return res.status(401).json({ error: 'Invalid password' })
}
req.game = game
req.db = db
next()
}
function publicGame(g) {
return {
id: g.id, slug: g.slug, title: g.title, description: g.description || '',
mood: g.mood || 'light',
timeline_start: g.timeline_start, timeline_end: g.timeline_end,
start_title: g.start_title || '', start_description: g.start_description || '', start_mood: g.start_mood || '',
end_title: g.end_title || '', end_description: g.end_description || '', end_mood: g.end_mood || '',
created_at: g.created_at
}
}
// --- Games ---
app.get('/api/games', (req, res) => {
const db = loadDB()
res.json(db.games.map(g => ({
id: g.id, slug: g.slug, title: g.title, description: g.description,
mood: g.mood, timeline_start: g.timeline_start, timeline_end: g.timeline_end,
created_at: g.created_at
})))
})
app.post('/api/games', (req, res) => {
const { title, description, mood, timeline_start, timeline_end, password,
start_title, start_description, start_mood,
end_title, end_description, end_mood } = req.body
if (!title || !timeline_start || !timeline_end || !password) {
return res.status(400).json({ error: 'title, timeline_start, timeline_end, password required' })
}
const base = toSlug(title)
if (!base) return res.status(400).json({ error: 'Title must contain letters or numbers' })
const db = loadDB()
let slug = base, n = 1
while (db.games.find(g => g.slug === slug)) slug = `${base}-${n++}`
const game = {
id: newId(), slug, title, description: description || '',
mood: mood || 'light', timeline_start, timeline_end,
start_title: start_title || '', start_description: start_description || '', start_mood: start_mood || '',
end_title: end_title || '', end_description: end_description || '', end_mood: end_mood || '',
password_hash: bcrypt.hashSync(password, 10),
created_at: now(), items: []
}
db.games.push(game)
saveDB(db)
res.status(201).json(publicGame(game))
})
app.get('/api/games/:slug', (req, res) => {
const db = loadDB()
const game = db.games.find(g => g.slug === req.params.slug)
if (!game) return res.status(404).json({ error: 'Game not found' })
res.json(publicGame(game))
})
app.put('/api/games/:slug', requirePassword, (req, res) => {
const { title, description, mood, timeline_start, timeline_end,
start_title, start_description, start_mood,
end_title, end_description, end_mood } = req.body
if (!title || !timeline_start || !timeline_end) {
return res.status(400).json({ error: 'title, timeline_start, timeline_end required' })
}
const game = req.game
game.title = title
game.description = description || ''
game.mood = mood || 'light'
game.timeline_start = timeline_start
game.timeline_end = timeline_end
game.start_title = start_title || ''
game.start_description = start_description || ''
game.start_mood = start_mood || ''
game.end_title = end_title || ''
game.end_description = end_description || ''
game.end_mood = end_mood || ''
saveDB(req.db)
res.json(publicGame(game))
})
app.post('/api/games/:slug/verify', (req, res) => {
const { password } = req.body
if (!password) return res.status(400).json({ error: 'Password required' })
const db = loadDB()
const game = db.games.find(g => g.slug === req.params.slug)
if (!game) return res.status(404).json({ error: 'Game not found' })
if (!bcrypt.compareSync(password, game.password_hash)) {
return res.status(401).json({ error: 'Invalid password' })
}
res.json({ ok: true })
})
// --- Items ---
app.get('/api/games/:slug/items', (req, res) => {
const db = loadDB()
const game = db.games.find(g => g.slug === req.params.slug)
if (!game) return res.status(404).json({ error: 'Game not found' })
const sorted = [...(game.items || [])].sort((a, b) => a.date.localeCompare(b.date) || a.created_at.localeCompare(b.created_at))
res.json(sorted)
})
app.post('/api/games/:slug/items', requirePassword, (req, res) => {
const { type, title, description, mood, date, end_date, status, discipline } = req.body
if (!type || !title || !date) return res.status(400).json({ error: 'type, title, date required' })
if (!['event', 'project', 'person'].includes(type)) {
return res.status(400).json({ error: 'type must be event, project, or person' })
}
const item = {
id: newId(), type, title, description: description || '',
mood: mood || null, date, end_date: end_date || null,
status: status || null, discipline: discipline || null, created_at: now()
}
req.game.items = req.game.items || []
req.game.items.push(item)
saveDB(req.db)
res.status(201).json(item)
})
app.put('/api/games/:slug/items/:id', requirePassword, (req, res) => {
const items = req.game.items || []
const idx = items.findIndex(i => i.id === req.params.id)
if (idx === -1) return res.status(404).json({ error: 'Item not found' })
const { title, description, mood, date, end_date, status, discipline } = req.body
if (!title || !date) return res.status(400).json({ error: 'title, date required' })
items[idx] = { ...items[idx], title, description: description || '', mood: mood || null, date, end_date: end_date || null, status: status || null, discipline: discipline || null }
saveDB(req.db)
res.json(items[idx])
})
app.delete('/api/games/:slug/items/:id', requirePassword, (req, res) => {
const items = req.game.items || []
const idx = items.findIndex(i => i.id === req.params.id)
if (idx === -1) return res.status(404).json({ error: 'Item not found' })
items.splice(idx, 1)
saveDB(req.db)
res.json({ ok: true })
})
// --- Keywords ---
app.get('/api/games/:slug/keywords', (req, res) => {
const db = loadDB()
const game = db.games.find(g => g.slug === req.params.slug)
if (!game) return res.status(404).json({ error: 'Game not found' })
res.json([...(game.keywords || [])].sort((a, b) => a.term.localeCompare(b.term)))
})
app.post('/api/games/:slug/keywords', requirePassword, (req, res) => {
const { term, definition } = req.body
if (!term) return res.status(400).json({ error: 'term required' })
req.game.keywords = req.game.keywords || []
const existing = req.game.keywords.find(k => k.term.toLowerCase() === term.toLowerCase())
if (existing) {
existing.definition = definition || ''
saveDB(req.db)
return res.json(existing)
}
const kw = { id: newId(), term, definition: definition || '', created_at: now() }
req.game.keywords.push(kw)
saveDB(req.db)
res.status(201).json(kw)
})
app.put('/api/games/:slug/keywords/:id', requirePassword, (req, res) => {
const kws = req.game.keywords || []
const idx = kws.findIndex(k => k.id === req.params.id)
if (idx === -1) return res.status(404).json({ error: 'Keyword not found' })
const { term, definition } = req.body
kws[idx] = { ...kws[idx], term: term || kws[idx].term, definition: definition || '' }
saveDB(req.db)
res.json(kws[idx])
})
app.delete('/api/games/:slug/keywords/:id', requirePassword, (req, res) => {
const kws = req.game.keywords || []
const idx = kws.findIndex(k => k.id === req.params.id)
if (idx === -1) return res.status(404).json({ error: 'Keyword not found' })
kws.splice(idx, 1)
saveDB(req.db)
res.json({ ok: true })
})
app.listen(PORT, () => console.log(`Colony Archive running on http://localhost:${PORT}`))