Local Node app that fetches a Google Calendar ICS URL server-side (to avoid browser CORS restrictions) and renders a two-year overview with each day split into morning/afternoon/evening segments, colored by emoji found at the start of event titles (configured in tags.json). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
88 lines
2.5 KiB
JavaScript
88 lines
2.5 KiB
JavaScript
'use strict';
|
|
const http = require('node:http');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { buildOverview } = require('./days.js');
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const TAGS_PATH = path.join(__dirname, 'tags.json');
|
|
const INDEX_PATH = path.join(__dirname, 'public', 'index.html');
|
|
|
|
function readTags() {
|
|
return JSON.parse(fs.readFileSync(TAGS_PATH, 'utf8'));
|
|
}
|
|
|
|
function sendJSON(res, status, body) {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(body));
|
|
}
|
|
|
|
async function handleOverview(res, query) {
|
|
const icsUrl = query.get('url');
|
|
if (!icsUrl) return sendJSON(res, 400, { error: 'missing url parameter' });
|
|
|
|
let parsedUrl;
|
|
try {
|
|
parsedUrl = new URL(icsUrl);
|
|
} catch {
|
|
return sendJSON(res, 400, { error: 'invalid url' });
|
|
}
|
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
return sendJSON(res, 400, { error: 'url must be http or https' });
|
|
}
|
|
|
|
let icsText;
|
|
try {
|
|
const fetchRes = await fetch(icsUrl);
|
|
if (!fetchRes.ok) throw new Error(`HTTP ${fetchRes.status}`);
|
|
icsText = await fetchRes.text();
|
|
} catch (err) {
|
|
return sendJSON(res, 502, { error: `could not fetch calendar: ${err.message}` });
|
|
}
|
|
|
|
const now = new Date();
|
|
const rangeStart = `${now.getFullYear()}-01-01`;
|
|
const rangeEnd = `${now.getFullYear() + 1}-12-31`;
|
|
|
|
let overview;
|
|
try {
|
|
overview = buildOverview(icsText, readTags(), rangeStart, rangeEnd);
|
|
} catch (err) {
|
|
return sendJSON(res, 500, { error: `could not parse calendar: ${err.message}` });
|
|
}
|
|
|
|
sendJSON(res, 200, { rangeStart, rangeEnd, days: overview });
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
|
|
if (req.method === 'GET' && url.pathname === '/') {
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(fs.readFileSync(INDEX_PATH));
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/tags.json') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(fs.readFileSync(TAGS_PATH));
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/api/default-url') {
|
|
return sendJSON(res, 200, { url: process.env.CALURL || null });
|
|
}
|
|
|
|
if (req.method === 'GET' && url.pathname === '/api/overview') {
|
|
await handleOverview(res, url.searchParams);
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('not found');
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`http://localhost:${PORT}`);
|
|
});
|