From a6daab79616189666c3697cb6675a65b1b2c08c8 Mon Sep 17 00:00:00 2001 From: Christian van Dijk Date: Mon, 23 Feb 2026 15:46:31 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B1=20update=20CI=20workflow=20and=20r?= =?UTF-8?q?emove=20alpinejs=20frontend=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- devices-api/app-standalone.lua | 73 +++++++++++------- devices-api/db.lua | 1 - devices-worker/worker.lua | 2 +- frontend/{alpinejs => }/.vscode/launch.json | 0 frontend/{alpinejs => }/.vscode/settings.json | 0 frontend/Dockerfile | 4 +- frontend/{alpinejs => }/config.json | 0 frontend/{alpinejs => }/deno.json | 3 +- frontend/{alpinejs => }/deno.lock | 3 +- frontend/{alpinejs => }/main.ts | 43 ++++++----- frontend/{alpinejs => }/public/alpine.min.js | 0 frontend/{alpinejs => }/public/config.js | 0 frontend/{alpinejs => }/public/favicon.ico | Bin frontend/{alpinejs => }/public/index.html | 0 frontend/{alpinejs => }/public/main.js | 0 frontend/{alpinejs => }/public/style.css | 0 17 files changed, 78 insertions(+), 53 deletions(-) rename frontend/{alpinejs => }/.vscode/launch.json (100%) rename frontend/{alpinejs => }/.vscode/settings.json (100%) rename frontend/{alpinejs => }/config.json (100%) rename frontend/{alpinejs => }/deno.json (61%) rename frontend/{alpinejs => }/deno.lock (98%) rename frontend/{alpinejs => }/main.ts (52%) rename frontend/{alpinejs => }/public/alpine.min.js (100%) rename frontend/{alpinejs => }/public/config.js (100%) rename frontend/{alpinejs => }/public/favicon.ico (100%) rename frontend/{alpinejs => }/public/index.html (100%) rename frontend/{alpinejs => }/public/main.js (100%) rename frontend/{alpinejs => }/public/style.css (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f37771..19f0e37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: deno-version: v2 - name: Deno lint frontend - run: cd frontend/alpinejs && deno lint main.ts + run: cd frontend && deno lint main.ts continue-on-error: true build: diff --git a/devices-api/app-standalone.lua b/devices-api/app-standalone.lua index df29572..3e619a9 100644 --- a/devices-api/app-standalone.lua +++ b/devices-api/app-standalone.lua @@ -94,7 +94,7 @@ end -- Seed initial data local function seed_db() 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 if res and res[1] then count = tonumber(res[1].total) or 0 @@ -137,7 +137,8 @@ local function seed_db() for _, device in ipairs(devices) do 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.manufacturer, device.release_year, @@ -205,17 +206,17 @@ end local function b64(data) -- Minimal Base64 - local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' - return ((data:gsub('.', function(x) - local r,b='',x:byte() - for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end - return r; - end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) + local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + return ((data:gsub('.', function(x) + local r, byte = '', x:byte() + for i = 8, 1, -1 do r = r .. (byte % 2^i - byte % 2^(i - 1) > 0 and '1' or '0') end + return r + end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x) if (#x < 6) then return '' end local c=0 for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end - return b:sub(c+1,c+1) - end)..({ '', '==', '=' })[#data%3+1]) + return b:sub(c + 1, c + 1) + end) .. ({ '', '==', '=' })[#data % 3 + 1]) end local function encode_ws_frame(payload) @@ -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) else -- 64-bit length not implemented for simplicity - header = header .. string.char(127) .. string.rep(string.char(0), 4) .. - string.char(math.floor(len / 16777216) % 256) .. - string.char(math.floor(len / 65536) % 256) .. - string.char(math.floor(len / 256) % 256) .. - string.char(len % 256) + header = header .. string.char(127) .. string.rep(string.char(0), 4) + .. string.char(math.floor(len / 16777216) % 256) + .. string.char(math.floor(len / 65536) % 256) + .. string.char(math.floor(len / 256) % 256) + .. string.char(len % 256) end return header .. payload end @@ -275,7 +276,8 @@ end function Device.create(data, request_id) local res = db.with_connection(function(conn) 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.manufacturer, (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) local updates = {} 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.manufacturer) then table.insert(updates, "manufacturer = " .. pg:escape_literal(data.manufacturer)) end - if not is_json_null(data.release_year) then 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 not is_json_null(data.name) then + table.insert(updates, "name = " .. pg:escape_literal(data.name)) + end + if not is_json_null(data.manufacturer) then + table.insert(updates, "manufacturer = " .. pg:escape_literal(data.manufacturer)) + end + if not is_json_null(data.release_year) then + 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 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 -- or cases where headers are partially sent. client:settimeout(0.1) - local line, err = client:receive("*l") + local line = client:receive("*l") if not line or line == "" then break end local key, value = line:match("^([^:]+):%s*(.*)$") if key then @@ -588,7 +606,7 @@ function app.start() local red = get_redis_connection() if red then -- Note: redis-lua's subscribe() sets the connection into a subscription state. - local ok, err = pcall(function() return red:subscribe("devices:events") end) + local ok, err = pcall(function() return red:subscribe("devices:events") end) if ok then -- Set the underlying socket to a very small timeout for non-blocking feel pcall(function() @@ -626,9 +644,8 @@ function app.start() for i, c in ipairs(clients) do local line, err = c.socket:receive("*l") if line then - local method, full_path = parse_request(line) local headers = parse_headers(c.socket) - + if headers["upgrade"] == "websocket" then -- Handle WebSocket Handshake local key = headers["sec-websocket-key"] diff --git a/devices-api/db.lua b/devices-api/db.lua index 3ba17b0..e497db9 100644 --- a/devices-api/db.lua +++ b/devices-api/db.lua @@ -1,6 +1,5 @@ -- PostgreSQL connection pool using pgmoon local pgmoon = require("pgmoon") -local cjson = require("cjson") local DB_HOST = os.getenv("DB_HOST") or "localhost" local DB_PORT = tonumber(os.getenv("DB_PORT")) or 5432 diff --git a/devices-worker/worker.lua b/devices-worker/worker.lua index c1d7639..df116b7 100644 --- a/devices-worker/worker.lua +++ b/devices-worker/worker.lua @@ -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 }) end -local function process_event(red, event_json) +local function process_event(_red, event_json) local ok_decode, event = pcall(cjson.decode, event_json) if not ok_decode or not event then return false, "decode_failed" diff --git a/frontend/alpinejs/.vscode/launch.json b/frontend/.vscode/launch.json similarity index 100% rename from frontend/alpinejs/.vscode/launch.json rename to frontend/.vscode/launch.json diff --git a/frontend/alpinejs/.vscode/settings.json b/frontend/.vscode/settings.json similarity index 100% rename from frontend/alpinejs/.vscode/settings.json rename to frontend/.vscode/settings.json diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a59edaf..0655323 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -9,8 +9,8 @@ ENV PORT=${PORT} WORKDIR /app # Copy the frontend files -# Since the Dockerfile is in the root and files are in alpinejs/, we copy from there -COPY alpinejs/ . +# Since the Dockerfile is in the root and files are in ./, we copy from there +COPY . . # Ensure the deno user owns the directory so it can write public/config.js RUN chown -R deno:deno /app diff --git a/frontend/alpinejs/config.json b/frontend/config.json similarity index 100% rename from frontend/alpinejs/config.json rename to frontend/config.json diff --git a/frontend/alpinejs/deno.json b/frontend/deno.json similarity index 61% rename from frontend/alpinejs/deno.json rename to frontend/deno.json index b34891c..904e58f 100644 --- a/frontend/alpinejs/deno.json +++ b/frontend/deno.json @@ -3,6 +3,7 @@ "start": "deno run --allow-net --allow-read --allow-write --allow-env --allow-run --watch main.ts" }, "imports": { - "@dx/alpine-server": "jsr:@dx/alpine-server" + "@dx/alpine-server": "jsr:@dx/alpine-server", + "@oak/oak": "jsr:@oak/oak@^17.2.0" } } diff --git a/frontend/alpinejs/deno.lock b/frontend/deno.lock similarity index 98% rename from frontend/alpinejs/deno.lock rename to frontend/deno.lock index 42d6197..120fbc7 100644 --- a/frontend/alpinejs/deno.lock +++ b/frontend/deno.lock @@ -114,7 +114,8 @@ }, "workspace": { "dependencies": [ - "jsr:@dx/alpine-server@*" + "jsr:@dx/alpine-server@*", + "jsr:@oak/oak@^17.2.0" ] } } diff --git a/frontend/alpinejs/main.ts b/frontend/main.ts similarity index 52% rename from frontend/alpinejs/main.ts rename to frontend/main.ts index 794c13b..df705fa 100644 --- a/frontend/alpinejs/main.ts +++ b/frontend/main.ts @@ -1,23 +1,24 @@ -import { AlpineApp } from '@dx/alpine-server'; -import { Router } from 'jsr:@oak/oak'; +import { AlpineApp } from "@dx/alpine-server"; +import { Context, Router } from "@oak/oak"; let config = { - api_url: Deno.env.get('API_URL') || 'http://localhost:8080', - port: parseInt(Deno.env.get('PORT') || '8090') + api_url: Deno.env.get("API_URL") || "http://localhost:8080", + port: parseInt(Deno.env.get("PORT") || "8090"), }; try { - const configFile = await Deno.readTextFile('./config.json'); + const configFile = await Deno.readTextFile("./config.json"); const fileConfig = JSON.parse(configFile); config = { ...config, ...fileConfig }; -} catch (e) { - console.warn('Could not read config.json, using defaults', e.message); + // deno-lint-ignore no-explicit-any +} catch (e: any) { + console.warn("Could not read config.json, using defaults", e.message); } const app = new AlpineApp({ app: { dev: true, - staticFilesPath: './public', + staticFilesPath: "./public", }, oak: { listenOptions: { port: config.port }, @@ -25,17 +26,17 @@ const app = new AlpineApp({ }); const healthRouter = new Router(); -healthRouter.get('/health', (ctx) => { - ctx.response.body = { status: 'ok' }; +healthRouter.get("/health", (ctx: Context) => { + ctx.response.body = { status: "ok" }; ctx.response.status = 200; }); app.append(healthRouter); -app.use(async (ctx, next) => { +app.use(async (ctx: Context, next: () => Promise) => { await next(); - const contentType = ctx.response.headers.get('content-type') || ''; - if (contentType.includes('text/html')) { - let csp = ctx.response.headers.get('Content-Security-Policy'); + const contentType = ctx.response.headers.get("content-type") || ""; + if (contentType.includes("text/html")) { + let csp = ctx.response.headers.get("Content-Security-Policy"); if (!csp) { // 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. @@ -52,10 +53,13 @@ app.use(async (ctx, next) => { "font-src 'self'", "connect-src 'self'", "media-src 'self'", - ].join('; '); + ].join("; "); } - const updatedCsp = csp.replace("connect-src 'self'", `connect-src 'self' ${config.api_url} ws:`); - ctx.response.headers.set('Content-Security-Policy', updatedCsp); + const updatedCsp = csp.replace( + "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}`); // 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(); diff --git a/frontend/alpinejs/public/alpine.min.js b/frontend/public/alpine.min.js similarity index 100% rename from frontend/alpinejs/public/alpine.min.js rename to frontend/public/alpine.min.js diff --git a/frontend/alpinejs/public/config.js b/frontend/public/config.js similarity index 100% rename from frontend/alpinejs/public/config.js rename to frontend/public/config.js diff --git a/frontend/alpinejs/public/favicon.ico b/frontend/public/favicon.ico similarity index 100% rename from frontend/alpinejs/public/favicon.ico rename to frontend/public/favicon.ico diff --git a/frontend/alpinejs/public/index.html b/frontend/public/index.html similarity index 100% rename from frontend/alpinejs/public/index.html rename to frontend/public/index.html diff --git a/frontend/alpinejs/public/main.js b/frontend/public/main.js similarity index 100% rename from frontend/alpinejs/public/main.js rename to frontend/public/main.js diff --git a/frontend/alpinejs/public/style.css b/frontend/public/style.css similarity index 100% rename from frontend/alpinejs/public/style.css rename to frontend/public/style.css