more roadmap features
All checks were successful
Build and Push Docker Image / build (push) Successful in 16s

This commit is contained in:
jason
2026-03-30 14:34:10 -05:00
parent ba2a76f7dd
commit a0c1ae9703
7 changed files with 1855 additions and 10 deletions

View File

@@ -12,6 +12,8 @@ import mealsRouter from './routes/meals';
import messagesRouter from './routes/messages';
import countdownsRouter from './routes/countdowns';
import photosRouter from './routes/photos';
import dashboardRouter from './routes/dashboard';
import weatherRouter from './routes/weather';
// Run DB migrations on startup — aborts if any migration fails
runMigrations();
@@ -32,6 +34,8 @@ app.use('/api/meals', mealsRouter);
app.use('/api/messages', messagesRouter);
app.use('/api/countdowns', countdownsRouter);
app.use('/api/photos', photosRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/weather', weatherRouter);
// Serve built client — in Docker the client dist is copied here at build time
const CLIENT_DIST = path.join(__dirname, '../../client/dist');

View File

@@ -0,0 +1,69 @@
import { Router } from 'express';
import db from '../db/db';
const router = Router();
router.get('/', (_req, res) => {
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
const nowIso = new Date().toISOString().replace('T', ' ').slice(0, 19);
const in7days = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
.toISOString().replace('T', ' ').slice(0, 19);
// Today's meal
const meal_today = db.prepare('SELECT * FROM meals WHERE date = ?').get(today) ?? null;
// Upcoming events in the next 7 days
const upcoming_events = db.prepare(`
SELECT e.*, m.name AS member_name, m.color AS member_color
FROM events e
LEFT JOIN members m ON e.member_id = m.id
WHERE e.start_at >= ? AND e.start_at <= ?
ORDER BY e.start_at ASC
LIMIT 10
`).all(nowIso, in7days);
// Pending chores (up to 10 for preview)
const pending_chores = db.prepare(`
SELECT c.*, m.name AS member_name, m.color AS member_color,
(SELECT COUNT(*) FROM chore_completions cc WHERE cc.chore_id = c.id) AS completion_count
FROM chores c
LEFT JOIN members m ON c.member_id = m.id
WHERE c.status = 'pending'
ORDER BY c.due_date ASC NULLS LAST
LIMIT 10
`).all();
// Unchecked shopping items count
const shoppingRow = db.prepare(
'SELECT COUNT(*) AS count FROM shopping_items WHERE checked = 0'
).get() as { count: number };
// Pinned messages (not expired)
const pinned_messages = db.prepare(`
SELECT msg.*, m.name AS member_name, m.color AS member_color
FROM messages msg
LEFT JOIN members m ON msg.member_id = m.id
WHERE msg.pinned = 1
AND (msg.expires_at IS NULL OR msg.expires_at > ?)
ORDER BY msg.created_at DESC
LIMIT 6
`).all(nowIso);
// Countdowns marked for dashboard
const countdowns = db.prepare(`
SELECT * FROM countdowns
WHERE show_on_dashboard = 1
ORDER BY target_date ASC
`).all();
res.json({
meal_today,
upcoming_events,
pending_chores,
shopping_unchecked: (shoppingRow as any).count as number,
pinned_messages,
countdowns,
});
});
export default router;

View File

@@ -0,0 +1,54 @@
import { Router } from 'express';
import https from 'https';
import db from '../db/db';
const router = Router();
function getSetting(key: string): string {
return (db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as any)?.value ?? '';
}
router.get('/', (_req, res) => {
const apiKey = getSetting('weather_api_key').trim();
const location = getSetting('weather_location').trim();
const units = getSetting('weather_units').trim() || 'imperial';
if (!apiKey || !location) {
return res.json({ configured: false });
}
const url =
`https://api.openweathermap.org/data/2.5/weather` +
`?q=${encodeURIComponent(location)}` +
`&appid=${encodeURIComponent(apiKey)}` +
`&units=${encodeURIComponent(units)}`;
https.get(url, (apiRes) => {
let raw = '';
apiRes.on('data', (chunk: Buffer) => { raw += chunk.toString(); });
apiRes.on('end', () => {
try {
const data = JSON.parse(raw);
if (apiRes.statusCode !== 200) {
return res.json({ configured: true, error: data.message ?? 'Weather API error' });
}
res.json({
configured: true,
city: data.name as string,
temp: Math.round(data.main.temp as number),
feels_like: Math.round(data.main.feels_like as number),
humidity: data.main.humidity as number,
description: (data.weather?.[0]?.description ?? '') as string,
icon: (data.weather?.[0]?.icon ?? '') as string,
units,
});
} catch {
res.json({ configured: true, error: 'Failed to parse weather response' });
}
});
}).on('error', (err: Error) => {
res.json({ configured: true, error: err.message });
});
});
export default router;