Compare commits

..

42 Commits

Author SHA1 Message Date
redglow 66c0506e7e fix: import types
Build and Deploy / Build and Upload (push) Successful in 1m44s
Build and Deploy / Upload (push) Successful in 1m51s
2026-06-24 15:24:26 +02:00
redglow 1a7deed5b0 feat: refactored /api into its own router with error handler
Build and Deploy / Build and Upload (push) Successful in 1m33s
Build and Deploy / Upload (push) Successful in 1m48s
2026-06-24 10:09:48 +02:00
redglow 463943abab chore: allow biome run to fix "unsafe" parts 2026-06-24 10:09:22 +02:00
redglow d2b87f6f57 chore: more debugging
Build and Deploy / Build and Upload (push) Successful in 1m14s
Build and Deploy / Upload (push) Successful in 1m47s
2026-06-23 16:48:31 +02:00
redglow f729d24c65 chore: print data extracted from environment.
Build and Deploy / Build and Upload (push) Successful in 1m16s
Build and Deploy / Upload (push) Successful in 1m47s
2026-06-23 15:26:11 +02:00
redglow 4b1174cb30 ci: artifact => server
Build and Deploy / Build and Upload (push) Successful in 1m18s
Build and Deploy / Upload (push) Successful in 1m48s
2026-06-23 15:13:04 +02:00
redglow d6a3bd55a7 ci: artifact => server
Build and Deploy / Build and Upload (push) Successful in 1m10s
Build and Deploy / Upload (push) Failing after 10s
2026-06-23 15:07:46 +02:00
redglow b843f49399 ci: use rsync/ssh
Build and Deploy / Build and Upload (push) Successful in 1m24s
Build and Deploy / Upload (push) Successful in 11s
2026-06-23 15:03:33 +02:00
redglow 31dc117fb9 ci: vite.config.json => ts
Build and Deploy / Build and Upload (push) Successful in 1m38s
Build and Deploy / Upload (push) Successful in 8m30s
2026-06-23 14:34:44 +02:00
redglow c109a2f558 chore: updated run command 2026-06-23 14:34:34 +02:00
redglow 9ccc609522 ci: removed ssh ls
Build and Deploy / Build and Upload (push) Successful in 1m12s
Build and Deploy / Upload (push) Successful in 6m52s
2026-06-23 14:07:48 +02:00
redglow c17bf09df3 ci: revert to scp
Build and Deploy / Build and Upload (push) Successful in 1m8s
Build and Deploy / Upload (push) Failing after 9s
2026-06-23 14:01:21 +02:00
redglow b01af42d91 ci: check banner output and ssh v2
Build and Deploy / Build and Upload (push) Successful in 1m18s
Build and Deploy / Upload (push) Failing after 12s
2026-06-22 14:25:13 +02:00
redglow a5069f922f ci: fix rsync parameters
Build and Deploy / Build and Upload (push) Successful in 1m11s
Build and Deploy / Upload (push) Failing after 12s
2026-06-22 14:19:32 +02:00
redglow 279ab4bb9d ci: apt fix
Build and Deploy / Build and Upload (push) Successful in 1m11s
Build and Deploy / Upload (push) Failing after 12s
2026-06-22 14:15:59 +02:00
redglow 2a33bb81c1 ci: update apt before install
Build and Deploy / Build and Upload (push) Successful in 1m12s
Build and Deploy / Upload (push) Failing after 10s
2026-06-22 14:12:38 +02:00
redglow 5bb81ba2f2 ci: install rsync
Build and Deploy / Build and Upload (push) Successful in 1m14s
Build and Deploy / Upload (push) Failing after 3s
2026-06-22 14:10:25 +02:00
redglow 6164c1ca27 ci: use rsync instead of scp
Build and Deploy / Build and Upload (push) Successful in 1m31s
Build and Deploy / Upload (push) Failing after 4s
2026-06-22 14:07:58 +02:00
redglow ced95a9593 ci: scp recursive
Build and Deploy / Build and Upload (push) Successful in 1m13s
Build and Deploy / Upload (push) Successful in 6m35s
2026-06-22 13:56:12 +02:00
redglow 0e3740bb6a ci: use scp
Build and Deploy / Build and Upload (push) Successful in 1m15s
Build and Deploy / Upload (push) Failing after 5s
2026-06-22 13:45:38 +02:00
redglow 3028d5ffb2 ci: with "artifact" path
Build and Deploy / Build and Upload (push) Successful in 1m16s
Build and Deploy / Upload (push) Failing after 4s
2026-06-22 13:42:27 +02:00
redglow fbdb963b01 ci: remove download path
Build and Deploy / Build and Upload (push) Successful in 1m16s
Build and Deploy / Upload (push) Failing after 4s
2026-06-22 13:40:18 +02:00
redglow e577748914 ci: download artifact
Build and Deploy / Build and Upload (push) Successful in 1m13s
Build and Deploy / Upload (push) Failing after 8s
2026-06-22 13:35:19 +02:00
redglow ee0ba92ff6 ci: with path
Build and Deploy / Build and Upload (push) Successful in 1m9s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 13:22:12 +02:00
redglow defbe8ce16 ci: try no name for download artifact
Build and Deploy / Build and Upload (push) Successful in 1m10s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 13:15:01 +02:00
redglow c7e3417093 ci: more output
Build and Deploy / Build and Upload (push) Successful in 1m8s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 12:58:01 +02:00
redglow fcc7f3017c ci: workspace?
Build and Deploy / Build and Upload (push) Successful in 1m9s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 12:56:18 +02:00
redglow 2343af9d2e ci: some fixes
Build and Deploy / Build and Upload (push) Successful in 1m7s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 12:53:46 +02:00
redglow 440ad779fd ci: wat
Build and Deploy / Build and Upload (push) Successful in 1m10s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 12:50:09 +02:00
redglow ec724ddebe ci: print data
Build and Deploy / Build and Upload (push) Successful in 1m17s
Build and Deploy / Upload (push) Failing after 2s
2026-06-22 12:40:31 +02:00
redglow bbcc008317 ci: trying christopherHX
Build and Deploy / Build and Upload (push) Successful in 1m19s
Build and Deploy / Upload (push) Failing after 8s
2026-06-22 12:33:03 +02:00
redglow 32ea4de8de ci: try download v4
Build and Deploy / Build and Upload (push) Successful in 1m29s
Build and Deploy / Upload (push) Failing after 11s
2026-06-22 12:27:11 +02:00
redglow 517d6e5472 ci: gitea
Build and Deploy / Build and Upload (push) Successful in 1m21s
Build and Deploy / Upload (push) Failing after 13s
2026-06-22 12:14:48 +02:00
redglow 7ad5e5082b ci: publish
Build and Deploy / Build and Upload (push) Successful in 1m23s
Build and Deploy / upload (push) Failing after 12s
2026-06-22 12:11:21 +02:00
redglow bee21cffba ci: fix upload-artifact
Build and Deploy / Build (push) Successful in 1m29s
2026-06-22 08:56:49 +02:00
redglow b634decd1f ci: upload-artifact => gitea-upload-artifact
Build and Deploy / Build (push) Failing after 3s
see https://gitea.com/actions/gitea-upload-artifact#gitea-upload-artifact
2026-06-22 08:54:00 +02:00
redglow ce29a85d61 ci: missing "with" for upload artifact
Build and Deploy / Build (push) Failing after 1m22s
2026-06-22 08:49:26 +02:00
redglow c1c5aabd36 ci: artifacts
Build and Deploy / Build (push) Failing after 1m31s
2026-06-22 08:47:08 +02:00
redglow 96f28dbba4 ci: artifacts 2026-06-22 08:46:08 +02:00
redglow 742132fbb0 ci: yarn install
Build and Deploy / Build (push) Successful in 1m15s
2026-06-22 08:34:40 +02:00
redglow b8bb83624d ci: yarn install (dev)
Build and Deploy / Build (push) Failing after 2s
2026-06-22 08:33:56 +02:00
redglow c762b81b01 ci: run-name 2026-06-22 08:31:55 +02:00
7 changed files with 210 additions and 129 deletions
+50 -3
View File
@@ -1,5 +1,5 @@
name: Build and Deploy
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
run-name: Build And Deploy (by ${{ gitea.actor }}) 🚀
on:
workflow_dispatch:
push:
@@ -7,8 +7,55 @@ on:
- main
jobs:
checkout:
name: Build
build:
name: Build and Upload
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
- name: Install yarn
shell: bash
run: npm i yarn -g
- name: Yarn install (dev)
shell: bash
run: yarn install
- name: Vite build
shell: bash
run: ./node_modules/.bin/vite build
- name: Publish artifacts
uses: https://gitea.com/actions/gitea-upload-artifact@v4
with:
name: artifact
path: |
dist
index.html
package.json
public
src
tsconfig.json
vite.config.ts
yarn.lock
Upload:
steps:
- name: Download artifacts
uses: https://github.com/christopherHX/gitea-download-artifact@v4
with:
name: artifact
path: server
- name: Install rsync
run: |
apt-get update
apt-get -y install rsync
- name: Upload and install dependencies in server
run: |
mkdir -p ~/.ssh
chmod 600 ~/.ssh
echo "$SSH_KEYSCAN" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
# scp -r -P 44 artifact owofannouncements@foxthesystem.space:~/server
rsync -avzxh --delete -e 'ssh -p 44' server owofannouncements@foxthesystem.space:~
ssh -p 44 owofannouncements@foxthesystem.space "/bin/bash -c 'cd server ; yarn install --production=true'"
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KEYSCAN: ${{ secrets.SSH_KEYSCAN }}
+1 -1
View File
@@ -25,7 +25,7 @@ Compile:
Run:
```
NODE_ENV=production HTTP_PORT=1234 DATABASE_PATH=/opt/owof-announcements/data.db UPLOADS_DIR=/opt/owof-announcements/uploads node /opt/owof-announcements/code/src/server/main.ts
NODE_ENV=production HTTP_PORT=19285 DATABASE_PATH=/opt/owofannouncements/data.db UPLOADS_DIR=/opt/owofannouncements/uploads node /opt/owofannouncements/server/src/server/main.ts
```
Copy to server:
+1 -1
View File
@@ -7,7 +7,7 @@
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
"start": "NODE_ENV=production tsx src/server/main.ts",
"build": "vite build",
"biome-full": "biome check --write"
"biome-full": "biome check --write --unsafe"
},
"dependencies": {
"@types/better-sqlite3": "^7.6.13",
+147
View File
@@ -0,0 +1,147 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import {
type NextFunction,
type Request,
type Response,
Router,
} from "express";
import multer from "multer";
import z from "zod";
import { baseUploadsFolder } from "./base-uploads-folder.ts";
import { db } from "./db/database.ts";
const PostInput = z.object({
file: z.object({ filename: z.string() }),
body: z.object({
publication_date: z.coerce
.date()
.transform((x) => new Date(x).toISOString()),
title: z.string().nonempty(),
text: z.string().nonempty(),
}),
});
const PatchInput = z.object({
file: z.object({ filename: z.string().optional() }).optional(),
body: z.object({
publication_date: z.coerce
.date()
.optional()
.transform((x) => (x ? new Date(x).toISOString() : x)),
title: z.string().nonempty().optional(),
text: z.string().nonempty().optional(),
}),
});
const AnnouncementIdInput = z.object({
announcementId: z.coerce.number().positive(),
});
const GetFromGame = z.object({
gameName: z.string().nonempty(),
});
const upload = multer({
dest: baseUploadsFolder,
});
export const apiRouter = Router();
apiRouter.get("/announcements", async (_, res) => {
console.log(`Processing GET /api/announcements`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.execute();
res.json(announcements);
});
apiRouter.get("/announcements/from-game", async (req, res) => {
console.log(`Processing GET /api/announcements/from-game`);
const fromGameData = GetFromGame.parse(req.query);
console.log(`Request from game ${fromGameData.gameName}`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.where("publication_date", "<=", new Date().toISOString())
.limit(3)
.execute();
res.json(announcements);
});
apiRouter.get("/announcements/:announcementId", async (req, res) => {
console.log(`Processing GET /api/announcements/:announcementId`);
const { announcementId } = AnnouncementIdInput.parse(req.params);
const announcement = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
res.json(announcement);
});
apiRouter.post("/announcements", upload.single("image"), async (req, res) => {
console.log(`Processing POST /api/announcements`);
const postInput = PostInput.parse(req);
const announcement = {
title: postInput.body.title,
text: postInput.body.text,
publication_date: postInput.body.publication_date,
image: postInput.file.filename,
};
await db.insertInto("announcements").values(announcement).execute();
res.redirect("/");
});
apiRouter.post(
"/announcements/:announcementId",
upload.single("image"),
async (req, res) => {
console.log(`Processing POST /api/announcements/:announcementId`);
const patchInput = PatchInput.parse(req);
const { announcementId } = AnnouncementIdInput.parse(req.params);
await db
.updateTable("announcements")
.set({
title: patchInput.body.title,
text: patchInput.body.text,
publication_date: patchInput.body.publication_date,
image: patchInput.file?.filename,
})
.where("id", "=", announcementId)
.execute();
res.redirect("/");
},
);
apiRouter.delete("/announcements/:announcementId", async (req, res) => {
console.log(`Processing DELETE /api/announcements`);
const { announcementId } = AnnouncementIdInput.parse(req.params);
const { image } = await db
.selectFrom("announcements")
.select("image")
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
await db
.deleteFrom("announcements")
.where("id", "=", announcementId)
.execute();
await unlink(path.join(baseUploadsFolder, image));
res.status(204).send();
});
apiRouter.use(
(_err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(_err);
res.status(500).send("Internal Server Error.");
},
);
+2
View File
@@ -0,0 +1,2 @@
export const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
console.log(`Using ${baseUploadsFolder} as base uploads folder`);
+3 -1
View File
@@ -2,8 +2,10 @@ import SQLite from "better-sqlite3";
import { Kysely, SqliteDialect } from "kysely";
import type { Database } from "./types.ts";
const databasePath = process.env.DATABASE_PATH ?? "./data.db";
console.log(`Using ${databasePath} as database path`);
const dialect = new SqliteDialect({
database: new SQLite(process.env.DATABASE_PATH ?? "./data.db"),
database: new SQLite(databasePath),
});
export const db = new Kysely<Database>({
+6 -123
View File
@@ -1,133 +1,16 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import express from "express";
import multer from "multer";
import ViteExpress from "vite-express";
import z from "zod";
import { db } from "./db/database.ts";
const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
const upload = multer({
dest: baseUploadsFolder,
});
import { apiRouter } from "./api.ts";
import { baseUploadsFolder } from "./base-uploads-folder.ts";
const app = express();
const PostInput = z.object({
file: z.object({ filename: z.string() }),
body: z.object({
publication_date: z.coerce
.date()
.transform((x) => new Date(x).toISOString()),
title: z.string().nonempty(),
text: z.string().nonempty(),
}),
app.use((req, _res, next) => {
console.log(`Received request ${req.url}`);
next();
});
const PatchInput = z.object({
file: z.object({ filename: z.string().optional() }).optional(),
body: z.object({
publication_date: z.coerce
.date()
.optional()
.transform((x) => (x ? new Date(x).toISOString() : x)),
title: z.string().nonempty().optional(),
text: z.string().nonempty().optional(),
}),
});
const AnnouncementIdInput = z.object({
announcementId: z.coerce.number().positive(),
});
const GetFromGame = z.object({
gameName: z.string().nonempty(),
});
app.get("/api/announcements", async (_, res) => {
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.execute();
res.json(announcements);
});
app.get("/api/announcements/from-game", async (req, res) => {
const fromGameData = GetFromGame.parse(req.query);
console.log(`Request from game ${fromGameData.gameName}`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.where("publication_date", "<=", new Date().toISOString())
.limit(3)
.execute();
res.json(announcements);
});
app.get("/api/announcements/:announcementId", async (req, res) => {
const { announcementId } = AnnouncementIdInput.parse(req.params);
const announcement = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
res.json(announcement);
});
app.post("/api/announcements", upload.single("image"), async (req, res) => {
const postInput = PostInput.parse(req);
const announcement = {
title: postInput.body.title,
text: postInput.body.text,
publication_date: postInput.body.publication_date,
image: postInput.file.filename,
};
await db.insertInto("announcements").values(announcement).execute();
res.redirect("/");
});
app.post(
"/api/announcements/:announcementId",
upload.single("image"),
async (req, res) => {
const patchInput = PatchInput.parse(req);
const { announcementId } = AnnouncementIdInput.parse(req.params);
await db
.updateTable("announcements")
.set({
title: patchInput.body.title,
text: patchInput.body.text,
publication_date: patchInput.body.publication_date,
image: patchInput.file?.filename,
})
.where("id", "=", announcementId)
.execute();
res.redirect("/");
},
);
app.delete("/api/announcements/:announcementId", async (req, res) => {
const { announcementId } = AnnouncementIdInput.parse(req.params);
const { image } = await db
.selectFrom("announcements")
.select("image")
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
await db
.deleteFrom("announcements")
.where("id", "=", announcementId)
.execute();
await unlink(path.join(baseUploadsFolder, image));
res.status(204).send();
});
app.use("/api", apiRouter);
app.use("/images", express.static(baseUploadsFolder));