1 Commits
1.0.0 ... main

Author SHA1 Message Date
f3d7408e14 feat: implement logging functionality and improve API endpoint handling in backend; refactor frontend to use dynamic API base
All checks were successful
Build and publish Docker image / build-and-push (release) Successful in 14s
2025-11-12 00:55:44 +01:00
5 changed files with 134 additions and 34 deletions

View File

@@ -7,6 +7,32 @@ const fs = require('fs');
const app = express(); const app = express();
// lightweight level logger
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
const LOG_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
function log(level, ...args) {
if (LOG_ORDER[level] <= LOG_ORDER[LOG_LEVEL]) {
const ts = new Date().toISOString();
const out = `[${ts}] [${level.toUpperCase()}]`;
if (level === 'error') console.error(out, ...args);
else if (level === 'warn') console.warn(out, ...args);
else if (level === 'debug' && console.debug) console.debug(out, ...args);
else console.log(out, ...args);
}
}
// optional request logger (only when debug)
if (LOG_LEVEL === 'debug') {
app.use((req, res, next) => {
const start = Date.now();
res.once('finish', () => {
const ms = Date.now() - start;
log('debug', `${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms`);
});
next();
});
}
// Use DATA_DIR env var if set, otherwise use local "data" folder // Use DATA_DIR env var if set, otherwise use local "data" folder
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data'); const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
@@ -15,7 +41,7 @@ const db = new sqlite3.Database(dbPath);
const upload = multer({ dest: path.join(DATA_DIR, 'uploads/') }); const upload = multer({ dest: path.join(DATA_DIR, 'uploads/') });
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use('/uploads', express.static(path.join(DATA_DIR, 'uploads'))); app.use('/uploads', express.static(path.join(DATA_DIR, 'uploads'), { maxAge: '1d' }));
// Create uploads directory if it doesn't exist // Create uploads directory if it doesn't exist
fs.mkdirSync(path.join(DATA_DIR, 'uploads'), { recursive: true }); fs.mkdirSync(path.join(DATA_DIR, 'uploads'), { recursive: true });
@@ -40,6 +66,8 @@ app.get('/api/counters', (req, res) => {
id: Number(row.id), id: Number(row.id),
value: Number(row.value) value: Number(row.value)
})); }));
// log names at debug level (won't clutter info logs)
log('debug', `Fetched ${counters.length} counters:`, counters.map(c => c.name));
res.json(counters); res.json(counters);
}); });
}); });
@@ -49,7 +77,11 @@ app.post('/api/counters', upload.single('image'), (req, res) => {
const { name = 'Counter', value = 0 } = req.body; const { name = 'Counter', value = 0 } = req.body;
const image = req.file ? `/uploads/${req.file.filename}` : null; const image = req.file ? `/uploads/${req.file.filename}` : null;
db.run('INSERT INTO counters (name, value, image) VALUES (?, ?, ?)', [name, value, image], function (err) { db.run('INSERT INTO counters (name, value, image) VALUES (?, ?, ?)', [name, value, image], function (err) {
if (err) return res.status(500).json({ error: err.message }); if (err) {
log('error', 'Create counter failed:', err.message);
return res.status(500).json({ error: err.message });
}
log('info', `Created counter "${name}" (id: ${this.lastID})`);
res.json({ id: Number(this.lastID), name, value: Number(value), image }); res.json({ id: Number(this.lastID), name, value: Number(value), image });
}); });
}); });
@@ -60,6 +92,9 @@ app.put('/api/counters/:id', upload.single('image'), (req, res) => {
const id = req.params.id; const id = req.params.id;
let image = null; let image = null;
// Log incoming request at debug level only
log('debug', `PUT /api/counters/${id} body:`, req.body, 'file:', !!req.file);
// If a new image is uploaded, get its path // If a new image is uploaded, get its path
if (req.file) { if (req.file) {
image = `/uploads/${req.file.filename}`; image = `/uploads/${req.file.filename}`;
@@ -83,17 +118,46 @@ app.put('/api/counters/:id', upload.single('image'), (req, res) => {
params.push(id); params.push(id);
const sql = `UPDATE counters SET ${fields.join(', ')} WHERE id = ?`; const sql = `UPDATE counters SET ${fields.join(', ')} WHERE id = ?`;
// Log SQL and params at debug level
log('debug', `Updating counter ${id}`, sql, params);
db.run(sql, params, function (err) { db.run(sql, params, function (err) {
if (err) return res.status(500).json({ error: err.message }); if (err) {
res.json({ updated: this.changes }); log('error', `DB error updating counter ${id}:`, err.message);
return res.status(500).json({ error: err.message });
}
// Fetch the updated row to log the current name and value
db.get('SELECT name, value FROM counters WHERE id = ?', [id], (err2, row2) => {
if (err2) {
log('error', `Failed to fetch updated counter ${id}:`, err2.message);
} else if (row2) {
log('info', `Counter "${row2.name}" (id: ${id}) updated to value ${Number(row2.value)}`);
} else {
log('info', `Counter ${id} updated, changes: ${this.changes}`);
}
res.json({ updated: this.changes });
});
}); });
}); });
// Delete a counter // Delete a counter (fetch name first so we can log it)
app.delete('/api/counters/:id', (req, res) => { app.delete('/api/counters/:id', (req, res) => {
db.run('DELETE FROM counters WHERE id = ?', [req.params.id], function (err) { const id = req.params.id;
if (err) return res.status(500).json({ error: err.message }); db.get('SELECT name FROM counters WHERE id = ?', [id], (err, row) => {
res.json({ deleted: this.changes }); if (err) {
log('error', `Lookup before delete failed for id ${id}:`, err.message);
return res.status(500).json({ error: err.message });
}
const name = row && row.name ? row.name : null;
db.run('DELETE FROM counters WHERE id = ?', [id], function (err) {
if (err) {
log('error', `Delete counter ${name} with ${id} failed:`, err.message);
return res.status(500).json({ error: err.message });
}
log('info', `Deleted counter${name ? ` "${name}"` : ''} (id: ${id})`);
res.json({ deleted: this.changes });
});
}); });
}); });
@@ -109,7 +173,7 @@ app.get('/*path', (req, res) => {
const PORT = 3000; const PORT = 3000;
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`API server running on http://localhost:${PORT}`); console.log(`API server listening on port: ${PORT}`);
}); });
process.on('SIGTERM', () => { process.on('SIGTERM', () => {

View File

@@ -2,6 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import CounterCard from "./CounterCard.svelte"; import CounterCard from "./CounterCard.svelte";
import AddCounterCard from "./AddCounterCard.svelte"; import AddCounterCard from "./AddCounterCard.svelte";
import { API_COUNTERS } from "./config";
type Counter = { type Counter = {
id: number; id: number;
@@ -13,19 +14,17 @@
let counters: Counter[] = []; let counters: Counter[] = [];
let loading = true; let loading = true;
// API endpoint // Use a relative API base so it works no matter the host/origin
const API = "http://localhost:3000/api/counters"; const API = API_COUNTERS;
const CACHE_KEY = "tally-counters-cache"; const CACHE_KEY = "tally-counters-cache";
async function fetchCounters() { async function fetchCounters() {
loading = true; loading = true;
// Try to load from cache first
const cached = localStorage.getItem(CACHE_KEY); const cached = localStorage.getItem(CACHE_KEY);
if (cached) { if (cached) {
counters = JSON.parse(cached); counters = JSON.parse(cached);
loading = false; loading = false;
} }
// Always fetch fresh data in the background
const res = await fetch(API); const res = await fetch(API);
const fresh = await res.json(); const fresh = await res.json();
counters = fresh; counters = fresh;

View File

@@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import { clickOutside } from "./clickOutside"; import { clickOutside } from "./clickOutside";
import { API_BASE } from "./config";
import { onDestroy } from 'svelte';
export let counter: { export let counter: {
id: number; id: number;
@@ -21,16 +23,38 @@
export let onSetEditImage: (file: File | null) => void; export let onSetEditImage: (file: File | null) => void;
export let onCancelEdit: (id: number) => void; export let onCancelEdit: (id: number) => void;
// For previewing the new image // Build a safe image URL:
let previewUrl: string | null = null; function imageUrl(img?: string | null) {
if (!img) return null;
$: if (editImage) { // already absolute
previewUrl = URL.createObjectURL(editImage); if (img.startsWith("http") || img.startsWith("//")) return img;
} else { // ensure no double slashes when API_BASE is set
previewUrl = null; const base = (API_BASE || "").replace(/\/$/, "");
if (base) return `${base}${img}`;
return img; // relative path like "/uploads/..."
} }
const BACKEND_URL = "http://localhost:3000"; // preview management (revoke previous object URL)
let previewUrl: string | null = null;
let _prevObjectUrl: string | null = null;
$: {
if (editImage) {
const u = URL.createObjectURL(editImage);
if (_prevObjectUrl) URL.revokeObjectURL(_prevObjectUrl);
previewUrl = u;
_prevObjectUrl = u;
} else {
if (_prevObjectUrl) {
URL.revokeObjectURL(_prevObjectUrl);
_prevObjectUrl = null;
}
previewUrl = null;
}
}
onDestroy(() => {
if (_prevObjectUrl) URL.revokeObjectURL(_prevObjectUrl);
});
</script> </script>
<div <div
@@ -56,11 +80,7 @@
{#if previewUrl} {#if previewUrl}
<img src={previewUrl} alt="Preview" class="card-img" /> <img src={previewUrl} alt="Preview" class="card-img" />
{:else if counter.image} {:else if counter.image}
<img <img src={imageUrl(counter.image)} alt={counter.name} class="card-img" />
src={`${BACKEND_URL}${counter.image}`}
alt={counter.name}
class="card-img"
/>
{:else} {:else}
<div class="card-img placeholder"></div> <div class="card-img placeholder"></div>
{/if} {/if}
@@ -89,10 +109,10 @@
<div class="card-img-container"> <div class="card-img-container">
{#if counter.image} {#if counter.image}
<img <img
src={`${BACKEND_URL}${counter.image}`} src={imageUrl(counter.image)}
alt={counter.name} alt={counter.name}
class="card-img" class="card-img"
/> />
{:else} {:else}
<div class="card-img placeholder">No image</div> <div class="card-img placeholder">No image</div>
{/if} {/if}

3
frontend/src/config.ts Normal file
View File

@@ -0,0 +1,3 @@
export const API_BASE = import.meta.env.VITE_API_URL ?? ''; // empty = same origin
export const API_COUNTERS = `${API_BASE}/api/counters`;
export const UPLOADS_BASE = `${API_BASE}/uploads`;

View File

@@ -1,7 +1,21 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte' import { svelte } from '@sveltejs/vite-plugin-svelte';
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()], plugins: [svelte()],
}) server: {
port: Number(process.env.VITE_PORT) || 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
},
'/uploads': {
target: 'http://localhost:3000',
changeOrigin: true,
secure: false,
},
},
},
});