🧱 update CI workflow and remove alpinejs frontend files
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3
frontend/alpinejs/deno.lock → frontend/deno.lock
generated
3
frontend/alpinejs/deno.lock → frontend/deno.lock
generated
@@ -114,7 +114,8 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@dx/alpine-server@*"
|
"jsr:@dx/alpine-server@*",
|
||||||
|
"jsr:@oak/oak@^17.2.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
Before Width: | Height: | Size: 699 B After Width: | Height: | Size: 699 B |
Reference in New Issue
Block a user