more roadmap features
All checks were successful
Build and Push Docker Image / build (push) Successful in 16s
All checks were successful
Build and Push Docker Image / build (push) Successful in 16s
This commit is contained in:
@@ -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');
|
||||
|
||||
69
apps/server/src/routes/dashboard.ts
Normal file
69
apps/server/src/routes/dashboard.ts
Normal 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;
|
||||
54
apps/server/src/routes/weather.ts
Normal file
54
apps/server/src/routes/weather.ts
Normal 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;
|
||||
Reference in New Issue
Block a user