8715589cbb
- /admin: first-visit password setup (scrypt), signed 30-day session cookie, calendars add/remove/reorder/rename/color/hide, masked URLs, per-feed test, history (20) with restore; /photo-admin -> /admin#photos placeholder - settings in local /data (settings.json + auth.json + history/), seeded once from the calendar secret; dashboard + health read from settings - start.mjs drops privileges after reading secrets and runs a front proxy that stamps the real client IP; admin refuses non-LAN and proxied requests - deploy: HOBBS_DATA_DIR=/var/lib/hobbs-organizer/data, image 1.1.0 (1.0.0 kept) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
48 lines
1.9 KiB
TypeScript
48 lines
1.9 KiB
TypeScript
import { addCalendarDays, organizerDate, readCalendar } from "../../_lib/calendar-feed";
|
|
import { readSettings } from "../../_lib/store";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function GET() {
|
|
const { calendars } = await readSettings();
|
|
const feeds = calendars.filter((calendar) => calendar.enabled);
|
|
if (!feeds.length) {
|
|
return Response.json({ items: [], calendarsAvailable: 0, calendarsTotal: 0, updatedAt: new Date().toISOString() }, { status: 503 });
|
|
}
|
|
|
|
const now = Date.now();
|
|
const windowStart = now - 45 * 24 * 60 * 60 * 1_000;
|
|
const horizon = now + 60 * 24 * 60 * 60 * 1_000;
|
|
const today = organizerDate(new Date(now));
|
|
const windowStartDate = addCalendarDays(today, -45);
|
|
const horizonDate = addCalendarDays(today, 60);
|
|
const results = await Promise.allSettled(
|
|
feeds.map((feed, feedIndex) =>
|
|
readCalendar(feed.url, { feedIndex, tone: feed.tone, nameOverride: feed.name }, windowStart, horizon, windowStartDate, horizonDate),
|
|
),
|
|
);
|
|
const successful = results.filter((result): result is PromiseFulfilledResult<Awaited<ReturnType<typeof readCalendar>>> => result.status === "fulfilled");
|
|
const items = successful
|
|
.flatMap((result) => result.value.items)
|
|
.filter((item, index, all) => all.findIndex((candidate) => candidate.id === item.id) === index)
|
|
.sort((left, right) => {
|
|
const leftValue = left.allDay ? `${left.start}T00:00:00` : left.start;
|
|
const rightValue = right.allDay ? `${right.start}T00:00:00` : right.start;
|
|
return leftValue.localeCompare(rightValue);
|
|
})
|
|
.slice(0, 160);
|
|
|
|
return Response.json(
|
|
{
|
|
items,
|
|
calendarsAvailable: successful.length,
|
|
calendarsTotal: feeds.length,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
{
|
|
status: successful.length ? 200 : 503,
|
|
headers: { "Cache-Control": "private, max-age=60, stale-while-revalidate=240" },
|
|
},
|
|
);
|
|
}
|