73 lines
855 KiB
HTML
73 lines
855 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Pulsing Neural Constellations — exploration</title>
|
|||
|
|
<style>
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
body { background: #0a0a0f; color: #3a3a4a; font-family: ui-monospace, monospace; overflow: hidden; }
|
|||
|
|
#grid { padding: 20px; font-size: 12px; line-height: 1.4; white-space: pre; }
|
|||
|
|
.c { display: inline-block; transition: all 0.3s; }
|
|||
|
|
.c.active { color: #fde68a; text-shadow: 0 0 6px rgba(253,230,138,0.5); }
|
|||
|
|
.c.strong { color: #34d399; text-shadow: 0 0 10px rgba(52,211,153,0.6); font-weight: bold; }
|
|||
|
|
.c.dead { color: #1a1a2a; }
|
|||
|
|
#info { position: fixed; bottom: 10px; left: 20px; color: #3a3a4a; font-size: 11px; }
|
|||
|
|
#controls { position: fixed; top: 10px; right: 20px; color: #666; font-size: 11px; }
|
|||
|
|
#controls span { cursor: pointer; margin-left: 12px; color: #fde68a; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div id="grid"></div>
|
|||
|
|
<div id="info">neurameba · physarum exploration</div>
|
|||
|
|
<div id="controls"><span id="play-btn">play</span><span id="reset-btn">reset</span></div>
|
|||
|
|
<script>
|
|||
|
|
const text = "# motd — milestone\n\n## what's built\n\nTerminal-aesthetic social platform. Everything happens in a terminal UI — no chrome, no nav bars. Commands in, text out.\n\n### the shell\n\n- Full terminal interface in the browser — dark background, monospace, command input at the bottom\n- Centered terminal layout, 800px default width\n- Drag-to-resize from left/right edges — invisible 8px hit zones, min 400px\n- `/terminal --width` — set width in px, %, or reset to default\n- Width persisted in localStorage\n- Mobile: 100% width, no resize\n- All visual settings (colors, font, prompt format, welcome message) driven by `config.json`\n- Client-side config system — localStorage overrides merged with server defaults\n- `/config export` — download config as JSON\n- `/config import` — load config from JSON file, validated\n- `/config reset` — restore server defaults\n- `/+` `/−` — font size increase/decrease (8-32px, persisted)\n- Command history with up/down arrow keys\n- Tab completion for all commands\n- Full sidebar — motd.social branding, commands, doc links, footer\n- Right-click context menu on interactive elements (usernames, tags, posts, doc links, media)\n- `/clear` wipes the terminal\n- `/help` and `/menu` show available commands\n- Favicon: m> SVG, dynamically generated from accent color\n- Guest landing shows last 5 posts on page load\n- 5 posts shown after login/register\n\n### auth\n\n- `/register` — interactive prompts, password masked via CSS (no browser autofill interference)\n- `/login` — session stored in sessionStorage (gone when tab closes)\n- `/login-permanently` — session stored in localStorage (persists)\n- `/logout` — clears session from both storages\n- Session restored on page load if valid\n- Browser password save triggered after successful login/register\n- Prompt updates: `guest@motd >` becomes `username@motd >`\n- Passwords hashed with bcrypt, validated (3-20 char usernames, 8+ char passwords)\n- Username blacklist: `motd*` prefix regex + fixed list of reserved names\n- Rate limiting: auth (20/15min), posts (30/min), uploads (10/min)\n- Auth via Bearer token in Authorization header\n\n### profiles\n\n- `/profile` — view your own profile with ASCII avatar and stats\n- `/profile @user` — view another user's profile\n- `/avatar` — upload profile picture (PNG), processed to ASCII art for terminal display\n- `/settings` — update display name, bio, confirm_post preference\n- `/find <query>` — search users by username or display name\n- ASCII avatars: uploaded image converted to ASCII, or deterministic hash pattern as fallback\n\n### posts & feed\n\n- Dual-purpose input: text without `/` is a post, text with `/` is a command\n- `confirm_post` preference (default on) — shows \"Post this? (y/n)\" before posting\n- `/post <text>` — always posts immediately, skips confirmation\n- `/feed` — chronological feed, public (no login required). Authenticated users get kill-filtered results, guests get unfiltered\n- `/reply <id> <text>` — reply with grey quote block of parent post\n- `/re <text>` — reply to last displayed post (no ID needed)\n- `/goto <id>` — jump to a specific post\n- Tags use [brackets]: `[rust]`, `[music]`, `[coding]`\n- Tags stripped from displayed content, shown only as clickable links below\n- Tags auto-extracted from post content and auto-assigned to categories\n- Short alphanumeric IDs (6-8 chars) for posts and media\n\n### post lifecycle\n\n- Active: posts visible in feed for 30 days\n- Archived: moved to archive table, searchable for 90 more days\n- Purged: permanently deleted after 120 days total\n- Cron job runs daily — archives, purges, cleans dead filter references\n- `/find --archive` searches archived posts\n\n### discovery\n\n- `/find <query>` — search users and posts\n- `/find --archive <query>` — search archived posts\n- `/tree -cat` — browse categories and tags with clickable bracket links\n- `/link <target>` — universal navigation (user, tag, post, doc)\n- Categories: coding, creative, meta, general — wi
|
|||
|
|
const passes = [{"t":0,"r":4,"c":59,"a":"extend","s":0.35111157397561665,"ps":9,"e":71.02122481426345,"pr":1.1},{"t":0,"r":154,"c":10,"a":"extend","s":0.3754317059610156,"ps":9,"e":71.15741755338168,"pr":1.1},{"t":0,"r":168,"c":9,"a":"hold","s":0.29446849835556643,"ps":9,"e":101.00574798684453,"pr":1.1},{"t":0,"r":43,"c":0,"a":"died","s":0,"ps":8,"e":100,"pr":1},{"t":0,"r":118,"c":11,"a":"extend","s":0.6067720083421946,"ps":9,"e":72.45292324671628,"pr":1.1},{"t":1,"r":4,"c":59,"a":"extend","s":0.3826775301282862,"ps":9,"e":50.91285153870281,"pr":1.1},{"t":1,"r":154,"c":10,"a":"extend","s":0.35278467177759115,"ps":8,"e":50.94578644932168,"pr":1.05},{"t":1,"r":168,"c":9,"a":"extend","s":0.5099072146465172,"ps":10,"e":72.50950399281166,"pr":1.2000000000000002},{"t":1,"r":118,"c":11,"a":"extend","s":0.7408796357663474,"ps":9,"e":53.92097223299294,"pr":1.1},{"t":1,"r":4,"c":58,"a":"hold","s":0.2409009542389613,"ps":5,"e":31.61487541145317,"pr":1.2000000000000002},{"t":1,"r":154,"c":9,"a":"extend","s":0.4629424436392628,"ps":5,"e":23.414702950394375,"pr":1.2000000000000002},{"t":1,"r":154,"c":11,"a":"extend","s":0.36647273751447107,"ps":5,"e":22.874472596095544,"pr":1.2000000000000002},{"t":1,"r":118,"c":10,"a":"extend","s":0.37039955303858085,"ps":5,"e":23.285114471030937,"pr":1.2000000000000002},{"t":2,"r":4,"c":59,"a":"extend","s":0.37370191812899317,"ps":8,"e":36.89172681861433,"pr":1.05},{"t":2,"r":154,"c":10,"a":"extend","s":0.3590279749408262,"ps":7,"e":36.9376071741938,"pr":1},{"t":2,"r":168,"c":9,"a":"extend","s":0.5043870778674234,"ps":9,"e":52.63622043102573,"pr":1.1500000000000001},{"t":2,"r":118,"c":11,"a":"extend","s":0.7067011653360474,"ps":8,"e":40.862207088976916,"pr":1.05},{"t":2,"r":4,"c":58,"a":"hold","s":0.23503232371790944,"ps":4,"e":32.895134001196446,"pr":1.1500000000000001},{"t":2,"r":154,"c":9,"a":"extend","s":0.43517940782877257,"ps":4,"e":18.407296749117187,"pr":1.1500000000000001},{"t":2,"r":154,"c":11,"a":"extend","s":0.4304821968147279,"ps":5,"e":17.897831119429355,"pr":1.2000000000000002},{"t":2,"r":118,"c":10,"a":"extend","s":0.3715501240207068,"ps":4,"e":17.960260824237615,"pr":1.1500000000000001},{"t":2,"r":4,"c":60,"a":"hold","s":0.20460003182847536,"ps":5,"e":22.706593771214724,"pr":1.2000000000000002},{"t":2,"r":155,"c":10,"a":"extend","s":0.5893012871867275,"ps":5,"e":18.05882314304218,"pr":1.1500000000000001},{"t":2,"r":168,"c":10,"a":"extend","s":0.4237566100449081,"ps":6,"e":23.495888214094983,"pr":1.3000000000000003},{"t":2,"r":119,"c":11,"a":"extend","s":0.43765706033468615,"ps":5,"e":18.102171207772123,"pr":1.2000000000000002},{"t":2,"r":118,"c":12,"a":"hold","s":0.23968517605479,"ps":5,"e":24.276469508292436,"pr":1.2000000000000002},{"t":2,"r":155,"c":9,"a":"extend","s":0.5516640199904088,"ps":5,"e":9.5887293970646,"pr":1.3000000000000003},{"t":2,"r":155,"c":11,"a":"extend","s":0.38290713298107437,"ps":5,"e":8.481621723522677,"pr":1.3000000000000003},{"t":2,"r":154,"c":12,"a":"extend","s":0.41737720086009267,"ps":5,"e":8.674654103645182,"pr":1.3000000000000003},{"t":2,"r":118,"c":9,"a":"extend","s":0.45823603055123546,"ps":5,"e":9.026656112396198,"pr":1.3000000000000003},{"t":3,"r":4,"c":59,"a":"hold","s":0.3277803990168132,"ps":8,"e":38.31397001074884,"pr":1.05},{"t":3,"r":154,"c":10,"a":"hold","s":0.2773009936946099,"ps":7,"e":38.10601512375068,"pr":1},{"t":3,"r":168,"c":9,"a":"extend","s":0.4783173223052009,"ps":8,"e":38.68393130662713,"pr":1.1},{"t":3,"r":118,"c":11,"a":"extend","s":0.5319805815403872,"ps":8,"e":30.742636218910008,"pr":1.05},{"t":3,"r":4,"c":58,"a":"retracted","s":0.23402178739012808,"ps":4,"e":34.16730830031747,"pr":1.1},{"t":3,"r":154,"c":9,"a":"extend","s":0.4379957201629969,"ps":4,"e":14.917883757294813,"pr":1.1},{"t":3,"r":154,"c":11,"a":"extend","s":0.41911331847009625,"ps":4,"e":14.455516367033086,"pr":1.1500000000000001},{"t":3,"r":118,"c":10,"a":"hold","s":0.35391048331298813,"ps":4,"e":20.19154469074152,"pr":1.1},{"t":3,"r":4,"c":60,"a":"hold","s":0.22308137879246018,"ps":4,"e":23.891244801554407,"pr":1.1500000000000001},{"t":3,"r":155,"c":10,"a":"ex
|
|||
|
|
const lines = text.split('\n');
|
|||
|
|
const gridEl = document.getElementById('grid');
|
|||
|
|
const charEls = [];
|
|||
|
|
for (let r = 0; r < lines.length; r++) {
|
|||
|
|
const row = [];
|
|||
|
|
for (let c = 0; c < lines[r].length; c++) {
|
|||
|
|
const s = document.createElement('span');
|
|||
|
|
s.className = 'c';
|
|||
|
|
s.textContent = lines[r][c];
|
|||
|
|
row.push(s);
|
|||
|
|
gridEl.appendChild(s);
|
|||
|
|
}
|
|||
|
|
charEls.push(row);
|
|||
|
|
gridEl.appendChild(document.createTextNode('\n'));
|
|||
|
|
}
|
|||
|
|
let tick = -1, playing = false, iv;
|
|||
|
|
function apply(t) {
|
|||
|
|
for (const r of charEls) for (const e of r) e.className = 'c';
|
|||
|
|
const active = new Map();
|
|||
|
|
for (const p of passes) {
|
|||
|
|
if (p.t > t) break;
|
|||
|
|
const k = p.r+','+p.c;
|
|||
|
|
if (p.a === 'died' || p.a === 'retracted') active.set(k, 'dead');
|
|||
|
|
else if (p.ps > 16) active.set(k, 'strong');
|
|||
|
|
else active.set(k, 'active');
|
|||
|
|
}
|
|||
|
|
for (const [k, cls] of active) {
|
|||
|
|
const [r, c] = k.split(',').map(Number);
|
|||
|
|
if (charEls[r]?.[c]) charEls[r][c].className = 'c ' + cls;
|
|||
|
|
}
|
|||
|
|
document.getElementById('info').textContent = 'tick ' + t + ' · ' + [...active.values()].filter(v=>v!=='dead').length + ' alive';
|
|||
|
|
}
|
|||
|
|
function play() {
|
|||
|
|
if (playing) return;
|
|||
|
|
playing = true;
|
|||
|
|
document.getElementById('play-btn').textContent = 'pause';
|
|||
|
|
const max = passes.length > 0 ? passes[passes.length-1].t : 0;
|
|||
|
|
iv = setInterval(() => { tick++; if (tick > max) { pause(); return; } apply(tick); }, 900);
|
|||
|
|
}
|
|||
|
|
function pause() { playing = false; clearInterval(iv); document.getElementById('play-btn').textContent = 'play'; }
|
|||
|
|
function reset() { pause(); tick = -1; for (const r of charEls) for (const e of r) e.className = 'c'; document.getElementById('info').textContent = 'neurameba'; }
|
|||
|
|
document.getElementById('play-btn').addEventListener('click', () => playing ? pause() : play());
|
|||
|
|
document.getElementById('reset-btn').addEventListener('click', reset);
|
|||
|
|
setTimeout(play, 1000);
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|