Config files for my server. Except not my secrets

rework landing to be a lil more robust + remove the text gradient bc workin emojis is better lmao

vielle.dev 76e50f1f 3dfaaece

verified
+2
compose.yaml
··· 10 10 11 11 landing: 12 12 build: ./landing 13 + environment: 14 + - PORT=8000 13 15 restart: unless-stopped 14 16 15 17 caddy:
+2 -1
landing/Dockerfile
··· 2 2 WORKDIR /app 3 3 4 4 COPY ./ /app 5 + RUN deno install 5 6 RUN deno bundle --unstable-raw-imports --output bundle.js /app/landing.ts 6 7 7 - CMD deno --allow-net bundle.js 8 + CMD deno --allow-net --unstable-temporal bundle.js
-66
landing/css.css
··· 1 - @property --gradient-offset { 2 - syntax: "<length>"; 3 - inherits: false; 4 - initial-value: 0px; 5 - } 6 - 7 - @keyframes scroll { 8 - from { 9 - --gradient-offset: 0px; 10 - } 11 - 12 - to { 13 - --gradient-offset: var(--gradient-gap); 14 - } 15 - } 16 - 17 - html { 18 - background: repeating-linear-gradient(#fff1, #0000 5px, #fff1 10px), black; 19 - min-height: 100%; 20 - font-family: "Monaspace neon var", monospace; 21 - font-weight: bold; 22 - } 23 - 24 - html, 25 - body { 26 - margin: 0; 27 - } 28 - 29 - pre { 30 - /* config */ 31 - --light: lime; 32 - --dark: green; 33 - --gradient-gap: 10px; 34 - --gradient-size: 5px; 35 - --gradient-offset: 0px; 36 - --gradient-max: calc(var(--gradient-gap) * 2 + var(--gradient-size)); 37 - 38 - /* override some resource://content-accessible/plaintext.css styles in ff */ 39 - white-space: pre !important; 40 - width: max-content; 41 - padding: 1em; 42 - margin-block: 0; 43 - padding-block-start: 0; 44 - 45 - text-shadow: 0 0 5px lime; 46 - color: lime; 47 - @supports (background-clip: text) { 48 - color: transparent; 49 - background: repeating-linear-gradient( 50 - var(--light), 51 - var(--light) calc(var(--gradient-gap) - var(--gradient-offset)), 52 - var(--dark) calc(var(--gradient-gap) - var(--gradient-offset)), 53 - var(--dark) 54 - calc( 55 - var(--gradient-gap) - var(--gradient-offset) + var(--gradient-size) 56 - ), 57 - var(--light) 58 - calc( 59 - var(--gradient-gap) - var(--gradient-offset) + var(--gradient-size) 60 - ), 61 - var(--light) var(--gradient-max) 62 - ) 63 - text; 64 - animation: 10s infinite scroll linear; 65 - } 66 - }
+5
landing/deno.json
··· 1 + { 2 + "imports": { 3 + "zod": "npm:zod@^4.1.13" 4 + } 5 + }
+16
landing/deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:zod@^4.1.13": "4.1.13" 5 + }, 6 + "npm": { 7 + "zod@4.1.13": { 8 + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==" 9 + } 10 + }, 11 + "workspace": { 12 + "dependencies": [ 13 + "npm:zod@^4.1.13" 14 + ] 15 + } 16 + }
+1
landing/heading.txt
··· 13 13 Setup Guide: https://vielle.dev/blog/pds-on-a-pi/ 14 14 Protocol: https://atproto.com/ 15 15 .dong file: https://dongs.zip/ 16 + health:
+156
landing/index.ts
··· 1 + import css from "./styles.css" with { type: "text" }; 2 + import heading from "./heading.txt" with { type: "text" }; 3 + import { 4 + didDocSchema, 5 + getRecordSchema, 6 + listRecordsSchema, 7 + listReposSchema, 8 + profileSelfSchema, 9 + statusphereSchema, 10 + tealFmStatusSchema, 11 + } from "./schemas.ts"; 12 + 13 + const PORT = Number(Deno.env.get("PORT")); 14 + const PDS = Deno.env.get("PDS") ?? "http://pi:8000"; 15 + 16 + async function renderUser(did: string): Promise<string> { 17 + const handle = fetch( 18 + did.startsWith("did:plc") 19 + ? "https://plc.directory/" + did 20 + : `https://${did.replace("did:web:", "")}/.well-known/did.json` 21 + ) 22 + .then((res) => res.json()) 23 + .then((res) => didDocSchema.safeParse(res).data) 24 + .then( 25 + (doc) => 26 + doc.alsoKnownAs 27 + .filter((x) => x.startsWith("at://"))[0] 28 + ?.replace("at://", "") ?? "invalid.handle" 29 + ); 30 + 31 + const displayName = fetch( 32 + `${PDS}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.actor.profile&rkey=self` 33 + ) 34 + .then((res) => res.json()) 35 + .then((data) => getRecordSchema(profileSelfSchema).safeParse(data).data) 36 + .then((data) => (data ? data.value.displayName : undefined)); 37 + 38 + const statusphere = fetch( 39 + `${PDS}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=xyz.statusphere.status` 40 + ) 41 + .then((res) => res.json()) 42 + .then((data) => listRecordsSchema(statusphereSchema).safeParse(data).data) 43 + .then((data) => 44 + data && data.records.length > 0 ? data.records[0].value.status : undefined 45 + ); 46 + 47 + const nowPlaying = fetch( 48 + `${PDS}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=fm.teal.alpha.actor.status&rkey=self` 49 + ) 50 + .then((res) => res.json()) 51 + .then((data) => getRecordSchema(tealFmStatusSchema).safeParse(data).data) 52 + .then((data) => 53 + data 54 + ? data.value.item.trackName + 55 + ((data.value.item.artists.length > 0 56 + ? ` - ${data.value.item.artists[0].artistName}` 57 + : "") + 58 + (data.value.item.artists.length > 1 59 + ? `${data.value.item.artists.length - 1} more artist${data.value.item.artists.length > 1 ? "s" : ""}` 60 + : "")) 61 + : undefined 62 + ); 63 + 64 + return ( 65 + "- " + 66 + [ 67 + (await statusphere) 68 + ? `${await displayName} (${await statusphere})` 69 + : await displayName, 70 + 71 + `@${await handle}`, 72 + `at://${did}`, 73 + await nowPlaying, 74 + ] 75 + .filter((x) => x) 76 + .join("\n ") 77 + ); 78 + } 79 + 80 + function landingPage(): Response { 81 + // get upstream pds status 82 + const status = fetch(`${PDS}/xrpc/_health`).then( 83 + async (res) => `${res.status} ${res.statusText} ${await res.text()}` 84 + ); 85 + 86 + const users = fetch(`${PDS}/xrpc/com.atproto.sync.listRepos`) 87 + .then((res) => res.json()) 88 + .then((data) => listReposSchema.safeParse(data).data) 89 + .then((data) => 90 + data ? data.repos.filter((x) => x.active).map((x) => x.did) : undefined 91 + ) 92 + .then(async (users) => 93 + users 94 + ? await Promise.all(users.map((did) => renderUser(did))).then((x) => 95 + x.join("\n\n") 96 + ) 97 + : undefined 98 + ); 99 + 100 + const body = new ReadableStream({ 101 + async start(controller: ReadableStreamDefaultController<string>) { 102 + controller.enqueue(heading); 103 + controller.enqueue(await status); 104 + controller.enqueue("\n\n"); 105 + controller.enqueue(await users); 106 + controller.close(); 107 + }, 108 + }); 109 + 110 + // buffer the output just to be easier to handle 111 + const stagger = new TransformStream({ 112 + async transform( 113 + chunk: string, 114 + controller: TransformStreamDefaultController<string> 115 + ) { 116 + for (const part of chunk) { 117 + controller.enqueue(part); 118 + await new Promise((res) => setTimeout(res, 10)); 119 + } 120 + }, 121 + }); 122 + 123 + return new Response( 124 + body.pipeThrough(stagger).pipeThrough(new TextEncoderStream()), 125 + { 126 + headers: { 127 + "Content-Type": "text/text; charset=utf-8", 128 + "Access-Control-Allow-Origin": "*", 129 + Link: "</styles.css>; rel=stylesheet", 130 + }, 131 + } 132 + ); 133 + } 134 + 135 + Deno.serve( 136 + { port: !Number.isNaN(PORT) && PORT <= 65535 && PORT > 0 ? PORT : 8000 }, 137 + (req) => { 138 + const path = new URL(req.url).pathname; 139 + switch (path) { 140 + case "/": 141 + return landingPage(); 142 + case "/styles.css": 143 + return new Response(css, { 144 + headers: { 145 + "Content-Type": "text/css; charset=utf-8", 146 + "Access-Control-Allow-Origin": "*", 147 + }, 148 + }); 149 + default: 150 + return new Response(`404 Not Found: ${path} does not exist`, { 151 + status: 400, 152 + statusText: "Not Found", 153 + }); 154 + } 155 + } 156 + );
-127
landing/landing.ts
··· 1 - import css from "./css.css" with { type: "text" }; 2 - import heading from "./heading.txt" with { type: "text" }; 3 - 4 - Deno.serve({ port: 8000 }, (req) => { 5 - switch (new URL(req.url).pathname) { 6 - case "/css": 7 - return new Response(css, { 8 - headers: { 9 - "Content-Type": "text/css; charset=utf-8", 10 - "Access-Control-Allow-Origin": "*", 11 - }, 12 - }); 13 - case "/": { 14 - const body = new ReadableStream({ 15 - async start(controller) { 16 - // get upstream pds status 17 - const status = fetch("http://pi:8000/xrpc/_health").then( 18 - async (res) => 19 - `${res.status} ${res.statusText}: ${await res.text()}` 20 - ); 21 - 22 - // get list of users; 23 - // get from pi since it's more reliable than external 24 - // not awaited so it runs in background while streaming 25 - const users = fetch("http://pi:8000/xrpc/com.atproto.sync.listRepos") 26 - // type cast because no point validating for smthn like this 27 - // real type has more info; not needed here 28 - .then((res) => res.json() as Promise<{ repos: { did: string }[] }>) 29 - .then((res) => 30 - // get display name, handle, and did for each user 31 - res.repos.map((repo) => ({ 32 - display: fetch( 33 - `http://pi:8000/xrpc/com.atproto.repo.getRecord?repo=${repo.did}&collection=app.bsky.actor.profile&rkey=self` 34 - ) 35 - .then((res) => res.json()) 36 - .then((profile) => profile?.value?.displayName ?? repo.did), 37 - // dont validate handles because I'm Lazy + trust myself 38 - handle: fetch( 39 - repo.did.startsWith("did:plc") 40 - ? "https://plc.directory/" + repo.did 41 - : `https://${repo.did.replace("did:web:", "")}/.well-known/did.json` 42 - ) 43 - .then((res) => res.json()) 44 - .then((doc) => doc.alsoKnownAs[0].replace("at://", "")), 45 - did: repo.did, 46 - statusphere: fetch( 47 - `http://pi:8000/xrpc/com.atproto.repo.listRecords?repo=${repo.did}&collection=xyz.statusphere.status` 48 - ) 49 - .then( 50 - (res) => 51 - res.json() as Promise<{ 52 - records: { 53 - cid: string; 54 - value: { 55 - status: string; 56 - createdAt: string; 57 - }; 58 - }[]; 59 - }> 60 - ) 61 - .then(({ records }) => 62 - records.map((x) => ({ 63 - cid: x.cid, 64 - value: { 65 - ...x.value, 66 - createdAt: new Date(x.value.createdAt), 67 - }, 68 - })) 69 - ) 70 - .then((x) => 71 - x 72 - .sort( 73 - (a, b) => 74 - (b as unknown as number) - (a as unknown as number) 75 - ) 76 - .at(0) 77 - ) 78 - .then((x) => x?.value?.status), 79 - })) 80 - ) 81 - .then(async (users) => 82 - ( 83 - await Promise.all( 84 - users.map( 85 - async (x) => 86 - ` 87 - - ${await x.display} 88 - ${await x.handle} ${(await x.statusphere) ? `(${await x.statusphere})` : ""} 89 - ${x.did}` 90 - ) 91 - ) 92 - ).join("\n") 93 - ); 94 - 95 - for (const char of heading) { 96 - await new Promise((res) => setTimeout(res, 10)); 97 - controller.enqueue(char); 98 - } 99 - 100 - for (const char of "\n/xrpc/_health/ " + (await status) + "\n") { 101 - await new Promise((res) => setTimeout(res, 10)); 102 - controller.enqueue(char); 103 - } 104 - 105 - for (const char of "\nAccounts:" + (await users)) { 106 - await new Promise((res) => setTimeout(res, 10)); 107 - controller.enqueue(char); 108 - } 109 - 110 - controller.close(); 111 - }, 112 - }); 113 - 114 - return new Response(body.pipeThrough(new TextEncoderStream()), { 115 - headers: { 116 - "Content-Type": "text/text; charset=utf-8", 117 - "Access-Control-Allow-Origin": "*", 118 - Link: "</css>; rel=stylesheet", 119 - }, 120 - }); 121 - } 122 - } 123 - 124 - return new Response("404", { 125 - status: 404, 126 - }); 127 - });
+49
landing/schemas.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const listReposSchema = z.object({ 4 + repos: z.array(z.object({ did: z.string(), active: z.boolean() })), 5 + }); 6 + 7 + export const didDocSchema = z.object({ 8 + alsoKnownAs: z.array(z.string()), 9 + }); 10 + 11 + export const profileSelfSchema = z.object({ 12 + $type: z.literal("app.bsky.actor.profile"), 13 + displayName: z.string().optional(), 14 + }); 15 + 16 + export const statusphereSchema = z.object({ 17 + $type: z.literal("xyz.statusphere.status"), 18 + status: z.string(), 19 + }); 20 + 21 + export const tealFmStatusSchema = z.object({ 22 + time: z.string(), 23 + item: z.object({ 24 + trackName: z.string(), 25 + artists: z.array( 26 + z.object({ 27 + artistName: z.string(), 28 + }) 29 + ), 30 + }), 31 + }); 32 + 33 + export function getRecordSchema<T>(record: T) { 34 + return z.object({ 35 + value: record, 36 + }); 37 + } 38 + 39 + export function listRecordsSchema<T>(record: T) { 40 + return z.object({ 41 + records: z.array( 42 + z.object({ 43 + uri: z.string(), 44 + cid: z.string(), 45 + value: record, 46 + }) 47 + ), 48 + }); 49 + }
+30
landing/styles.css
··· 1 + html { 2 + background: repeating-linear-gradient(#fff1, #0000 5px, #fff1 10px), black; 3 + min-height: 100%; 4 + font-family: 5 + "Monaspace neon var", 6 + -moz-fixed, 7 + monospace; 8 + font-weight: bold; 9 + } 10 + 11 + html, 12 + body { 13 + margin: 0; 14 + } 15 + 16 + pre { 17 + /* override some resource://content-accessible/plaintext.css styles in ff */ 18 + white-space: pre !important; 19 + width: max-content; 20 + padding: 1em; 21 + margin-block: 0; 22 + padding-block-start: 0; 23 + text-shadow: 0 0 5px lime; 24 + color: lime; 25 + 26 + &::selection { 27 + color: black; 28 + background-color: lime; 29 + } 30 + }