For now? I'm experimenting on an old concept.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Working webserver!


Signed-off-by: MLC Bloeiman <mar@strawmelonjuice.com>

+393 -487
+1 -1
.envrc
··· 1 1 if nix flake show &>/dev/null; then 2 2 use flake 3 3 fi 4 - export DATABASE_URL="postgres://lumina:lumina_pw@localhost:5432/lumina_config" 4 + export DATABASE_URL="sqlite3://$(pwd)/data/instance.db"
+6 -21
.gitignore
··· 1 1 *.beam 2 2 *.ez 3 3 /target/ 4 - 5 - # Ignore Gleam backend build artifacts 6 - backend/build 7 - client/priv/generated 8 - 9 - # Ignore Rust backend build artifacts 10 - backend-rs/target 11 - backend-rs/generated 12 - 13 - # Ignore Gleam frontend build artifacts 14 - frontend/prelude.mjs 15 - 16 4 erl_crash.dump 17 5 /test 18 6 *.log 19 7 package-lock.json 20 - rsffi/target 21 - rsffi/test 22 8 node_modules 23 9 # Ignore Editor files 24 10 .idea/ ··· 30 16 /target 31 17 32 18 # Added by me 33 - client/build/dev/javascript/lumina_client/lumina_client.ts 34 19 client/build/ 35 - /client/priv/static/lumina_client*.css 36 - /client/priv/static/lumina_client*.hash 37 - /client/priv/static/lumina_client*.mjs 38 - instance.sqlite 39 - instance.sqlite-shm 40 - instance.sqlite-wal 20 + server/build/ 21 + /server/priv/static/lumina_client*.css 22 + /server/priv/static/lumina_client*.hash 23 + /server/priv/static/lumina_client*.mjs 41 24 .env 42 25 /data 43 26 /built ··· 49 32 /notes/.obsidian/appearance.json 50 33 /notes/.obsidian/workspace.json 51 34 /notes/.obsidian/app.json 35 + 36 + # Direnv cache 52 37 .direnv/
+5
.tool-versions
··· 1 + bun 1.2.20 2 + gleam 1.15.0 3 + just latest 4 + watchexec latest 5 + dbmate latest
-37
Dockerfile
··· 1 - # syntax=docker/dockerfile:1 2 - 3 - FROM alpine:3.19 AS builder 4 - 5 - ENV MISE_DATA_DIR="/mise" 6 - ENV MISE_CONFIG_DIR="/mise" 7 - ENV MISE_CACHE_DIR="/mise/cache" 8 - ENV MISE_INSTALL_PATH="/usr/local/bin/mise" 9 - ENV BUN_INSTALL="/usr/local/bin/bun" 10 - ENV PATH="/usr/local/bin/bun/bin:/mise/shims:$PATH" 11 - 12 - RUN apk add --no-cache curl git unzip build-base bash 13 - 14 - # Install bun outside of mise because Alpine uses musl libc which the mise bun package does not support 15 - RUN curl -fsSL https://bun.sh/install | bash 16 - RUN curl https://mise.run | sh 17 - 18 - WORKDIR /build 19 - # Copy and install the mise.toml file first to leverage Docker cache 20 - COPY mise.toml ./mise.toml 21 - RUN mise trust && mise unuse bun && mise install 22 - # Copy the project files excluding mise.toml 23 - COPY --exclude=mise.toml . . 24 - # Build the project itself in release mode. 25 - RUN mkdir -p target/output && \ 26 - mise run build-server-release && \ 27 - cp ./target/release/lumina-server ./target/output/; 28 - 29 - 30 - # --- Final runtime image --- 31 - FROM alpine:3.19 32 - RUN apk add --no-cache ca-certificates 33 - WORKDIR /app 34 - COPY --from=builder /build/target/output/lumina-server /app/lumina-server 35 - EXPOSE 8085 36 - CMD ["/app/lumina-server"] 37 -
+40 -56
Justfile
··· 1 1 [private] 2 2 default: 3 - @just --list 3 + @just --list 4 4 5 5 [doc("Build the styles for Lumina client")] 6 6 [group('building')] 7 7 build-styles: 8 - cd ./client/ && tailwindcss -i ./app.css -o ../server/priv/static/lumina_client.css 8 + cd ./client/ && tailwindcss -i ./app.css -o ../server/priv/static/lumina_client.css 9 9 10 10 [doc("Build the server-side of Lumina")] 11 11 [group('building')] 12 12 build-server: build-client 13 - cd server && gleam build && gleam export erlang-shipment 13 + cd server; \ 14 + gleam export erlang-shipment 15 + git add -N ./server/build/erlang-shipment/* -f 16 + nix build --impure ".#container" || { \ 17 + if [[ -d .jj ]]; then jj file untrack ./server/build/erlang-shipment/* >/dev/null 2>&1; \ 18 + else git reset ./server/build/erlang-shipment/* >/dev/null 2>&1; fi; \ 19 + exit 1; \ 20 + } 21 + @if [[ -d .jj ]]; then jj file untrack ./server/build/erlang-shipment/*; else git reset ./server/build/erlang-shipment/* >/dev/null 2>&1; fi 14 22 15 - [doc("Build the server-side of Lumina optimised for release")] 16 - [group('building')] 17 - build-server-release: build-client 18 - cargo build --release 23 + @echo "Loading into Podman ..." 24 + @podman load < result && echo -e "Podman image \033[1;35mluminapeonies:latest\033[0m built!" 25 + @rm result 19 26 20 27 [doc("Build the client-side of Lumina and it's styles")] 21 28 [group('building')] 22 29 build-client: build-styles 23 - cd ./client/ &&\ 24 - gleam build --target javascript &&\ 25 - find ./client/src/ -type f -print0 | xargs -0 sha256sum | sha256sum | awk '{print $1}' > "./priv/static/lumina_client_rev.hash" &&\ 26 - echo 'import { main } from "./lumina_client.mjs";document.addEventListener("DOMContentLoaded", main())' > "./build/dev/javascript/lumina_client/lumina_client.ts" &&\ 27 - bun build ./build/dev/javascript/lumina_client/lumina_client.ts --minify --outfile ../server/priv/static/lumina_client.min.mjs --target=browser &&\ 28 - bun build ./build/dev/javascript/lumina_client/lumina_client.ts --outfile ../server/priv/static/lumina_client.mjs --target=browser 30 + cd ./client/ &&\ 31 + gleam build --target javascript &&\ 32 + find ./src/ -type f -print0 | xargs -0 sha256sum | sha256sum | awk '{print $1}' > "../server/priv/static/lumina_client_rev.hash" &&\ 33 + echo 'import { main } from "./lumina_client.mjs";document.addEventListener("DOMContentLoaded", main())' > "./build/dev/javascript/lumina_client/lumina_client.ts" &&\ 34 + bun build ./build/dev/javascript/lumina_client/lumina_client.ts --minify --outfile ../server/priv/static/lumina_client.min.mjs --target=browser &&\ 35 + bun build ./build/dev/javascript/lumina_client/lumina_client.ts --outfile ../server/priv/static/lumina_client.mjs --target=browser 29 36 30 37 [doc("Prefetch Gleam dependencies to speed up future builds")] 31 38 [group('prepare')] 32 39 prefetch-gleam-deps: 33 - cd ./client && gleam deps download 40 + cd ./client && gleam deps download 34 41 35 42 [doc("Install Bun dependencies")] 36 43 [group('prepare')] 37 44 bun-install: 38 - cd ./client && bun i 45 + cd ./client && bun i 39 46 40 47 [group('prepare')] 41 48 create-data-dirs: 42 - mkdir -p ./data 43 - mkdir -p ./data/postgres 44 - mkdir -p ./data/redis 49 + mkdir -p ./data 45 50 46 51 [doc("Clean all build artifacts")] 47 52 clean-all: 48 - cargo clean 49 - rm -rf ./client/node_modules 50 - rm -rf ./client/build 51 - rm -rf ./client/build/dev/javascript/lumina_client/lumina_client.mjs 52 - rm -rf ./client/build/dev/javascript/lumina_client/lumina_client.ts 53 - rm -rf ./client/priv/static/lumina_client.min.mjs 54 - rm -rf ./client/priv/static/lumina_client.css 53 + cargo clean 54 + rm -rf ./client/node_modules 55 + rm -rf ./client/build 56 + rm -rf ./client/build/dev/javascript/lumina_client/lumina_client.mjs 57 + rm -rf ./client/build/dev/javascript/lumina_client/lumina_client.ts 58 + rm -rf ./server/priv/static/lumina_client.min.mjs 59 + rm -rf ./server/priv/static/lumina_client.css 55 60 56 - [doc("Just runs the Podman image for a Redis and Postgres server for local development run to connect to.")] 61 + [doc("Prepares database")] 57 62 [group("local-devel")] 58 63 local-devel-prep: create-data-dirs 59 - @echo "This script needs to be rewritten for the gleam branch you are on." 60 - @exit 1 61 - @podman inspect -f '{{{{.State.Running}}}}' lumina-redis 2>/dev/null | grep -q 'true' \ 62 - && echo "lumina-redis is already running." \ 63 - || podman run -d --replace \ 64 - --name lumina-redis \ 65 - -p 6379:6379 \ 66 - -v ./data/redis:/data \ 67 - docker.io/redis/redis-stack:7.2.0-v18 68 - @podman inspect -f '{{{{.State.Running}}}}' luminadb 2>/dev/null | grep -q 'true' \ 69 - && echo "luminadb is already running." \ 70 - || podman run -d --replace \ 71 - -p 5432:5432 \ 72 - --name luminadb \ 73 - -e POSTGRES_USER=lumina \ 74 - -e POSTGRES_PASSWORD=lumina_pw \ 75 - -e POSTGRES_DB=lumina_config \ 76 - -v ./data/postgres:/var/lib/postgresql/data:Z \ 77 - docker.io/library/postgres:17-alpine3.22 78 - sqlx db create 79 - sqlx migrate run 80 - echo "Postgres database created and migrations ran" 64 + dbmate up 81 65 82 66 83 67 [doc("Run the server in development mode")] 84 68 [group("local-devel")] 85 - local-devel $LUMINA_POSTGRES_PASSWORD="lumina_pw": build-server 86 - @echo "This script needs to be rewritten for the gleam branch you are on." 87 - @exit 1 69 + local-devel: local-devel-prep build-server 70 + podman run -v ./data/:/data -p 3000:3000 localhost/luminapeonies:latest 88 71 89 72 [doc("Run the server in development mode with file watching")] 90 73 [group("local-devel")] 91 74 local-devel-watch: 92 - watchexec --restart --stop-timeout=0 --shell=sh -e rs,gleam,toml,css,ts,json -- just local-devel 75 + watchexec --restart --stop-timeout=0 --shell=sh -e rs,gleam,toml,css,ts,json -- just local-devel 93 76 94 77 [doc("Runs the commands from local-devel automatically, watches")] 95 78 [group("local-devel")] 96 79 dev: 97 - @just local-devel-prep 98 - @just local-devel-watch 80 + @just local-devel-prep 81 + @just local-devel-watch 99 82 100 83 [group("local-devel")] 101 84 [doc("Run pgweb (8081) and redis-commander (8082) for local development")] 102 85 local-devel-dataexplorer: local-devel-prep 103 - podman run -d --replace --name lumina-redis-commander -p 8082:8081 -e REDIS_HOSTS=host.containers.internal:6379 ghcr.io/joeferner/redis-commander:latest 104 - podman run -d --replace --name lumina-pgweb -p 8081:8081 -e'PGWEB_DATABASE_URL=postgres://lumina:lumina_pw@host.containers.internal:5432/lumina_config?sslmode=disable' sosedoff/pgweb:latest 86 + @echo "This script needs to be rewritten for the gleam branch you are on." 87 + @exit 1 88 +
-23
build.Dockerfile
··· 1 - # syntax=docker/dockerfile:1 2 - 3 - # --- Build stage --- 4 - FROM lumina-build-environment AS builder 5 - 6 - WORKDIR /build 7 - 8 - # Copy the project files excluding mise.toml 9 - COPY --exclude=mise.toml . . 10 - 11 - # Builds debug version 12 - RUN mkdir -p target/output && \ 13 - mise run build-server && \ 14 - cp ./target/debug/lumina-server ./target/output/; 15 - 16 - # --- Final runtime image --- 17 - FROM alpine:3.19 18 - RUN apk add --no-cache ca-certificates 19 - WORKDIR /app 20 - COPY --from=builder /build/target/output/lumina-server /app/lumina-server 21 - COPY assets /app/assets 22 - EXPOSE 8085 23 - CMD ["/app/lumina-server"]
+85
db/migrations/20260403165425_initialisation.sql
··· 1 + -- migrate:up 2 + 3 + CREATE TABLE IF NOT EXISTS logs ( 4 + id INTEGER PRIMARY KEY AUTOINCREMENT, 5 + level TEXT CHECK(level IN ('INFO', 'WARN', 'ERROR', 'DEBUG')), 6 + message TEXT NOT NULL, 7 + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP 8 + ); 9 + 10 + -- Users: Storing UUIDs as TEXT, but we'll remove the dashes to make it minecraft-like UUIDS, this saves a little space :3 11 + CREATE TABLE IF NOT EXISTS users ( 12 + id TEXT PRIMARY KEY NOT NULL CHECK(length(id) = 32), 13 + foreign_instance_id TEXT, 14 + foreign_user_id TEXT, 15 + email TEXT NOT NULL UNIQUE, 16 + username TEXT NOT NULL UNIQUE, 17 + password TEXT NOT NULL 18 + ); 19 + 20 + -- Items: On the Postgres equevalent, this was a split task between the ultimate source 21 + -- of all posts (timelines) and the types table. In this variant I had an opportunity to create some order. 22 + -- The timelines table is still the table we look up most posts in... BUT: the items table now carries the items themselves. 23 + CREATE TABLE IF NOT EXISTS items ( 24 + id TEXT PRIMARY KEY NOT NULL CHECK(length(id) = 32), 25 + item_type TEXT CHECK(item_type IN ('text', 'media', 'article')), -- DM's and such will be added later, I don't have reference types for those yet. 26 + author_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, 27 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP 28 + ); 29 + 30 + -- Timelines: 31 + CREATE TABLE IF NOT EXISTS timelines ( 32 + tlid TEXT NOT NULL CHECK(length(tlid) = 32), 33 + item_id TEXT NOT NULL REFERENCES items (id) ON DELETE CASCADE, 34 + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 35 + PRIMARY KEY (tlid, item_id) 36 + ); 37 + 38 + -- Sessions: 39 + CREATE TABLE IF NOT EXISTS sessions ( 40 + id TEXT PRIMARY KEY NOT NULL CHECK(length(id) = 32), 41 + user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, 42 + session_key TEXT NOT NULL UNIQUE, 43 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP 44 + ); 45 + 46 + -- Item Content 47 + CREATE TABLE IF NOT EXISTS post_text ( 48 + id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE, 49 + content TEXT NOT NULL, 50 + foreign_instance_id TEXT, 51 + foreign_post_id TEXT 52 + ); 53 + 54 + CREATE TABLE IF NOT EXISTS post_media ( 55 + id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE, 56 + upload_id TEXT NOT NULL, 57 + caption TEXT, 58 + foreign_instance_id TEXT, 59 + foreign_post_id TEXT 60 + ); 61 + 62 + CREATE TABLE IF NOT EXISTS post_article ( 63 + id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE, 64 + title TEXT NOT NULL, 65 + content TEXT NOT NULL, 66 + foreign_instance_id TEXT, 67 + foreign_post_id TEXT 68 + ); 69 + 70 + -- Indices for performance 71 + CREATE INDEX IF NOT EXISTS idx_items_author ON items(author_id); 72 + CREATE INDEX IF NOT EXISTS idx_timelines_ts ON timelines(timestamp); 73 + 74 + -- migrate:down 75 + 76 + DROP INDEX IF EXISTS idx_timelines_ts; 77 + DROP INDEX IF EXISTS idx_items_author; 78 + DROP TABLE IF EXISTS post_article; 79 + DROP TABLE IF EXISTS post_media; 80 + DROP TABLE IF EXISTS post_text; 81 + DROP TABLE IF EXISTS sessions; 82 + DROP TABLE IF EXISTS timelines; 83 + DROP TABLE IF EXISTS items; 84 + DROP TABLE IF EXISTS users; 85 + DROP TABLE IF EXISTS logs;
+58
db/schema.sql
··· 1 + CREATE TABLE IF NOT EXISTS "schema_migrations" (version varchar(128) primary key); 2 + CREATE TABLE logs ( 3 + id INTEGER PRIMARY KEY AUTOINCREMENT, 4 + level TEXT CHECK(level IN ('INFO', 'WARN', 'ERROR', 'DEBUG')), 5 + message TEXT NOT NULL, 6 + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP 7 + ); 8 + CREATE TABLE users ( 9 + id TEXT PRIMARY KEY NOT NULL CHECK(length(id) = 32), 10 + foreign_instance_id TEXT, 11 + foreign_user_id TEXT, 12 + email TEXT NOT NULL UNIQUE, 13 + username TEXT NOT NULL UNIQUE, 14 + password TEXT NOT NULL 15 + ); 16 + CREATE TABLE items ( 17 + id TEXT PRIMARY KEY NOT NULL CHECK(length(id) = 32), 18 + item_type TEXT CHECK(item_type IN ('text', 'media', 'article')), -- DM's and such will be added later, I don't have reference types for those yet. 19 + author_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, 20 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP 21 + ); 22 + CREATE TABLE timelines ( 23 + tlid TEXT NOT NULL CHECK(length(tlid) = 32), 24 + item_id TEXT NOT NULL REFERENCES items (id) ON DELETE CASCADE, 25 + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 26 + PRIMARY KEY (tlid, item_id) 27 + ); 28 + CREATE TABLE sessions ( 29 + id TEXT PRIMARY KEY NOT NULL CHECK(length(id) = 32), 30 + user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, 31 + session_key TEXT NOT NULL UNIQUE, 32 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP 33 + ); 34 + CREATE TABLE post_text ( 35 + id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE, 36 + content TEXT NOT NULL, 37 + foreign_instance_id TEXT, 38 + foreign_post_id TEXT 39 + ); 40 + CREATE TABLE post_media ( 41 + id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE, 42 + upload_id TEXT NOT NULL, 43 + caption TEXT, 44 + foreign_instance_id TEXT, 45 + foreign_post_id TEXT 46 + ); 47 + CREATE TABLE post_article ( 48 + id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE, 49 + title TEXT NOT NULL, 50 + content TEXT NOT NULL, 51 + foreign_instance_id TEXT, 52 + foreign_post_id TEXT 53 + ); 54 + CREATE INDEX idx_items_author ON items(author_id); 55 + CREATE INDEX idx_timelines_ts ON timelines(timestamp); 56 + -- Dbmate schema migrations 57 + INSERT INTO "schema_migrations" (version) VALUES 58 + ('20260403165425');
-42
env.Dockerfile
··· 1 - # syntax=docker/dockerfile:1 2 - 3 - FROM alpine:3.19 4 - 5 - ENV MISE_DATA_DIR="/mise" 6 - ENV MISE_CONFIG_DIR="/mise" 7 - ENV MISE_CACHE_DIR="/mise/cache" 8 - ENV MISE_INSTALL_PATH="/usr/local/bin/mise" 9 - ENV BUN_INSTALL="/usr/local/bin/bun" 10 - ENV PATH="/usr/local/bin/bun/bin:/mise/shims:$PATH" 11 - 12 - RUN apk add --no-cache curl git unzip build-base bash 13 - 14 - # Install bun outside of mise because Alpine uses musl libc which the mise bun package does not support 15 - RUN curl -fsSL https://bun.sh/install | bash 16 - RUN curl https://mise.run | sh 17 - 18 - WORKDIR /build 19 - # Copy and install the mise.toml file first to leverage Docker cache 20 - COPY mise.toml ./mise.toml 21 - COPY mise/ ./mise/ 22 - RUN mise trust && mise unuse bun && mise install 23 - 24 - # ------- Prefetch Rust dependencies ------- 25 - 26 - # Copy the manifests cargo needs to prefetch dependencies 27 - COPY Cargo.toml Cargo.lock ./ 28 - RUN mkdir server 29 - COPY server/Cargo.toml ./server/ 30 - # Prefetch dependencies 31 - RUN cargo fetch --locked 32 - 33 - # ------- Prefetch Bun dependencies ------- 34 - 35 - RUN mkdir client 36 - COPY client/package.json client/bun.lock ./client/ 37 - RUN mise run bun-install --locked 38 - 39 - # ------- Prefetch Gleam dependencies ------- 40 - 41 - COPY client/gleam.toml client/manifest.toml ./client/ 42 - RUN mise run prefetch-gleam-deps
+3 -3
flake.lock
··· 2 2 "nodes": { 3 3 "nixpkgs": { 4 4 "locked": { 5 - "lastModified": 1773734432, 6 - "narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=", 5 + "lastModified": 1775036866, 6 + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", 7 7 "owner": "NixOS", 8 8 "repo": "nixpkgs", 9 - "rev": "cda48547b432e8d3b18b4180ba07473762ec8558", 9 + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", 10 10 "type": "github" 11 11 }, 12 12 "original": {
+11 -5
flake.nix
··· 31 31 "run" 32 32 ]; 33 33 WorkingDir = "/data"; 34 - # Env should be at the same level as Cmd 35 34 Env = [ 36 - "PATH=${pkgs.jre21_minimal}/bin:${pkgs.rcon-cli}/bin:/usr/bin:/bin" 35 + "PATH=/usr/bin:/bin" 37 36 "PORT=3000" 38 37 ]; 39 38 }; ··· 51 50 packages.container = myImage; 52 51 53 52 devShells.default = pkgs.mkShell { 53 + shellHook = '' 54 + bun install --cwd=client/ --silent --only-missing 55 + echo "❄️ dev environment loaded, use 'just dev' next, or use 'just --list' for recipies." 56 + ''; 54 57 buildInputs = with pkgs; [ 55 58 # Gleam application 56 59 gleam ··· 59 62 bun 60 63 tailwindcss_4 61 64 # Task runner 62 - watchexec 63 - just 64 - # Build image and run development pg using: 65 + watchexec 66 + just 67 + # Migrations 68 + dbmate 69 + sqlite 70 + # Containerisation 65 71 podman 66 72 ]; 67 73 };
-78
migrations/0001_luminadb.sql
··· 1 - -- Create logs table 2 - CREATE TABLE IF NOT EXISTS logs 3 - ( 4 - type VARCHAR NOT NULL, 5 - message TEXT NOT NULL, 6 - timestamp TIMESTAMP NOT NULL 7 - ); 8 - 9 - -- Create users table 10 - CREATE TABLE IF NOT EXISTS users 11 - ( 12 - id UUID DEFAULT gen_random_uuid() UNIQUE PRIMARY KEY, 13 - foreign_instance_id VARCHAR, 14 - foreign_user_id UUID, 15 - email VARCHAR NOT NULL UNIQUE, 16 - username VARCHAR NOT NULL UNIQUE, 17 - password VARCHAR NOT NULL 18 - ); 19 - 20 - -- Create timelines table 21 - CREATE TABLE IF NOT EXISTS timelines 22 - ( 23 - tlid UUID NOT NULL, 24 - item_id UUID NOT NULL, 25 - timestamp TIMESTAMP WITH TIME ZONE NOT NULL, 26 - PRIMARY KEY (tlid, item_id) 27 - ); 28 - 29 - -- Create item type lookup table 30 - CREATE TABLE IF NOT EXISTS itemtypelookupdb 31 - ( 32 - itemtype VARCHAR NOT NULL, 33 - item_id UUID NOT NULL PRIMARY KEY 34 - ); 35 - 36 - -- Create sesions table 37 - CREATE TABLE IF NOT EXISTS sessions 38 - ( 39 - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, 40 - user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, 41 - session_key VARCHAR NOT NULL UNIQUE, 42 - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP 43 - ); 44 - 45 - -- Create table for posts in text type 46 - CREATE TABLE IF NOT EXISTS post_text 47 - ( 48 - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 49 - author_id UUID REFERENCES users (id), 50 - content TEXT NOT NULL, 51 - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 52 - foreign_instance_id VARCHAR, 53 - foreign_post_id VARCHAR 54 - ); 55 - 56 - -- Create table for posts of media type 57 - CREATE TABLE IF NOT EXISTS post_media 58 - ( 59 - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 60 - author_id UUID REFERENCES users (id), 61 - minio_object_id VARCHAR NOT NULL, 62 - caption TEXT, 63 - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 64 - foreign_instance_id VARCHAR, 65 - foreign_post_id VARCHAR 66 - ); 67 - 68 - -- Create table for posts of article type 69 - CREATE TABLE IF NOT EXISTS post_article 70 - ( 71 - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 72 - author_id UUID REFERENCES users (id), 73 - title VARCHAR NOT NULL, 74 - content TEXT NOT NULL, 75 - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 76 - foreign_instance_id VARCHAR, 77 - foreign_post_id VARCHAR 78 - );
-1
migrations/0002_Rename_'itemtypelookupdb'_to_'itemtypes'.sql
··· 1 - ALTER TABLE itemtypelookupdb RENAME TO itemtypes;
-24
mise.toml
··· 1 - [tools] 2 - bun = "1.2.20" 3 - "cargo:sqlx-cli" = "latest" 4 - gleam = "1.15.0" 5 - just = "latest" 6 - "rust" = { version = "1.94.0", components = "rust-analyzer" } 7 - watchexec = "latest" 8 - 9 - 10 - [settings] 11 - [settings.cargo] 12 - binstall = false # This fails in some cases, watchexec can be compiled from source quickly enough 13 - [settings.npm] 14 - bun = true 15 - 16 - [task_config] 17 - includes = [ 18 - "./mise/tasks/podman.toml", 19 - "./mise/tasks/format.toml", 20 - "./mise/tasks/check.toml", 21 - "./mise/tasks/run.toml", 22 - "./mise/tasks/moved-to-just.toml", 23 - ] 24 - dir = "{{ config_root }}"
-47
mise/tasks/check.toml
··· 1 - # Check/lint/test-related tasks for Lumina 2 - [check] 3 - description = "Run the check command on both server and client." 4 - run = { tasks = ["check-server", "check-client"] } 5 - 6 - [check-server] 7 - depends = ["build-client"] 8 - hide = true 9 - run = "cargo check" 10 - dir = "./server" 11 - 12 - [check-client] 13 - hide = true 14 - run = "gleam check" 15 - dir = "./client/" 16 - 17 - [check-watch] 18 - tools.watchexec = "latest" 19 - description = "Run the server in development mode with file watching" 20 - run = "mise watch --restart -e rs,gleam,toml,css,ts,json --clear=clear check" 21 - 22 - [clippy_watch] 23 - tools.watchexec = "latest" 24 - run = "watchexec -c clear -restart -e rs,toml 'clear ; cargo clippy'" 25 - 26 - 27 - [local-devel-test] 28 - alias = "test" 29 - description = "Run the check command on both server and client." 30 - run = { tasks = ["check-server", "check-client"] } 31 - 32 - [local-devel-test-server] 33 - depends = ["build-client"] 34 - hide = true 35 - run = "cargo test" 36 - dir = "./server" 37 - env.LUMINA_POSTGRES_PASSWORD = "lumina_pw" 38 - 39 - [local-devel-test-client] 40 - hide = true 41 - run = "gleam test" 42 - dir = "./client/" 43 - 44 - [local-devel-test-watch] 45 - tools.watchexec = "latest" 46 - description = "Run the server in development mode with file watching" 47 - run = "mise watch --restart -e rs,gleam,toml,css,ts,json --clear=clear test"
-20
mise/tasks/format.toml
··· 1 - # Formatting-related tasks for Lumina 2 - [format] 3 - description = "Format the codebase" 4 - depends = ["format-server", "format-client", "format-metafiles"] 5 - run = [] 6 - 7 - [format-server] 8 - hide = true 9 - run = "cargo fmt" 10 - dir = "./server" 11 - 12 - [format-client] 13 - hide = true 14 - run = "gleam format" 15 - dir = "./client" 16 - 17 - [format-metafiles] 18 - hide = true 19 - run = ["bun x prettier . '!./data' '!./notes' --write", "taplo format"] 20 - tools.taplo = "latest"
-60
mise/tasks/moved-to-just.toml
··· 1 - # Moved to just, kept here to preserve backwards compatibility. 2 - 3 - [build-styles] 4 - hide = true 5 - description = "$just build-styles" 6 - run = "just build-styles" 7 - 8 - [build-server] 9 - hide = true 10 - description = "$just build-server" 11 - run = "just build-server" 12 - 13 - [build-client] 14 - hide = true 15 - description = "$just build-client" 16 - run = "just build-client" 17 - 18 - [build-server-release] 19 - hide = true 20 - description = "$just build-server-release" 21 - run = "just build-server-release" 22 - 23 - [clean-all] 24 - hide = true 25 - description = "$just clean-all" 26 - run = "just clean-all" 27 - 28 - [prefetch-gleam-deps] 29 - hide = true 30 - description = "$just prefetch-gleam-deps" 31 - run = "prefetch-gleam-deps" 32 - 33 - [create-data-dirs] 34 - hide = true 35 - run = "just create-data-dirs" 36 - 37 - [bun-install] 38 - hide = true 39 - description = "$just bun-install" 40 - run = "just bun-install" 41 - 42 - [local-devel-prep] 43 - hide = true 44 - description = "$just local-devel-prep" 45 - run = "just local-devel-prep" 46 - 47 - [local-devel] 48 - hide = true 49 - description = "$just local-devel" 50 - run = "just local-devel" 51 - 52 - [local-devel-watch] 53 - hide = true 54 - description = "$just local-devel-watch" 55 - run = "just local-devel-watch" 56 - 57 - [local-devel-dataexplorer] 58 - hide = true 59 - description = "$just local-devel-dataexplorer" 60 - run = "just local-devel-dataexplorer"
-62
mise/tasks/podman.toml
··· 1 - # Podman-related tasks for Lumina 2 - # == Pod management =================================================================== 3 - [pod-stop-all] 4 - description = "Stop and remove all Lumina pods and containers" 5 - run = [ 6 - "podman pod stop lumina-postgres-pod || echo ok", 7 - "podman pod rm lumina-postgres-pod || echo ok", 8 - "podman container rm lumina-server-postgres luminadb || echo ok", 9 - ] 10 - 11 - # ===================================================================================== 12 - # == with postgres ==================================================================== 13 - [devel] 14 - description = "Run Lumina with PostgreSQL in a pod, development build (no optimisations)" 15 - depends = "create-data-dirs" 16 - run = [ 17 - # Clean up any existing pods/containers first 18 - "podman pod stop lumina-postgres-pod || echo ok", 19 - "podman pod rm lumina-postgres-pod || echo ok", 20 - "podman container rm lumina-server-postgres luminadb lumina-redis || echo ok", 21 - # Create the pod 22 - "podman pod create --name lumina-postgres-pod -p 8085:8085 -p 5432:5432 -p 8081:8081 -p 8082:8082", 23 - # Pre-run the redis commander and pgweb containers so they are available immediately 24 - "podman run -d --replace --name lumina-redis-commander --pod lumina-postgres-pod -e REDIS_HOSTS=lumina-redis -e PORT=8082 ghcr.io/joeferner/redis-commander:latest", 25 - "podman run -d --replace --name lumina-pgweb --pod lumina-postgres-pod -e DATABASE_URL=postgres://lumina:lumina_pw@luminadb:5432/lumina_config?sslmode=disable sosedoff/pgweb:latest", 26 - # Build and run 27 - "podman run -d --name luminadb --pod lumina-postgres-pod -e POSTGRES_USER=lumina -e POSTGRES_PASSWORD=lumina_pw -e POSTGRES_DB=lumina_config -v ./data/postgres:/var/lib/postgresql/data:Z docker.io/library/postgres:17-alpine3.22", 28 - "podman run -d --replace --name lumina-redis --pod lumina-postgres-pod -v ./data/redis:/data:Z redis", 29 - "podman build -f build.Dockerfile -t lumina-server:dev .", 30 - "echo pgweb on http://127.0.0.1:8081, Redis Commander on http://127.0.0.1:8082", 31 - "podman run --name lumina-server-postgres --pod lumina-postgres-pod -e LUMINA_DB_TYPE=postgres -e LUMINA_POSTGRES_HOST=localhost -e LUMINA_POSTGRES_PORT=5432 -e LUMINA_POSTGRES_USERNAME=lumina -e LUMINA_POSTGRES_PASSWORD=lumina_pw -e LUMINA_POSTGRES_DATABASE=lumina_config -e LUMINA_SERVER_PORT=8085 -e LUMINA_SERVER_ADDR=0.0.0.0 lumina-server:dev", 32 - ] 33 - 34 - 35 - [pod-up] 36 - description = "Run Lumina with PostgreSQL in a pod [optimised build]" 37 - env = { LUMINA_DOCKER_OPTIMIZE_BUILD = "true" } 38 - depends = "create-data-dirs" 39 - run = [ 40 - # Clean up any existing pods/containers first 41 - "podman pod stop lumina-postgres-pod || echo ok", 42 - "podman pod rm lumina-postgres-pod || echo ok", 43 - "podman container rm lumina-server-postgres luminadb lumina-redis || echo ok", 44 - # Build and run 45 - "podman build --build-arg optimize_build=true -t lumina-server:latest .", 46 - "podman pod create --name lumina-postgres-pod -p 8085:8085 -p 5432:5432", 47 - "podman run -d --replace --name lumina-redis --pod lumina-postgres-pod -v ./data/redis:/data redis", 48 - "podman run -d --name luminadb --pod lumina-postgres-pod -e POSTGRES_USER=lumina -e POSTGRES_PASSWORD=lumina_pw -e POSTGRES_DB=lumina_config -v ./data/postgres:/var/lib/postgresql/data:Z docker.io/library/postgres:17-alpine3.22", 49 - "podman run --name lumina-server-postgres --pod lumina-postgres-pod -e LUMINA_DB_TYPE=postgres -e LUMINA_POSTGRES_HOST=localhost -e LUMINA_POSTGRES_PORT=5432 -e LUMINA_POSTGRES_USERNAME=lumina -e LUMINA_POSTGRES_PASSWORD=lumina_pw -e LUMINA_POSTGRES_DATABASE=lumina_config -e LUMINA_SERVER_PORT=8085 -e LUMINA_SERVER_ADDR=0.0.0.0 lumina-server:latest", 50 - ] 51 - 52 - # ==================================================================================== 53 - # == Build environment image ========================================================= 54 - [build-env-image] 55 - description = "Build and tag the lumina-build-environment image from env.Dockerfile" 56 - run = ["podman build -f env.Dockerfile -t lumina-build-environment ."] 57 - 58 - [devel-watch] 59 - tools.watchexec = "latest" 60 - description = "Run the server in development mode with file watching" 61 - run = "mise x -- watchexec --restart --stop-timeout=0 --shell=sh -e rs,gleam,toml,css,ts,json mise run devel" 62 - run_windows = "mise x -- watchexec --restart --stop-timeout=0 --shell=cmd -e rs,gleam,toml,css,ts,json mise run devel"
mise/tasks/run.toml

This is a binary file and will not be displayed.

+2 -1
server/gleam.toml
··· 24 24 gleam_erlang = ">= 1.3.0 and < 2.0.0" 25 25 wisp = ">= 2.2.2 and < 3.0.0" 26 26 woof = ">= 1.2.0 and < 2.0.0" 27 - youid = ">= 1.5.1 and < 2.0.0" 27 + youid = ">= 1.6.0 and < 2.0.0" 28 28 pog = ">= 4.1.0 and < 5.0.0" 29 29 ewe = ">= 3.0.7 and < 4.0.0" 30 30 gleam_otp = ">= 1.2.0 and < 2.0.0" 31 31 collie = ">= 1.0.0 and < 2.0.0" 32 32 parrot = ">= 2.2.1 and < 3.0.0" 33 33 sqlight = ">= 1.0.3 and < 2.0.0" 34 + simplifile = ">= 2.4.0 and < 3.0.0" 34 35 35 36 [dev_dependencies] 36 37 gleeunit = ">= 1.0.0 and < 2.0.0"
+3 -2
server/manifest.toml
··· 27 27 { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 28 28 { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 29 29 { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 30 - { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 30 + { name = "logging", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "BC5F18CE5DD9686100229FE5409BDC3DD5C46D5A7DF2F804AD2D8F0DD6C5060E" }, 31 31 { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 32 32 { name = "mist", version = "6.0.2", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "6B03DEEA38A02F276333CB27B53B16D3D45BD741B89599085A601BAF635F2006" }, 33 33 { name = "opentelemetry_api", version = "1.5.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "F53EC8A1337AE4A487D43AC89DA4BD3A3C99DDF576655D071DEED8B56A2D5DDA" }, ··· 62 62 parrot = { version = ">= 2.2.1 and < 3.0.0" } 63 63 pog = { version = ">= 4.1.0 and < 5.0.0" } 64 64 shellout = { version = ">= 1.8.0 and < 2.0.0" } 65 + simplifile = { version = ">= 2.4.0 and < 3.0.0" } 65 66 sqlight = { version = ">= 1.0.3 and < 2.0.0" } 66 67 wisp = { version = ">= 2.2.2 and < 3.0.0" } 67 68 woof = { version = ">= 1.2.0 and < 2.0.0" } 68 - youid = { version = ">= 1.5.1 and < 2.0.0" } 69 + youid = { version = ">= 1.6.0 and < 2.0.0" }
+1 -1
server/priv/static/lumina_client.css
··· 1 - /*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */ 1 + /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ 2 2 @layer properties; 3 3 @layer theme, base, components, utilities; 4 4 @layer theme {
+178 -3
server/src/lumina_server.gleam
··· 1 - import gleam/io 1 + import envoy 2 + import ewe.{type Request, type Response} 3 + import gleam/erlang/application 4 + import gleam/erlang/process 5 + import gleam/http/response 6 + import gleam/int 7 + import gleam/list 8 + import gleam/option.{None} 9 + import gleam/result 10 + import gleam/uri 11 + import simplifile 12 + import sqlight 13 + import woof 14 + 15 + type HandlerContext { 16 + HandlerContext(db: sqlight.Connection, client_hash: String, assets: String) 17 + } 18 + 19 + pub fn main() { 20 + use db <- sqlight.with_connection("/data/instance.db") 21 + // At some point everything should go here, I think. 22 + // woof.set_sink(woof.beam_logger_sink) 23 + // But for now, do both! 24 + woof.set_sink(fn(entry, formatted) { 25 + woof.beam_logger_sink(entry, formatted) 26 + woof.default_sink(entry, formatted) 27 + }) 28 + let setuplog = woof.new("WARMUP") 29 + // PRAGMA's 30 + let _ = sqlight.exec("PRAGMA journal_mode = WAL;", db) 31 + let _ = sqlight.exec("PRAGMA synchronous = NORMAL;", db) 32 + let _ = sqlight.exec("PRAGMA cache_size = -64000;", db) 33 + let _ = sqlight.exec("PRAGMA foreign_keys = ON;", db) 34 + 35 + // Logging 36 + woof.configure(woof.Config( 37 + level: woof.Debug, 38 + format: woof.Text, 39 + colors: woof.Auto, 40 + )) 41 + 42 + let assets = case application.priv_directory("lumina_server") { 43 + Ok(outcome) -> outcome 44 + Error(_) -> { 45 + setuplog |> woof.log(woof.Error, "could not get priv folder.", []) 46 + panic 47 + } 48 + } 49 + 50 + // Check client hash 51 + let client_hash = case 52 + simplifile.read(assets <> "/static/lumina_client_rev.hash") 53 + { 54 + Error(_) -> { 55 + setuplog 56 + |> woof.log( 57 + woof.Error, 58 + "could not load client revision's hash from filesystem.", 59 + [], 60 + ) 61 + panic 62 + } 63 + Ok(outcome) -> { 64 + setuplog 65 + |> woof.log(woof.Info, "Found client revision!", [#("revision", outcome)]) 66 + outcome 67 + } 68 + } 69 + // And start! 70 + let assert Ok(_) = 71 + ewe.new(handler(_, HandlerContext(db:, assets:, client_hash:))) 72 + |> ewe.bind("0.0.0.0") 73 + |> ewe.listening( 74 + port: envoy.get("PORT") 75 + |> result.map(int.parse) 76 + |> result.flatten() 77 + |> result.unwrap(3000), 78 + ) 79 + |> ewe.start 80 + 81 + process.sleep_forever() 82 + } 83 + 84 + fn handler(req: Request, handler_ctx: HandlerContext) -> Response { 85 + let httplogger = fn( 86 + level: woof.Level, 87 + msg: String, 88 + vars: List(#(String, String)), 89 + ) { 90 + woof.new("WEBSERVER") 91 + |> woof.log(level, msg, vars |> list.append([#("path", req.path)])) 92 + } 93 + case req.path |> uri.path_segments() { 94 + ["/"] | [""] | [] -> { 95 + response.new(200) 96 + |> response.set_header("content-type", "text/html; charset=utf-8") 97 + |> response.set_body(ewe.TextData( 98 + "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\" /><title>Lumina</title><link rel=\"preconnect\" href=\"https://fontlay.com\" corossorigin /><link href=\"https://fontlay.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Elms+Sans:ital,wght@0,100..900;1,100..900&family=Gantari:ital,wght@0,100..900;1,100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&family=Vend+Sans&display=swap\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/lumina.css\"/><meta name=\"robots\" content=\"noai, noimageai, nofollow\"><script>window.clientHash = \"" 99 + <> { handler_ctx.client_hash } 100 + <> "\";</script><script type=\"module\" src=\"/static/lumina.min.mjs\"></script></head><body id=\"app\"></body></html>", 101 + )) 102 + } 103 + ["static", "lumina.min.mjs"] -> { 104 + let file = handler_ctx.assets <> "/static/lumina_client.min.mjs" 105 + case ewe.file(file, None, None) { 106 + Error(_) -> { 107 + httplogger(woof.Error, "Missing application assets.", []) 108 + response.new(500) 109 + |> response.set_header("content-type", "text/plain; charset=utf-8") 110 + |> response.set_body(ewe.TextData("500 Internal Server Error")) 111 + } 112 + Ok(outcome) -> { 113 + response.new(200) 114 + |> response.set_header( 115 + "content-type", 116 + "application/javascript; charset=utf-8", 117 + ) 118 + |> response.set_body(outcome) 119 + } 120 + } 121 + } 122 + ["static", "lumina.mjs"] -> { 123 + let file = handler_ctx.assets <> "/static/lumina_client.mjs" 124 + case ewe.file(file, None, None) { 125 + Error(_) -> { 126 + httplogger(woof.Error, "Missing application assets.", []) 127 + response.new(500) 128 + |> response.set_header("content-type", "text/plain; charset=utf-8") 129 + |> response.set_body(ewe.TextData("500 Internal Server Error")) 130 + } 131 + Ok(outcome) -> { 132 + response.new(200) 133 + |> response.set_header( 134 + "content-type", 135 + "application/javascript; charset=utf-8", 136 + ) 137 + |> response.set_body(outcome) 138 + } 139 + } 140 + } 141 + ["static", "lumina.css"] -> { 142 + let file = handler_ctx.assets <> "/static/lumina_client.css" 143 + case ewe.file(file, None, None) { 144 + Error(_) -> { 145 + httplogger(woof.Error, "Missing application assets.", []) 146 + response.new(500) 147 + |> response.set_header("content-type", "text/plain; charset=utf-8") 148 + |> response.set_body(ewe.TextData("500 Internal Server Error")) 149 + } 150 + Ok(outcome) -> { 151 + response.new(200) 152 + |> response.set_header("content-type", "text/css; charset=utf-8") 153 + |> response.set_body(outcome) 154 + } 155 + } 156 + } 2 157 3 - pub fn main() -> Nil { 4 - io.println("Hello from lumina_server!") 158 + ["static", staticfile] -> { 159 + let file = handler_ctx.assets <> "/static/" <> staticfile 160 + case ewe.file(file, None, None) { 161 + Error(_) -> { 162 + httplogger(woof.Warning, "Not found.", [#("file", file)]) 163 + response.new(404) 164 + |> response.set_header("content-type", "text/plain; charset=utf-8") 165 + |> response.set_body(ewe.TextData("404! Not found!")) 166 + } 167 + Ok(outcome) -> { 168 + response.new(200) 169 + |> response.set_body(outcome) 170 + } 171 + } 172 + } 173 + _ -> { 174 + httplogger(woof.Warning, "Not found.", []) 175 + response.new(404) 176 + |> response.set_header("content-type", "text/plain; charset=utf-8") 177 + |> response.set_body(ewe.TextData("404! Not found!")) 178 + } 179 + } 5 180 }