Initial commit: eldov.win portfolio with admin backend
Privates Portfolio von eldov (Jan Wagner). Next.js 16 / React 19 / Tailwind 4 /
SQLite. Eigenes Dark-/Terminal-Theme (Magenta+Grün-Akzente, ASCII-Boxen).
Public Routes (DE default, /en für Englisch):
- /, /ueber-mich, /projekte, /projekte/[slug], /kontakt, /impressum, /datenschutz
- Sitemap, robots, OG, JSON-LD
Admin (HMAC-Single-User, HttpOnly-Cookie):
- /admin/login, /admin/dashboard, /admin/profile, /admin/projects (CRUD)
- API: /api/admin/{login,logout,session,profile,projects,projects/[slug]}
Initial-Seed: Hermes, Casino-Bot, Polymarket-Trader, QuantMuse, Crypto-/Trade-Bot,
TS6-Bot, Freewarez-Landingpage, Minecraft-Portal, OSS-Themes, AI-Subscription-Manager,
LAMP-Link-Themes, Android. KEIN Batchmaker/W-Make (das gehört zu w-make.com).
Stack:
- better-sqlite3 mit Schema-Migration + idempotentem Seed
- HMAC-signiertes Session-Cookie mit timingSafeEqual
- Rate-Limit für /api/admin/login
- Docker + deploy-vps.sh analog w-make-com → free-warez.top
This commit is contained in:
+448
@@ -0,0 +1,448 @@
|
||||
// Initiales Seed für eldov.win — Bio + Projekte aus dem Workspace.
|
||||
// Wird beim ersten Request aufgerufen, wenn Profile leer ist oder
|
||||
// `projects` keine Zeilen enthält.
|
||||
|
||||
import type Database from "better-sqlite3";
|
||||
|
||||
const PROFILE_DEFAULTS = {
|
||||
handle: "eldov",
|
||||
real_name: "Jan Wagner",
|
||||
role: "Self-Hoster · Bot-Bauer · Privatprojekte",
|
||||
location: "Deutschland, remote",
|
||||
bio_de: [
|
||||
"Ich baue Dinge, die ich selbst benutzen will — meistens nachts.",
|
||||
"Hermes ist meine Multi-Agent-Fleet: ein Dutzend Rollen, mehrere Hosts, ein Context-Bus. Es ist das Nervensystem hinter fast allen anderen Projekten hier.",
|
||||
"Daneben schreibe ich Bots, die Daten lesen und Aktionen auslösen — Casino, Prediction-Markets, Crypto, TeamSpeak. Ich bin kein Trader, ich bin jemand, der die Mechanik verstehen will und die Reibung in den Werkzeugen reduziert.",
|
||||
"Wenn ich nicht am Code bin, self-hoste ich Freewarez (Gitea, Jellyfin, Discourse, Authelia, Uptime-Kuma und ein paar mehr), betreibe ein Minecraft-Portal und schraube an meinem Setup aus Nix, Tailscale und 9router.",
|
||||
"Was Sie hier nicht finden: Industrie-Software und Auftragsarbeit. Die steht auf w-make.com. Hier ist alles andere: Hacks, Spielwiesen, Notizen.",
|
||||
].join("\n\n"),
|
||||
bio_en: [
|
||||
"I build things I want to use myself — usually at night.",
|
||||
"Hermes is my multi-agent fleet: a dozen roles, several hosts, a context bus. It is the nervous system behind almost every other project here.",
|
||||
"I also write bots that read data and trigger actions — casino, prediction markets, crypto, TeamSpeak. I am not a trader; I am someone who wants to understand the mechanics and reduce the friction in the tools.",
|
||||
"When I am not coding I self-host freewarez (Gitea, Jellyfin, Discourse, Authelia, Uptime-Kuma and a few more), run a Minecraft portal and tinker with my setup of Nix, Tailscale and 9router.",
|
||||
"What you will not find here: industrial software and client work. That lives at w-make.com. This is everything else: hacks, playgrounds, notes.",
|
||||
].join("\n\n"),
|
||||
facts_de: [
|
||||
"Handle 'eldov' seit 2003. Klarname nur, weil er sowieso auf w-make.com steht.",
|
||||
"Stack-Schwerpunkte: TypeScript, Node, Python, Rust, Nix, SQLite, Postgres.",
|
||||
"Infrastruktur: zwei VPS-Hosts (free-warez.win / .top), ein Laptop, ein WSL-PC, alles über Tailscale und 9router vernetzt.",
|
||||
"Bevorzugt kleine Werkzeuge mit klarer Verantwortung statt großer Frameworks.",
|
||||
],
|
||||
facts_en: [
|
||||
"Handle 'eldov' since 2003. Real name is public because it already is on w-make.com.",
|
||||
"Stack focus: TypeScript, Node, Python, Rust, Nix, SQLite, Postgres.",
|
||||
"Infrastructure: two VPS hosts (free-warez.win / .top), one laptop, one WSL PC, all networked via Tailscale and 9router.",
|
||||
"Prefers small tools with clear responsibility over large frameworks.",
|
||||
],
|
||||
links_json: JSON.stringify([
|
||||
{ label: "Gitea · eldov", href: "https://gitea.free-warez.win/eldov", kind: "git" },
|
||||
{ label: "Hermes (Multi-Agent-Fleet)", href: "https://hermes.free-warez.win", kind: "service" },
|
||||
{ label: "Free-Warez-Selfhosting", href: "https://free-warez.top", kind: "service" },
|
||||
{ label: "Minecraft-Portal", href: "https://minecraft.free-warez.win", kind: "service" },
|
||||
{ label: "w-make.com (geschäftlich)", href: "https://w-make.com", kind: "external" },
|
||||
]),
|
||||
};
|
||||
|
||||
type ProjectSeed = {
|
||||
slug: string;
|
||||
number: string;
|
||||
status: string;
|
||||
category: string;
|
||||
stack: string;
|
||||
href?: string;
|
||||
featured: number;
|
||||
summary_de: string;
|
||||
summary_en: string;
|
||||
body_de: string;
|
||||
body_en: string;
|
||||
stance_de?: string;
|
||||
stance_en?: string;
|
||||
sort_index: number;
|
||||
};
|
||||
|
||||
const PROJECTS: ProjectSeed[] = [
|
||||
{
|
||||
slug: "hermes",
|
||||
number: "01",
|
||||
status: "Aktiv · Continuous",
|
||||
category: "AI-Fleet · Multi-Agent",
|
||||
stack: "Python · TypeScript · MCP · Nix · SQLite · 9router",
|
||||
href: "https://hermes.free-warez.win",
|
||||
featured: 1,
|
||||
sort_index: 10,
|
||||
summary_de:
|
||||
"Self-hosted Multi-Agent-Fleet. Ein Context-Bus, ein Bus-Check, ein Watchdog. Hermes orchestriert die anderen Projekte.",
|
||||
summary_en:
|
||||
"Self-hosted multi-agent fleet. One context bus, one bus check, one watchdog. Hermes orchestrates the other projects.",
|
||||
stance_de:
|
||||
"Nervensystem. Wenn Hermes ausfällt, sehe ich es zuerst an allen anderen Bots.",
|
||||
stance_en:
|
||||
"Nervous system. When Hermes goes down I see it first on every other bot.",
|
||||
body_de: [
|
||||
"Hermes ist das Ding, ohne das die anderen Projekte nicht laufen würden.",
|
||||
"Es ist ein Context-Bus, ein Botnetz-Registry, ein Worker-Router und eine Handvoll Werkzeuge, die alle dasselbe Vokabular sprechen. MCP-Server, ein CLI, eine WebUI, eine Forum-Intelligence.",
|
||||
"Wer hier nach Architektur sucht, findet sie: getrennte Welten, dedizierte Workspaces, ein einziger Bus. Was nicht hierher gehört: Casino-Logik, Industrie-Logik, Persönliches.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"Hermes is the thing without which the other projects would not run.",
|
||||
"It is a context bus, a botnet registry, a worker router and a handful of tools that all share the same vocabulary. MCP server, CLI, web UI, forum intelligence.",
|
||||
"If you are looking for architecture here, you will find it: separated worlds, dedicated workspaces, a single bus. What does not belong here: casino logic, industrial logic, personal stuff.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "casino-bot",
|
||||
number: "02",
|
||||
status: "Aktiv · Live-RTP",
|
||||
category: "Bot · Casino",
|
||||
stack: "Node.js · ESM · patchright · Express · better-sqlite3",
|
||||
featured: 1,
|
||||
sort_index: 20,
|
||||
summary_de:
|
||||
"Live-RTP-Analyse für BC.Game (Hacksaw). Liest Slot-Statistiken, bewertet Slots, spielt nach Regeln.",
|
||||
summary_en:
|
||||
"Live-RTP analysis for BC.Game (Hacksaw). Reads slot statistics, scores slots, plays by rules.",
|
||||
stance_de:
|
||||
"Mechanik verstehen, nicht gewinnen. Das System ist ehrlich über das, was es nicht weiß.",
|
||||
stance_en:
|
||||
"Understand the mechanics, not beat them. The system is honest about what it does not know.",
|
||||
body_de: [
|
||||
"Vollautomatischer Slot-Bot für Hacksaw Gaming über BC.Game. Analysiert Live-RTP, wählt statistisch günstige Slots, spielt mit konfigurierbarem Bankroll-Management und lernt aus jeder Session.",
|
||||
"Was hier nicht passiert: Live-Resultate, Quoten-Versprechen, Affiliate-Links. Wer das System ernst nimmt, schaut in die Docs, nicht auf einen ROI-Screenshot.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"Fully automated slot bot for Hacksaw Gaming on BC.Game. Analyses live RTP, picks statistically favourable slots, plays with configurable bankroll management and learns from every session.",
|
||||
"What does not happen here: live results, odds promises, affiliate links. If you take the system seriously, read the docs — do not look at an ROI screenshot.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "polymarket-trader",
|
||||
number: "03",
|
||||
status: "Aktiv · Paper-Trading",
|
||||
category: "Bot · Prediction-Markets",
|
||||
stack: "Python · polymarket-cli · LLM-Thesis · Kelly-Sizing",
|
||||
featured: 1,
|
||||
sort_index: 30,
|
||||
summary_de:
|
||||
"Prediction-Market-Trader für Polymarket. Scanner, LLM-Thesis, Kelly-Sizing. Live-Orders bleiben fail-closed.",
|
||||
summary_en:
|
||||
"Prediction-market trader for Polymarket. Scanner, LLM thesis, Kelly sizing. Live orders remain fail-closed.",
|
||||
stance_de:
|
||||
"Paper-Trade zuerst, immer. Live-Order-Pfad ist hart verriegelt, bis die Simulation sauber ist.",
|
||||
stance_en:
|
||||
"Paper trade first, always. The live-order path is hard-locked until the simulation is clean.",
|
||||
body_de: [
|
||||
"Inspiriert vom LunarResearcher-Artikel. Modulare Architektur: Scanner, Brain, Executor, Exit, Data.",
|
||||
"Live-Orders sind standardmäßig verriegelt. Der CLI-Einstieg verweigert Live-Trades fail-closed, bis die Paper-Run über genug Tage sauber gelaufen ist.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"Inspired by the LunarResearcher article. Modular architecture: scanner, brain, executor, exit, data.",
|
||||
"Live orders are locked by default. The CLI entry refuses live trades fail-closed until the paper run has been clean for enough days.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "quantmuse",
|
||||
number: "04",
|
||||
status: "Aktiv · Development",
|
||||
category: "Quant · Trading",
|
||||
stack: "Python · Pandas · NumPy · Backtesting · AI-Analyse",
|
||||
href: "https://gitea.free-warez.win/eldov/QuantMuse",
|
||||
featured: 0,
|
||||
sort_index: 40,
|
||||
summary_de:
|
||||
"Quant Trading System mit AI-Analyse, Realtime-Daten und Risikomanagement. Code, kein Screenshot-Versprechen.",
|
||||
summary_en:
|
||||
"Quant trading system with AI analysis, real-time data and risk management. Code, not screenshot promises.",
|
||||
stance_de:
|
||||
"Backtest zuerst. Keine öffentliche Performance, nur Methodik.",
|
||||
stance_en:
|
||||
"Backtest first. No public performance, only methodology.",
|
||||
body_de: [
|
||||
"QuantMuse ist die größere Quant-Werkbank: Signale, Strategien, Backtests, Risk.",
|
||||
"Was hier nicht passiert: P&L-Screenshots, »Strategy of the Month«, Affiliate-Broker.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"QuantMuse is the bigger quant workshop: signals, strategies, backtests, risk.",
|
||||
"What does not happen here: P&L screenshots, »strategy of the month«, affiliate brokers.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "crypto-bot",
|
||||
number: "05",
|
||||
status: "Early Stage",
|
||||
category: "Bot · Crypto",
|
||||
stack: "TypeScript · EVM · DEX-Wrapper · On-Chain-Analyse",
|
||||
featured: 0,
|
||||
sort_index: 50,
|
||||
summary_de:
|
||||
"Crypto-Tracking, DeFi-Automatisierung und On-Chain-Analyse. Ethereum, L2s, Polygon.",
|
||||
summary_en:
|
||||
"Crypto tracking, DeFi automation and on-chain analysis. Ethereum, L2s, Polygon.",
|
||||
stance_de:
|
||||
"Werkzeuge statt Empfehlungen. Wer es benutzt, soll es selbst entscheiden.",
|
||||
stance_en:
|
||||
"Tools, not recommendations. Whoever uses it decides for themselves.",
|
||||
body_de: [
|
||||
"Domäne: Ethereum, Arbitrum, Optimism, Polygon. DeFi: Uniswap, Sushi, Yield-Farming, Liquidity-Pools. On-Chain: Wallet-Analyse, Token-Swaps, Gas-Optimierung.",
|
||||
"Status: Early stage — Projektstart ausstehend.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"Domain: Ethereum, Arbitrum, Optimism, Polygon. DeFi: Uniswap, Sushi, yield farming, liquidity pools. On-chain: wallet analysis, token swaps, gas optimisation.",
|
||||
"Status: early stage — project kickoff pending.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "trade-bot",
|
||||
number: "06",
|
||||
status: "Early Stage",
|
||||
category: "Bot · Multi-Asset-Trading",
|
||||
stack: "Python · Binance · Bybit · dYdX · Paper-Trading",
|
||||
featured: 0,
|
||||
sort_index: 60,
|
||||
summary_de:
|
||||
"Algorithmischer Trading-Bot für Crypto, Aktien, CFDs. Paper-Trading zuerst.",
|
||||
summary_en:
|
||||
"Algorithmic trading bot for crypto, stocks, CFDs. Paper trading first.",
|
||||
stance_de:
|
||||
"Domäne: Spot, Futures, Perps, Forex, Indices. Kein Live-Trading ohne dokumentiertes Risk-Modell.",
|
||||
stance_en:
|
||||
"Domain: spot, futures, perps, forex, indices. No live trading without a documented risk model.",
|
||||
body_de: [
|
||||
"Multi-Asset-Bot mit Fokus auf sauberes Risk-Modell und Backtest-Disziplin.",
|
||||
"Anti-Patterns stehen in `HERMES.md`. Eskalationsmatrix ebenso.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"Multi-asset bot focused on a clean risk model and backtest discipline.",
|
||||
"Anti-patterns live in `HERMES.md`. So does the escalation matrix.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "ts6-bot",
|
||||
number: "07",
|
||||
status: "Stabilitäts-Hardening",
|
||||
category: "Bot · Voice-Infra",
|
||||
stack: "TypeScript · Node.js · TeamSpeak 6 Query · Docker",
|
||||
featured: 0,
|
||||
sort_index: 70,
|
||||
summary_de:
|
||||
"TeamSpeak-6-Bot für Server-Automatisierung. REST/Polling, SSH Query, Reconnect mit Backoff.",
|
||||
summary_en:
|
||||
"TeamSpeak 6 bot for server automation. REST polling, SSH query, reconnect with backoff.",
|
||||
stance_de:
|
||||
"TS6 ist beta. Chat-Commands bleiben deaktiviert bis ein Event-Test sauber läuft.",
|
||||
stance_en:
|
||||
"TS6 is beta. Chat commands stay disabled until an event test runs cleanly.",
|
||||
body_de: [
|
||||
"TS6-Bot mit Server-, Channel- und Client-Daten via REST/Polling. Events via SSH Server Query. `textmessage` ist in TS6 beta10 fehlerhaft (1538) und deshalb nicht freigeschaltet.",
|
||||
"Reconnect mit Exponential Backoff bei Verbindungsabbrüchen.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"TS6 bot with server, channel and client data via REST polling. Events via SSH Server Query. `textmessage` is broken in TS6 beta10 (1538) and therefore disabled.",
|
||||
"Reconnect with exponential backoff on connection drops.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "freewarez-landingpage",
|
||||
number: "08",
|
||||
status: "Live",
|
||||
category: "Web · Landing",
|
||||
stack: "HTML5 · Glassmorphism CSS",
|
||||
href: "https://free-warez.top",
|
||||
featured: 0,
|
||||
sort_index: 80,
|
||||
summary_de:
|
||||
"Landingpage für die Free-Warez-Selfhosting-Welt. free-warez.top.",
|
||||
summary_en:
|
||||
"Landing page for the Free-Warez self-hosting world. free-warez.top.",
|
||||
stance_de:
|
||||
"Glas-Ästhetik, keine Tracker, keine Schriften von Dritten.",
|
||||
stance_en:
|
||||
"Glass aesthetic, no trackers, no third-party fonts.",
|
||||
body_de: [
|
||||
"free-warez.top ist die öffentliche Seite meiner Self-Hosting-Welt. Was nicht beworben wird: VPS-Internas, Passwörter, API-Keys.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"free-warez.top is the public face of my self-hosting world. What is not advertised: VPS internals, passwords, API keys.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "minecraft-portal",
|
||||
number: "09",
|
||||
status: "Live",
|
||||
category: "Web · Gaming-Community",
|
||||
stack: "Next.js · Tailwind · Prisma",
|
||||
href: "https://minecraft.free-warez.win",
|
||||
featured: 0,
|
||||
sort_index: 90,
|
||||
summary_de:
|
||||
"Minecraft-Portal (Ashen Atlas) für unsere Community. Next.js mit eigener Datenbank.",
|
||||
summary_en:
|
||||
"Minecraft portal (Ashen Atlas) for our community. Next.js with its own database.",
|
||||
stance_de:
|
||||
"Spielwiese für ein produktives Web-Setup mit echten Nutzern.",
|
||||
stance_en:
|
||||
"Playground for a productive web setup with real users.",
|
||||
body_de: [
|
||||
"minecraft.free-warez.win ist das Community-Portal. Wer dort spielt, kennt mein Stack-Setup, ohne dass ich es erklären muss.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"minecraft.free-warez.win is the community portal. Whoever plays there knows my stack setup without me having to explain it.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "oss-themes",
|
||||
number: "10",
|
||||
status: "Live · Sammlung",
|
||||
category: "Web · Theming",
|
||||
stack: "CSS · SCSS · XML · Blade",
|
||||
featured: 0,
|
||||
sort_index: 100,
|
||||
summary_de:
|
||||
"Themes für Open-Source-Selfhosting: Jellyfin, Gitea, Discourse, Kavita, Authelia, Uptime-Kuma, Erugo, LAMP-Link, Stirling-PDF, Element.",
|
||||
summary_en:
|
||||
"Themes for open-source self-hosting: Jellyfin, Gitea, Discourse, Kavita, Authelia, Uptime Kuma, Erugo, LAMP Link, Stirling PDF, Element.",
|
||||
stance_de:
|
||||
"Jedes Theme hat ein eigenes Repo und ein eigenes Deploy-Skript. Kein Mega-Theme-Monolith.",
|
||||
stance_en:
|
||||
"Each theme has its own repo and its own deploy script. No mega-theme monolith.",
|
||||
body_de: [
|
||||
"Sammlung von Themes, die meinen Self-Hosting-Stack lesbar machen. Glas, Dunkelheit, Akzent — je nach Produkt.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"A collection of themes that make my self-hosting stack readable. Glass, darkness, accent — depending on the product.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "ai-subscription-manager",
|
||||
number: "11",
|
||||
status: "Live",
|
||||
category: "Web · AI-Ops",
|
||||
stack: "Python · FastAPI · Jinja2 · WebUI",
|
||||
href: "https://ai.free-warez.win",
|
||||
featured: 0,
|
||||
sort_index: 110,
|
||||
summary_de:
|
||||
"Verwaltet AI-Subscriptions und Provider-Routing. Welche Modelle, welche Keys, welche Kosten.",
|
||||
summary_en:
|
||||
"Manages AI subscriptions and provider routing. Which models, which keys, which costs.",
|
||||
stance_de:
|
||||
"Internes Werkzeug, das nach außen sichtbar sein darf, weil es keine fremden Daten enthält.",
|
||||
stance_en:
|
||||
"Internal tool that is allowed to be visible because it does not contain third-party data.",
|
||||
body_de: [
|
||||
"ai-subscription-manager ist die Schaltzentrale für AI-Provider, Keys und Budgets.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"ai-subscription-manager is the control centre for AI providers, keys and budgets.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "lamp-link-themes",
|
||||
number: "12",
|
||||
status: "Live",
|
||||
category: "Web · Link-in-Bio",
|
||||
stack: "PHP · the23 Theme Engine · CSS-Vars",
|
||||
href: "https://link.free-warez.win",
|
||||
featured: 0,
|
||||
sort_index: 120,
|
||||
summary_de:
|
||||
"Theme für LAMP-Link-Shortener und Bio-Pages. the23 Theme Engine.",
|
||||
summary_en:
|
||||
"Theme for LAMP link shortener and bio pages. the23 theme engine.",
|
||||
stance_de:
|
||||
"Klein, schnell, gut lesbar auf dem Phone.",
|
||||
stance_en:
|
||||
"Small, fast, readable on the phone.",
|
||||
body_de: [
|
||||
"LAMP-Link-Themes bedienen link.free-warez.win und andere Bio-Pages. Fokus: Phone-First.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"LAMP Link themes serve link.free-warez.win and other bio pages. Focus: phone first.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
{
|
||||
slug: "android",
|
||||
number: "13",
|
||||
status: "Aktiv",
|
||||
category: "Mobile · Desktop",
|
||||
stack: "Android · Affine · Kotlin · Compose",
|
||||
featured: 0,
|
||||
sort_index: 130,
|
||||
summary_de:
|
||||
"Android-Apps und Affine-basierte Desktop-Projekte.",
|
||||
summary_en:
|
||||
"Android apps and Affine-based desktop projects.",
|
||||
stance_de:
|
||||
"Mobile First, auch wenn der Use-Case es nicht erzwingt.",
|
||||
stance_en:
|
||||
"Mobile first, even when the use case does not demand it.",
|
||||
body_de: [
|
||||
"Coding/Android enthält Android-Projekte und Affine-Desktop-Apps.",
|
||||
].join("\n\n"),
|
||||
body_en: [
|
||||
"Coding/Android contains Android projects and Affine desktop apps.",
|
||||
].join("\n\n"),
|
||||
},
|
||||
];
|
||||
|
||||
export function seedProfile(db: Database.Database): boolean {
|
||||
const existing = db.prepare("SELECT id FROM profile WHERE id = 1").get();
|
||||
if (existing) return false;
|
||||
db.prepare(
|
||||
`INSERT INTO profile
|
||||
(id, handle, real_name, role, location, bio_de, bio_en, facts_de, facts_en, links_json)
|
||||
VALUES (1, @handle, @real_name, @role, @location, @bio_de, @bio_en, @facts_de, @facts_en, @links_json)`
|
||||
).run({
|
||||
handle: PROFILE_DEFAULTS.handle,
|
||||
real_name: PROFILE_DEFAULTS.real_name,
|
||||
role: PROFILE_DEFAULTS.role,
|
||||
location: PROFILE_DEFAULTS.location,
|
||||
bio_de: PROFILE_DEFAULTS.bio_de,
|
||||
bio_en: PROFILE_DEFAULTS.bio_en,
|
||||
facts_de: JSON.stringify(PROFILE_DEFAULTS.facts_de),
|
||||
facts_en: JSON.stringify(PROFILE_DEFAULTS.facts_en),
|
||||
links_json: PROFILE_DEFAULTS.links_json,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export function seedProjects(db: Database.Database): number {
|
||||
const count = (db.prepare("SELECT COUNT(*) as c FROM projects").get() as { c: number }).c;
|
||||
if (count > 0) return 0;
|
||||
const stmt = db.prepare(
|
||||
`INSERT INTO projects
|
||||
(slug, number, status, category, stack, href, featured,
|
||||
summary_de, summary_en, body_de, body_en, stance_de, stance_en, sort_index)
|
||||
VALUES
|
||||
(@slug, @number, @status, @category, @stack, @href, @featured,
|
||||
@summary_de, @summary_en, @body_de, @body_en, @stance_de, @stance_en, @sort_index)`
|
||||
);
|
||||
const insertMany = db.transaction((rows: ProjectSeed[]) => {
|
||||
for (const r of rows) {
|
||||
stmt.run({
|
||||
slug: r.slug,
|
||||
number: r.number,
|
||||
status: r.status,
|
||||
category: r.category,
|
||||
stack: r.stack,
|
||||
href: r.href ?? null,
|
||||
featured: r.featured,
|
||||
summary_de: r.summary_de,
|
||||
summary_en: r.summary_en,
|
||||
body_de: r.body_de,
|
||||
body_en: r.body_en,
|
||||
stance_de: r.stance_de ?? "",
|
||||
stance_en: r.stance_en ?? "",
|
||||
sort_index: r.sort_index,
|
||||
});
|
||||
}
|
||||
});
|
||||
insertMany(PROJECTS);
|
||||
return PROJECTS.length;
|
||||
}
|
||||
|
||||
export function seedIfEmpty(db: Database.Database): { profileSeeded: boolean; projectsSeeded: number } {
|
||||
const profileSeeded = seedProfile(db);
|
||||
const projectsSeeded = seedProjects(db);
|
||||
return { profileSeeded, projectsSeeded };
|
||||
}
|
||||
Reference in New Issue
Block a user