🧱 update CI workflow and remove alpinejs frontend files
Some checks failed
CI / Lint (push) Successful in 21s
CI / Helm Lint (push) Successful in 5s
CI / Build (push) Failing after 43s
CI / Test (push) Has been skipped

This commit is contained in:
Christian van Dijk
2026-02-23 15:46:31 +01:00
parent a545ef5af4
commit a6daab7961
17 changed files with 78 additions and 53 deletions

View File

@@ -37,7 +37,7 @@ jobs:
deno-version: v2 deno-version: v2
- name: Deno lint frontend - name: Deno lint frontend
run: cd frontend/alpinejs && deno lint main.ts run: cd frontend && deno lint main.ts
continue-on-error: true continue-on-error: true
build: build:

View File

@@ -94,7 +94,7 @@ end
-- Seed initial data -- Seed initial data
local function seed_db() local function seed_db()
db.with_connection(function(conn) db.with_connection(function(conn)
local res, err = conn:query("SELECT COUNT(*) as total FROM devices") local res = conn:query("SELECT COUNT(*) as total FROM devices")
local count = 0 local count = 0
if res and res[1] then if res and res[1] then
count = tonumber(res[1].total) or 0 count = tonumber(res[1].total) or 0
@@ -137,7 +137,8 @@ local function seed_db()
for _, device in ipairs(devices) do for _, device in ipairs(devices) do
conn:query( conn:query(
"INSERT INTO devices (name, manufacturer, release_year, cpu, ram_mb, storage_mb, display_size, battery_hours) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", "INSERT INTO devices (name, manufacturer, release_year, cpu, ram_mb, storage_mb, "
.. "display_size, battery_hours) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
device.name, device.name,
device.manufacturer, device.manufacturer,
device.release_year, device.release_year,
@@ -207,9 +208,9 @@ local function b64(data)
-- Minimal Base64 -- Minimal Base64
local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
return ((data:gsub('.', function(x) return ((data:gsub('.', function(x)
local r,b='',x:byte() local r, byte = '', x:byte()
for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end for i = 8, 1, -1 do r = r .. (byte % 2^i - byte % 2^(i - 1) > 0 and '1' or '0') end
return r; return r
end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x) end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
if (#x < 6) then return '' end if (#x < 6) then return '' end
local c=0 local c=0
@@ -227,11 +228,11 @@ local function encode_ws_frame(payload)
header = header .. string.char(126) .. string.char(math.floor(len / 256)) .. string.char(len % 256) header = header .. string.char(126) .. string.char(math.floor(len / 256)) .. string.char(len % 256)
else else
-- 64-bit length not implemented for simplicity -- 64-bit length not implemented for simplicity
header = header .. string.char(127) .. string.rep(string.char(0), 4) .. header = header .. string.char(127) .. string.rep(string.char(0), 4)
string.char(math.floor(len / 16777216) % 256) .. .. string.char(math.floor(len / 16777216) % 256)
string.char(math.floor(len / 65536) % 256) .. .. string.char(math.floor(len / 65536) % 256)
string.char(math.floor(len / 256) % 256) .. .. string.char(math.floor(len / 256) % 256)
string.char(len % 256) .. string.char(len % 256)
end end
return header .. payload return header .. payload
end end
@@ -275,7 +276,8 @@ end
function Device.create(data, request_id) function Device.create(data, request_id)
local res = db.with_connection(function(conn) local res = db.with_connection(function(conn)
return conn:query( return conn:query(
"INSERT INTO devices (name, manufacturer, release_year, cpu, ram_mb, storage_mb, display_size, battery_hours) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id", "INSERT INTO devices (name, manufacturer, release_year, cpu, ram_mb, storage_mb, "
.. "display_size, battery_hours) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id",
data.name, data.name,
data.manufacturer, data.manufacturer,
(not is_json_null(data.release_year)) and tonumber(data.release_year) or nil, (not is_json_null(data.release_year)) and tonumber(data.release_year) or nil,
@@ -302,14 +304,30 @@ function Device.update(id, data, request_id)
db.with_connection(function(conn) db.with_connection(function(conn)
local updates = {} local updates = {}
local pg = conn local pg = conn
if not is_json_null(data.name) then table.insert(updates, "name = " .. pg:escape_literal(data.name)) end if not is_json_null(data.name) then
if not is_json_null(data.manufacturer) then table.insert(updates, "manufacturer = " .. pg:escape_literal(data.manufacturer)) end table.insert(updates, "name = " .. pg:escape_literal(data.name))
if not is_json_null(data.release_year) then table.insert(updates, "release_year = " .. tonumber(data.release_year)) end end
if not is_json_null(data.cpu) then table.insert(updates, "cpu = " .. pg:escape_literal(data.cpu)) end if not is_json_null(data.manufacturer) then
if not is_json_null(data.ram_mb) then table.insert(updates, "ram_mb = " .. tonumber(data.ram_mb)) end table.insert(updates, "manufacturer = " .. pg:escape_literal(data.manufacturer))
if not is_json_null(data.storage_mb) then table.insert(updates, "storage_mb = " .. tonumber(data.storage_mb)) end end
if not is_json_null(data.display_size) then table.insert(updates, "display_size = " .. pg:escape_literal(data.display_size)) end if not is_json_null(data.release_year) then
if not is_json_null(data.battery_hours) then table.insert(updates, "battery_hours = " .. tonumber(data.battery_hours)) end table.insert(updates, "release_year = " .. tonumber(data.release_year))
end
if not is_json_null(data.cpu) then
table.insert(updates, "cpu = " .. pg:escape_literal(data.cpu))
end
if not is_json_null(data.ram_mb) then
table.insert(updates, "ram_mb = " .. tonumber(data.ram_mb))
end
if not is_json_null(data.storage_mb) then
table.insert(updates, "storage_mb = " .. tonumber(data.storage_mb))
end
if not is_json_null(data.display_size) then
table.insert(updates, "display_size = " .. pg:escape_literal(data.display_size))
end
if not is_json_null(data.battery_hours) then
table.insert(updates, "battery_hours = " .. tonumber(data.battery_hours))
end
if #updates > 0 then if #updates > 0 then
table.insert(updates, "updated_at = CURRENT_TIMESTAMP") table.insert(updates, "updated_at = CURRENT_TIMESTAMP")
@@ -370,7 +388,7 @@ local function parse_headers(client)
-- Use a small timeout for individual header lines to handle slow clients -- Use a small timeout for individual header lines to handle slow clients
-- or cases where headers are partially sent. -- or cases where headers are partially sent.
client:settimeout(0.1) client:settimeout(0.1)
local line, err = client:receive("*l") local line = client:receive("*l")
if not line or line == "" then break end if not line or line == "" then break end
local key, value = line:match("^([^:]+):%s*(.*)$") local key, value = line:match("^([^:]+):%s*(.*)$")
if key then if key then
@@ -626,7 +644,6 @@ function app.start()
for i, c in ipairs(clients) do for i, c in ipairs(clients) do
local line, err = c.socket:receive("*l") local line, err = c.socket:receive("*l")
if line then if line then
local method, full_path = parse_request(line)
local headers = parse_headers(c.socket) local headers = parse_headers(c.socket)
if headers["upgrade"] == "websocket" then if headers["upgrade"] == "websocket" then

View File

@@ -1,6 +1,5 @@
-- PostgreSQL connection pool using pgmoon -- PostgreSQL connection pool using pgmoon
local pgmoon = require("pgmoon") local pgmoon = require("pgmoon")
local cjson = require("cjson")
local DB_HOST = os.getenv("DB_HOST") or "localhost" local DB_HOST = os.getenv("DB_HOST") or "localhost"
local DB_PORT = tonumber(os.getenv("DB_PORT")) or 5432 local DB_PORT = tonumber(os.getenv("DB_PORT")) or 5432

View File

@@ -32,7 +32,7 @@ local function move_to_dlq(red, event_json, reason)
log.warn("Event moved to DLQ", { reason = reason, dlq_key = DLQ_KEY }) log.warn("Event moved to DLQ", { reason = reason, dlq_key = DLQ_KEY })
end end
local function process_event(red, event_json) local function process_event(_red, event_json)
local ok_decode, event = pcall(cjson.decode, event_json) local ok_decode, event = pcall(cjson.decode, event_json)
if not ok_decode or not event then if not ok_decode or not event then
return false, "decode_failed" return false, "decode_failed"

View File

@@ -9,8 +9,8 @@ ENV PORT=${PORT}
WORKDIR /app WORKDIR /app
# Copy the frontend files # Copy the frontend files
# Since the Dockerfile is in the root and files are in alpinejs/, we copy from there # Since the Dockerfile is in the root and files are in ./, we copy from there
COPY alpinejs/ . COPY . .
# Ensure the deno user owns the directory so it can write public/config.js # Ensure the deno user owns the directory so it can write public/config.js
RUN chown -R deno:deno /app RUN chown -R deno:deno /app

View File

@@ -3,6 +3,7 @@
"start": "deno run --allow-net --allow-read --allow-write --allow-env --allow-run --watch main.ts" "start": "deno run --allow-net --allow-read --allow-write --allow-env --allow-run --watch main.ts"
}, },
"imports": { "imports": {
"@dx/alpine-server": "jsr:@dx/alpine-server" "@dx/alpine-server": "jsr:@dx/alpine-server",
"@oak/oak": "jsr:@oak/oak@^17.2.0"
} }
} }

View File

@@ -114,7 +114,8 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@dx/alpine-server@*" "jsr:@dx/alpine-server@*",
"jsr:@oak/oak@^17.2.0"
] ]
} }
} }

View File

@@ -1,23 +1,24 @@
import { AlpineApp } from '@dx/alpine-server'; import { AlpineApp } from "@dx/alpine-server";
import { Router } from 'jsr:@oak/oak'; import { Context, Router } from "@oak/oak";
let config = { let config = {
api_url: Deno.env.get('API_URL') || 'http://localhost:8080', api_url: Deno.env.get("API_URL") || "http://localhost:8080",
port: parseInt(Deno.env.get('PORT') || '8090') port: parseInt(Deno.env.get("PORT") || "8090"),
}; };
try { try {
const configFile = await Deno.readTextFile('./config.json'); const configFile = await Deno.readTextFile("./config.json");
const fileConfig = JSON.parse(configFile); const fileConfig = JSON.parse(configFile);
config = { ...config, ...fileConfig }; config = { ...config, ...fileConfig };
} catch (e) { // deno-lint-ignore no-explicit-any
console.warn('Could not read config.json, using defaults', e.message); } catch (e: any) {
console.warn("Could not read config.json, using defaults", e.message);
} }
const app = new AlpineApp({ const app = new AlpineApp({
app: { app: {
dev: true, dev: true,
staticFilesPath: './public', staticFilesPath: "./public",
}, },
oak: { oak: {
listenOptions: { port: config.port }, listenOptions: { port: config.port },
@@ -25,17 +26,17 @@ const app = new AlpineApp({
}); });
const healthRouter = new Router(); const healthRouter = new Router();
healthRouter.get('/health', (ctx) => { healthRouter.get("/health", (ctx: Context) => {
ctx.response.body = { status: 'ok' }; ctx.response.body = { status: "ok" };
ctx.response.status = 200; ctx.response.status = 200;
}); });
app.append(healthRouter); app.append(healthRouter);
app.use(async (ctx, next) => { app.use(async (ctx: Context, next: () => Promise<unknown>) => {
await next(); await next();
const contentType = ctx.response.headers.get('content-type') || ''; const contentType = ctx.response.headers.get("content-type") || "";
if (contentType.includes('text/html')) { if (contentType.includes("text/html")) {
let csp = ctx.response.headers.get('Content-Security-Policy'); let csp = ctx.response.headers.get("Content-Security-Policy");
if (!csp) { if (!csp) {
// securityHeaders might not have set it yet if it runs after us in the call stack // securityHeaders might not have set it yet if it runs after us in the call stack
// but in run(), securityHeaders is app.use()ed BEFORE user middlewares. // but in run(), securityHeaders is app.use()ed BEFORE user middlewares.
@@ -52,10 +53,13 @@ app.use(async (ctx, next) => {
"font-src 'self'", "font-src 'self'",
"connect-src 'self'", "connect-src 'self'",
"media-src 'self'", "media-src 'self'",
].join('; '); ].join("; ");
} }
const updatedCsp = csp.replace("connect-src 'self'", `connect-src 'self' ${config.api_url} ws:`); const updatedCsp = csp.replace(
ctx.response.headers.set('Content-Security-Policy', updatedCsp); "connect-src 'self'",
`connect-src 'self' ${config.api_url} ws:`,
);
ctx.response.headers.set("Content-Security-Policy", updatedCsp);
} }
}); });
@@ -63,6 +67,9 @@ console.log(`URL: http://localhost:${config.port}`);
console.log(`API: ${config.api_url}`); console.log(`API: ${config.api_url}`);
// Create config.js file in public directory // Create config.js file in public directory
await Deno.writeTextFile('./public/config.js', `window.APP_CONFIG = ${JSON.stringify(config)};`); await Deno.writeTextFile(
"./public/config.js",
`window.APP_CONFIG = ${JSON.stringify(config)};`,
);
await app.run(); await app.run();

View File

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 699 B