fading-echoes-of-difference.../exploration.html

73 lines
No EOL
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fading Echoes of Difference — 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 — with seeded tag mappings\n\n### media\n\n- `/avatar` — upload profile picture (PNG only)\n- `/upload` — upload PNG (5MB), MP3 (10MB), MP4 (25MB)\n- PNG pipeline: original (max 1MB), thumbnail (80px), large (800px), ASCII art\n- `/attach <media_id>` — attach media to a post (inline command)\n- `/view` — view original media (ASCII art + inline image)\n- `/play` / `/pause` — audio and video playback with terminal progress bar\n- `/expand media` — expand all media in view inline\n- `/expand nada` — collapse all inline media\n- `/expand <id>` — expand media from a specific post\n- `/rmedia <id>` — delete your uploaded media\n- Media deleted on account termination and post purge\n\n### hidden moderation (/kill)\n\n- `/kill @username` — hides user's content from your feed silently\n- `/kill <post_id>` — hides a specific post\n- filter_score decays over time (7-day half-life), threshold at 10\n- Users filtered by enough people get suppressed from everyone's feed — silently, no ban, no notification\n- The word \"kill\" never appears in the database or API — stored as \"filters\" and \"filter_score\"\n\n### bookmarks\n\n- `/bookmark <target>` — bookmark a user, tag, post, or doc\n- `/unbookmark <target>` — remove a bookmark\n- `/bookmarks` — view all bookmarks grouped by type\n- `/bookmarks --type` — filter by user, tag, post, or doc\n- Bookmarks are private — no counts, no notifications, nobody sees them\n- Bookmarked posts show [archived] or [expired] status when applicable\n- Deduplication — bookmarking the same thing twice is a no-op\n- Right-click context menu: bookmark/unbookmark on any interactive element\n\n### docs\n\n- `/read <name>` — inline markdown reader\n- `/read` — list available docs\n- Docs served from `public/docs/` as markdown files\n- Available: about, api, commands, milestone, roadmap, plugins, changelog, terms, support\n- Terms include GDPR (EU rights, Helsinki servers) and age requirement (16+)\n\n### account management\n\n- `/terminate-account` — permanent account deletion\n- Cascading wipe: bookmarks, posts, media files, sessions, credentials, user record\n- Username confirmation required\n- Username freed immediately\n\n### api\n\nAll endpoints return `{ ok: true, data: {...} }` or `{ ok: false, error: \"...\" }`.\n\n```\nPOST /api/auth/register\nPOST /api/auth/login\nPOST /api/auth/logout\nGET /api/auth/me\n\nGET /api/profile/search?q=\nGET /api/profile/:username\nPUT /api/profile\n\nPOST /api/filter\nGET /api/filter\n\nPOST /api/posts\nGET /api/posts/feed (public, auth optional)\nGET /api/posts/search?q=&archive=\nGET /api/posts/:id\nGET /api/posts/:id/thread\n\nPOST /api/avatar\nGET /api/avatar/:username\n\nPOST /api/media/upload\nGET /api/media/:id\nGET /api/media/:id/thumb\nGET /api/media/:id/large\nGET /api/media/:id/ascii\n\nPOST /api/bookmarks\nDELETE /api/bookmarks\nGET /api/bookmarks?type=\n\nGET /api/docs\nGET /api/docs/:name\n\nGET /api/categories\n\nDELETE /api/account\n\nGET /api/commands\n```\n\n### tech\n\n- Node.js + Express backend\n- libSQL (SQLite-compatible) database\n- Vanilla JS single-page app — no framework\n- System monospace fonts only — no external font loading\n- Session-based auth with cookie + bearer token support\n- API-first: every feature accessible through the API\n- Daily cron for post lifecycle management\n- Rate limiting on auth, posts, and uploads\n- Deploy script with DB backup and auto-rollback\n- Server: Hetzner Helsinki, systemd + nginx + certbot\n";
const passes = [{"t":0,"r":4,"c":59,"a":"hold","s":0.18960024994683297,"ps":9,"e":100.16680199957466,"pr":1.1},{"t":0,"r":154,"c":10,"a":"hold","s":0.21900182847725905,"ps":9,"e":100.40201462781808,"pr":1.1},{"t":0,"r":168,"c":9,"a":"hold","s":0.2061279488488965,"ps":9,"e":100.29902359079117,"pr":1.1},{"t":1,"r":4,"c":59,"a":"hold","s":0.23313084044720495,"ps":9,"e":100.6818487231523,"pr":1.1},{"t":1,"r":154,"c":10,"a":"hold","s":0.2195997030986797,"ps":8,"e":100.95881225260752,"pr":1.05},{"t":1,"r":168,"c":9,"a":"hold","s":0.2074757747222325,"ps":8,"e":100.75882978856903,"pr":1.05},{"t":2,"r":4,"c":59,"a":"retracted","s":0.23313084044720495,"ps":8,"e":101.34689544672995,"pr":1.05},{"t":2,"r":154,"c":10,"a":"retracted","s":0.21900182847725905,"ps":7,"e":101.66082688042559,"pr":1},{"t":2,"r":168,"c":9,"a":"retracted","s":0.2061279488488965,"ps":7,"e":101.35785337936021,"pr":1}];
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>