Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3d7408e14 |
@@ -7,6 +7,32 @@ const fs = require('fs');
|
||||
|
||||
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
|
||||
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/') });
|
||||
app.use(cors());
|
||||
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
|
||||
fs.mkdirSync(path.join(DATA_DIR, 'uploads'), { recursive: true });
|
||||
@@ -40,6 +66,8 @@ app.get('/api/counters', (req, res) => {
|
||||
id: Number(row.id),
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -49,7 +77,11 @@ app.post('/api/counters', upload.single('image'), (req, res) => {
|
||||
const { name = 'Counter', value = 0 } = req.body;
|
||||
const image = req.file ? `/uploads/${req.file.filename}` : null;
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -60,6 +92,9 @@ app.put('/api/counters/:id', upload.single('image'), (req, res) => {
|
||||
const id = req.params.id;
|
||||
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 (req.file) {
|
||||
image = `/uploads/${req.file.filename}`;
|
||||
@@ -83,18 +118,47 @@ app.put('/api/counters/:id', upload.single('image'), (req, res) => {
|
||||
params.push(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) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (err) {
|
||||
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) => {
|
||||
db.run('DELETE FROM counters WHERE id = ?', [req.params.id], function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
const id = req.params.id;
|
||||
db.get('SELECT name FROM counters WHERE id = ?', [id], (err, row) => {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Serve static frontend files
|
||||
@@ -109,7 +173,7 @@ app.get('/*path', (req, res) => {
|
||||
|
||||
const PORT = 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`API server running on http://localhost:${PORT}`);
|
||||
console.log(`API server listening on port: ${PORT}`);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import CounterCard from "./CounterCard.svelte";
|
||||
import AddCounterCard from "./AddCounterCard.svelte";
|
||||
import { API_COUNTERS } from "./config";
|
||||
|
||||
type Counter = {
|
||||
id: number;
|
||||
@@ -13,19 +14,17 @@
|
||||
let counters: Counter[] = [];
|
||||
let loading = true;
|
||||
|
||||
// API endpoint
|
||||
const API = "http://localhost:3000/api/counters";
|
||||
// Use a relative API base so it works no matter the host/origin
|
||||
const API = API_COUNTERS;
|
||||
const CACHE_KEY = "tally-counters-cache";
|
||||
|
||||
async function fetchCounters() {
|
||||
loading = true;
|
||||
// Try to load from cache first
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (cached) {
|
||||
counters = JSON.parse(cached);
|
||||
loading = false;
|
||||
}
|
||||
// Always fetch fresh data in the background
|
||||
const res = await fetch(API);
|
||||
const fresh = await res.json();
|
||||
counters = fresh;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from "./clickOutside";
|
||||
import { API_BASE } from "./config";
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let counter: {
|
||||
id: number;
|
||||
@@ -21,16 +23,38 @@
|
||||
export let onSetEditImage: (file: File | null) => void;
|
||||
export let onCancelEdit: (id: number) => void;
|
||||
|
||||
// For previewing the new image
|
||||
let previewUrl: string | null = null;
|
||||
|
||||
$: if (editImage) {
|
||||
previewUrl = URL.createObjectURL(editImage);
|
||||
} else {
|
||||
previewUrl = null;
|
||||
// Build a safe image URL:
|
||||
function imageUrl(img?: string | null) {
|
||||
if (!img) return null;
|
||||
// already absolute
|
||||
if (img.startsWith("http") || img.startsWith("//")) return img;
|
||||
// ensure no double slashes when API_BASE is set
|
||||
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>
|
||||
|
||||
<div
|
||||
@@ -56,11 +80,7 @@
|
||||
{#if previewUrl}
|
||||
<img src={previewUrl} alt="Preview" class="card-img" />
|
||||
{:else if counter.image}
|
||||
<img
|
||||
src={`${BACKEND_URL}${counter.image}`}
|
||||
alt={counter.name}
|
||||
class="card-img"
|
||||
/>
|
||||
<img src={imageUrl(counter.image)} alt={counter.name} class="card-img" />
|
||||
{:else}
|
||||
<div class="card-img placeholder"></div>
|
||||
{/if}
|
||||
@@ -89,7 +109,7 @@
|
||||
<div class="card-img-container">
|
||||
{#if counter.image}
|
||||
<img
|
||||
src={`${BACKEND_URL}${counter.image}`}
|
||||
src={imageUrl(counter.image)}
|
||||
alt={counter.name}
|
||||
class="card-img"
|
||||
/>
|
||||
|
||||
3
frontend/src/config.ts
Normal file
3
frontend/src/config.ts
Normal 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`;
|
||||
@@ -1,7 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user