diff --git a/design.md b/design.md new file mode 100644 index 0000000..a41f7e3 --- /dev/null +++ b/design.md @@ -0,0 +1,97 @@ +# klbr.net Design System + +This project now uses a Signalis-inspired KLBR console language: black CRT surfaces, orange rails, white inset labels, gray instrument readouts, and dense operational panels. The goal is to make pages feel like usable in-world tools, not landing pages. + +## Core Tokens + +The global layout defines the shared base colors: + +| Token | CSS Variable | Value | Use | +| :--- | :--- | :--- | :--- | +| Accent Orange | `--klbr-accent` | `#e8530a` | Rails, borders, active controls, warning UI | +| Accent Glow | `--klbr-accent-glow` | `rgba(232, 83, 10, 0.4)` | Subtle CRT glow | +| Background | `--klbr-bg` | `#0a0a0a` | Page shell | +| Foreground | `--klbr-fg` | `#e8530a` | Default interface text | +| Panel Background | `--klbr-panel-bg` | `#000000` | Terminal interiors | +| Alert Red | `--klbr-alert` | `#e60012` | Critical alerts only | +| Amber | `--klbr-amber` | `#e58c00` | Secondary warnings | + +Page-local Signalis components may define matching aliases such as `--sig-orange`, `--sig-line`, `--sig-panel`, and `--sig-muted`. + +## CRT Treatment + +All pages inherit the global CRT pass from `Layout.astro`: + +- `body::after` draws horizontal scanlines plus a faint subpixel mask. +- `body::before` draws a viewport vignette. +- `body` applies slight blur, lower contrast, and saturation lift for an analog display feel. + +Full-screen dashboards add their own panel-local overlays: + +- repeating horizontal scanlines +- subtle vertical pixel columns +- radial corner falloff +- transparent outer casing/corner masks + +Do not add decorative blobs, gradients, or marketing-style hero sections. The visual density should come from instrument structure. + +## Layout Rules + +- Use full-screen pages for actual consoles or tools, via `Layout fullScreen={true}`. +- Use the default terminal shell only for small document pages that do not need a full instrument layout. +- Prefer three-panel or two-panel operational grids over centered cards. +- Panel sections use orange top rails and white inset title labels. +- Cards are only for repeated records, framed readouts, or modal/tool surfaces. Avoid cards inside cards. +- Text must stay within fixed-format controls at desktop and mobile sizes. + +## Receiver Dashboard + +`src/components/RadioReceiver.svelte` is the canonical implementation of the KLBR receiver console. + +Key constraints: + +- The receiver frequency value stays inside its bordered display. +- `FREQ` and `KHZ` sit outside the lower left/right edges of the frequency box. +- The tuner scale labels are fixed; only the white dial marker moves. +- FFT bars use gray placeholder columns with white signal fill on top. No orange fill bars. +- Side buttons are compact, with large labels nearly touching the button edges. +- The transcription section grows to absorb spare receiver height so `TRANSMIT` sits near the lower panel edge. +- Diagnostics status uses `NOMINAL`, `WARNING`, or `TESTING`. Do not use `LOCKED`. +- Do not add LSTR logo text or unit identification blocks. + +Audio is generated with Web Audio rather than bundled game samples: + +- tuning click and scrape +- weak-signal crackle +- signal-dependent static filtering +- carrier pulses near strong signals +- lock/warning cue, burst noise, and transmit confirmation + +## Diagnostics Footer + +The lower diagnostics cluster follows the screenshot structure: + +- left: five bio bars with gray/white fill +- center: cyan/orange clipped status readout +- right: small diagonal scope box +- below: centered time and date readout + +Keep the status readout terse. It should show only `STATUS` plus `NOMINAL`, `WARNING`, or `TESTING`. + +## Spool Page + +`src/pages/spool/index.astro` uses the same material language as a compact document page, not a full dashboard: + +- printer image and short device summary +- submit-record instructions and code block +- allowlist/source notes + +The page is documentation first. Keep it quieter than the receiver while retaining orange labels, black surfaces, CRT texture, and fixed-width terminal rhythm. +It should use a narrower, slightly taller terminal proportion than the receiver. On desktop, use a two-column shape: the printer image is the dominant full-height left column, while the device summary and submit-record instructions stack in the narrower right column. The submit panel should stretch to meet the bottom of the image column. Avoid extra panels, large hero text, or stacked sections that force scrolling. + +## Responsive Behavior + +- Full-screen consoles are constrained to viewport height on desktop and become vertical stacks on mobile. +- Use `min-width: 0` and `min-height: 0` on grid/flex children that may shrink. +- Use `clamp()` for fixed-format panel dimensions, not viewport-scaled typography everywhere. +- Do not let marquee bars, clocks, or panel footers overlap main panel borders. diff --git a/flake.lock b/flake.lock index 84905bc..6221f9b 100644 --- a/flake.lock +++ b/flake.lock @@ -17,11 +17,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1781359544, + "narHash": "sha256-iUuzKQcyXvopYDDzFpMK5eQKP3WIJExYny2kJtbgUcE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "9f11f828c213641c2369a9f1fa31fe31557e3156", "type": "github" }, "original": { diff --git a/radio-backend/Dockerfile b/radio-backend/Dockerfile new file mode 100644 index 0000000..e9bb4d7 --- /dev/null +++ b/radio-backend/Dockerfile @@ -0,0 +1,8 @@ +FROM oven/bun:1.1.20-alpine +WORKDIR /app +COPY package.json ./ +RUN bun install +COPY . . +ENV PORT=8080 +EXPOSE 8080 +CMD ["bun", "run", "index.ts"] diff --git a/radio-backend/bun.lock b/radio-backend/bun.lock new file mode 100644 index 0000000..75805d7 --- /dev/null +++ b/radio-backend/bun.lock @@ -0,0 +1,21 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "klbr-radio-backend", + "devDependencies": { + "@types/bun": "^1.1.4", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + } +} diff --git a/radio-backend/fly.toml b/radio-backend/fly.toml new file mode 100644 index 0000000..04be905 --- /dev/null +++ b/radio-backend/fly.toml @@ -0,0 +1,21 @@ +app = "klbr-radio" +primary_region = "ams" + +[build] + dockerfile = "Dockerfile" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "stop" + auto_start_machines = true + min_machines_running = 0 + processes = ["app"] + +[[vm]] + size = "shared-cpu-1x" + memory = "256mb" + +[mounts] + source = "radio_db" + destination = "/data" diff --git a/radio-backend/index.ts b/radio-backend/index.ts new file mode 100644 index 0000000..6b10106 --- /dev/null +++ b/radio-backend/index.ts @@ -0,0 +1,110 @@ +import { Database } from "bun:sqlite"; +import { existsSync } from "fs"; + +// Use persistent /data volume on Fly.io if exists, otherwise local folder +const dbDir = existsSync("/data") ? "/data" : "."; +const dbPath = `${dbDir}/radio.db`; + +console.log(`Initializing database at: ${dbPath}`); +const db = new Database(dbPath); + +// Create Table and Indexes +db.run(` + CREATE TABLE IF NOT EXISTS broadcasts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + callsign TEXT NOT NULL, + text TEXT NOT NULL, + frequency REAL NOT NULL, + created_at INTEGER NOT NULL + ) +`); +db.run(`CREATE INDEX IF NOT EXISTS idx_broadcasts_created_at ON broadcasts(created_at)`); + +const headers = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + "Content-Type": "application/json", +}; + +const server = Bun.serve({ + port: process.env.PORT || 8080, + async fetch(req) { + const url = new URL(req.url); + + // CORS Preflight + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers }); + } + + // Health Check + if (url.pathname === "/health") { + return new Response(JSON.stringify({ status: "ok" }), { status: 200, headers }); + } + + // GET /broadcasts - returns active signals in the last 7 days + if (url.pathname === "/broadcasts" && req.method === "GET") { + try { + const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + + // Auto-prune old records + db.run("DELETE FROM broadcasts WHERE created_at < ?", [oneWeekAgo]); + + const stmt = db.prepare("SELECT * FROM broadcasts WHERE created_at >= ? ORDER BY created_at DESC"); + const rows = stmt.all(oneWeekAgo); + return new Response(JSON.stringify(rows), { status: 200, headers }); + } catch (err: any) { + console.error("GET error:", err); + return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); + } + } + + // POST /broadcasts - submits a new signal + if (url.pathname === "/broadcasts" && req.method === "POST") { + try { + const body: any = await req.json(); + const callsign = (body.callsign || "").trim(); + const text = (body.text || "").trim(); + const frequency = parseFloat(body.frequency); + + if (!callsign || !text || isNaN(frequency)) { + return new Response(JSON.stringify({ error: "Missing required fields" }), { status: 400, headers }); + } + + if (callsign.length > 50 || text.length > 280) { + return new Response(JSON.stringify({ error: "Input exceeds maximum length limits" }), { status: 400, headers }); + } + + if (frequency < 50.0 || frequency > 300.0) { + return new Response(JSON.stringify({ error: "Frequency must be between 050.000 and 300.000" }), { status: 400, headers }); + } + + const now = Date.now(); + const stmt = db.prepare("INSERT INTO broadcasts (callsign, text, frequency, created_at) VALUES (?, ?, ?, ?)"); + const info = stmt.run(callsign, text, frequency, now); + + const newRecord = { + id: info.lastInsertRowid, + callsign, + text, + frequency, + created_at: now, + }; + + // Instantly prune old records to maintain low storage footprint + const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + db.run("DELETE FROM broadcasts WHERE created_at < ?", [oneWeekAgo]); + + console.log(`New broadcast added: ${callsign} @ ${frequency.toFixed(3)} MHz`); + return new Response(JSON.stringify(newRecord), { status: 201, headers }); + } catch (err: any) { + console.error("POST error:", err); + return new Response(JSON.stringify({ error: err.message }), { status: 500, headers }); + } + } + + return new Response(JSON.stringify({ error: "Route not found" }), { status: 404, headers }); + }, +}); + +console.log(`Bioresonance radio backend listening on port ${server.port}`); diff --git a/radio-backend/package.json b/radio-backend/package.json new file mode 100644 index 0000000..70ca3ce --- /dev/null +++ b/radio-backend/package.json @@ -0,0 +1,12 @@ +{ + "name": "klbr-radio-backend", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": {}, + "devDependencies": { + "@types/bun": "^1.1.4" + } +} diff --git a/radio-backend/radio.db b/radio-backend/radio.db new file mode 100644 index 0000000000000000000000000000000000000000..c1acb4281da3b2cea915aeba4af8c2d554bf631d GIT binary patch literal 16384 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCU|@k_1}I=c<1<3V@lh-cdcD7R85kIt z`4SlTRroIPCE!y&DmNMeqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?R70SEnO)po zk+G3CGcP5zA~U5TKB*`_F(o;%xTH8fxhOTUBsC>Iu>>K=;vD4a7~-nn>F44ap@36^ zg1?^vLX!rkSyq@2{^!ko;K)cE4m!qU{dWOObw)UFUmCm&Y@G_@Le ziMgpd#i@mwjggG(;);rlO|@V%5!NDDOeiKH#1%BS6ciK`GE)>h{X$&bU4s+?f;@d4 zgCZ5YT_Y77Lqq&M{hWhbeO>)RbigW;6LWHkGt=`FLR=$46#V={6#PPcd?0cqsTC#I z#M6o(-m6pya&`1U(}V~axD#Neaw#Zig8Jr6{CgSr@AK~+s%dQ0-J>Be8UmvsFd71* zAut*OqaiRF0;3@?8UmvsFd71*Auy~%K!J&kL0*`h)!WA@NH^Hjz|=9^(IKqFb61?H zJ>$HSRXKCy8QB;VrJ0~A%uLKo6U)JnVFBa3le#LMp#DE2{|5&C55qd7Mm;wg0;3@? z8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd70QDg^i#nH3ok>l2u`7#J9s_+K;df8&2W zq5@*nv!fv}8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Auv2cfQy-x5wa?PiIbU? I6Egn~0K)OH`v3p{ literal 0 HcmV?d00001 diff --git a/src/components/Marquee.astro b/src/components/Marquee.astro new file mode 100644 index 0000000..93a2386 --- /dev/null +++ b/src/components/Marquee.astro @@ -0,0 +1,91 @@ +--- +interface Props { + items: string[]; + direction?: "normal" | "reverse"; + speed?: string; + class?: string; +} + +const { items, direction = "normal", speed = "75s", class: className = "" } = Astro.props; +const separator = direction === "normal" ? "❖" : "◆"; +--- + +
+
+ {items.map((item) => ( + {item} + {separator} + ))} +
+ +
+ + diff --git a/src/components/RadioReceiver.svelte b/src/components/RadioReceiver.svelte new file mode 100644 index 0000000..111c8c1 --- /dev/null +++ b/src/components/RadioReceiver.svelte @@ -0,0 +1,3190 @@ + + +
+
+ + +
+
+
RECIEVER
+ +
+
+ {frequency.toFixed(3)} +
+
+ FREQ + KHZ +
+
+ +
+ 50 + 100 + 150 + 200 + 250 +
+ +
+
+ + + + + + +
+ +
+
+ + +
+
+ {#if decoderLock} + {#each decoderLock.frequencies as lockFrequency, i} + {#if decoderLock.lockedSlots[i]} + + {/if} + {/each} + {/if} + + +
+ +
+ + + +
+ + + + + + +
+
+
+ +
+
TRANSKRIPTION
+
+ {#if showDataDump} + DATA CACHE // DISCOVERED SIGNALS: {decoderRecords.length} + {#if decoderRecords.length === 0} + [NO LOCKED CARRIERS] + {:else} + {#each decoderRecords as record} + {@const recordLockedCount = getLockedCount(record)} +
+ {record.signal.callsign} // LOCK {recordLockedCount}/{REQUIRED_LOCK_COUNT} + {#each record.frequencies as lockFrequency, i} + {#if record.lockedSlots[i]} + {i === 0 ? "CARRIER" : "AUX-" + i} {lockFrequency.toFixed(3)} KHZ + {/if} + {/each} + {#if recordLockedCount >= REQUIRED_LOCK_COUNT} + {record.signal.text} + {:else} + MSG: UNDECIPHERED + {/if} +
+ {/each} + {/if} + {:else if transcriptionSignal} + {@const scrambledCall = scrambleText(transcriptionSignal.callsign, transcriptClarity, scrambleSeed)} + {@const scrambledTextVal = scrambleText(transcriptionSignal.text, transcriptClarity, scrambleSeed)} + {scrambledTextVal} + {scrambledCall} + {#if lockReadout} + {lockReadout} + {/if} + {:else} + [NO DATA FEED] + {/if} +
+
+ +
+
TRANSMIT
+
+ + + +
+
+ {feedbackMessage || "UPLINK READY // CURRENT CARRIER " + frequency.toFixed(3)} +
+
+ +
+ +
+
+
MEMORY
+ +
+ {#each serviceEndpoints as endpoint, i} + + {endpoint.label} + {endpoint.host} + + {/each} + {#each contactLinks as contact} + + {contact.label} Personnel Contact + {contact.value} + + {/each} +
+ +
+ +
+
+
DIAGNOSTICS
+ +
HYPERSENSITIVE
+
+
OVERCLUSTERED
+ +
FRAGMENTED
+
+
CATATONIC
+ +
+
+ {#each ["KOR", "ATR", "XTR", "P", "NP"] as stat, i} +
+ {stat} + +
+ {/each} +
+ +
0.78 && !decoderLock}> + STATUS + {decoderStatusText} +
+ + + +
+ {formatClock(currentTime)} + {formatDate(currentTime)} +
+
+ +
+
+ + {#if false} + + {/if} +
+ + diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index a699a3c..4d56cbf 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,8 +1,35 @@ --- +import Marquee from "../components/Marquee.astro"; + interface Props { title: string; + description?: string; + image?: string; + fullScreen?: boolean; } -const { title } = Astro.props; +const { + title, + description = "klbr.net host services and data endpoints", + image = "/klbr-pets-by-rotgutd-on-twt.gif", + fullScreen = false +} = Astro.props; + +const topMarqueeItems = [ + "KLBR-S2301 // VERBINDUNGS-PROTOKOLL 4.3.4 // BIORESONANZ-KONTROLLSTRUKTUR INTAKT", + "ACHTUNG // GESTALT COGNITIVE FLUCTUATION DETECTED", + "JENSEITS DER SONNE, IN DER DUNKELHEIT HINTER DEN STERNEN SAH ICH DAS GESICHT EINES GOTTES", + "DIE SECHS SEITEN DER WELT SAH ICH ENTFALTEN UND DAS LICHT VERSCHLANG MICH", + "[ SYSTEM STATUS: NOMINAL ]", + "ACHTUNG ACHTUNG // UMERZIEHUNGSANLAGE S-23 // BIORESONANCE CADRE ACTIVE" +]; + +const bottomMarqueeItems = [ + "UND ICH SAH: DAS SECHSTE SIEGEL BRACH UND DIE ERDE BEBTE UND DIE SONNE WURDE SCHWARZ", + "WER BIST DU? // WARUM BIST DU HIER? // GIB AUF // ES IST HOFFNUNGSLOS // STIRB", + "SELTSAM IST DIE NACHT WO DUNKLE STERNE AUFGEHEN, UND SELTSAME MONDE DURCH DEN HIMMEL KREISEN", + "LIEDER WELCHE DIE HYADEN SINGEN SOLLEN, WO DIE LUMPEN DES KÖNIGS WEHEN MÜSSEN UNGEHÖRT STERBEN", + "[ RECEIVER TENSION: NOMINAL ] // [ BIO-EFFICIENCY: 100% ]" +]; --- @@ -11,57 +38,342 @@ const { title } = Astro.props; {title} + + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+ + + + +
+ {fullScreen ? ( + + ) : ( +
+
+ 【 {title} 】 + SYS_VER // 4.3.4 // LENG_S23 +
+
+ +
+ +
+ )} +
+ + +
+ + + diff --git a/src/pages/index.astro b/src/pages/index.astro index c95b8e4..0d67e94 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,29 +1,8 @@ --- -import AutoLink from "../components/AutoLink.astro"; import Layout from "../layouts/Layout.astro"; +import RadioReceiver from "../components/RadioReceiver.svelte"; --- - -
-
-hi there~
-
-you have reached klbr.net. this host operates
-several data endpoints and services. see below.
-we hope they will be of help to you ^^;
-
-//make use of/
-  /file upload:      drop.klbr.net/
-  /slingshot:          sl.klbr.net/
-  /atproto relay:   relay.klbr.net/
-  /atproto spool:   spool.klbr.net/
-  /did:plc mirror:    plc.klbr.net
-   coverage:  compare.plc.klbr.net/
-
-//reach out/
-  /bsky           @klbr.net/
-  /email     90008@klbr.net/
-  
- -
+ + diff --git a/src/pages/spool/index.astro b/src/pages/spool/index.astro index 39139aa..d498779 100644 --- a/src/pages/spool/index.astro +++ b/src/pages/spool/index.astro @@ -1,36 +1,218 @@ --- import AutoLink from "../../components/AutoLink.astro"; import Layout from "../../layouts/Layout.astro"; ---- - -
-
- -
-  this is a physical device. be nice to it.
-    
-
-
-you are currently downloading the documentation
-for atspool, a receipt printer interface for the
-atproto network. it speaks the net.klbr.spool
-lexicon and prints data on paper.
-
-//submit a job/
-create this record:
-{
+const jobRecord = `{
   "$type": "net.klbr.spool.job",
-  "content": {
+  "content": {
     "$type": "net.klbr.spool.job.content.text",
     "text": "your message here"
-  }
-}
-the record key should be a TID.
-
-//info/
-  /there is an allowlist, mention/dm klbr.net on bsky/
-  /source: tangled.org/ptr.pet/atspool/
-  
-
+ } +}`; +--- + + +
+
+ receipt printer connected to atspool +
+ +
+
+

ATPROTO PRINT DEVICE

+

+ A physical receipt printer interface for the AT Protocol network. It speaks + net.klbr.spool and prints data on paper. +

+ +
+
QUEUE
ALLOWLIST
+
RKEY
TID
+
SOURCE
tangled.org/ptr.pet/atspool
+
+
+ +
+

SUBMIT A JOB

+

Create this record. Mention or DM klbr.net on bsky for allowlist access.

+
{jobRecord}
+

This is a physical device. Be nice to it.

+
+
+
+ +