Files
silo-server/web/src/lib/date.ts
T
QuickandClaude Fable 5 3d2368aed7 feat(notifications): admin server channels broadcasting new content and request activity
Add admin-owned broadcast destinations ("community channels"): Discord or
generic webhooks fed straight from release_events by a per-channel watermark
sweep, announcing newly added movies/episodes as grouped digest posts plus
configurable media request lifecycle events (submitted/approved/declined/
fulfilled).

- Extend release_events with a kind discriminator and add a movie
  availability spine (movie_availability + kind-keyed
  notification_content_seed_state; first full scan seeds silently so
  upgrades never flood the movie back catalog)
- Sweep worker reads events by (created_at, id) cursor with batch-window
  grouping, per-channel backoff, and auto-disable; request events post
  best-effort via new requests.LifecycleNotifier hooks
- Reuse the webhook stack throughout: URL encryption (new AAD namespace),
  SSRF guard, embed limits, HMAC signing; shared type/name validation
  extracted for both services
- Admin CRUD API under /admin/notifications/server-channels and a Server
  Channels section in the notifications admin settings UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:04:35 -04:00

42 lines
1.3 KiB
TypeScript

/**
* Compact relative-time label ("just now", "5m ago", "3h ago", "2d ago").
* Returns null for missing or unparseable values.
*/
export function formatRelativeTime(value: string | null): string | null {
if (!value) {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
const diffMinutes = Math.round((Date.now() - date.getTime()) / 60_000);
if (diffMinutes < 1) {
return "just now";
}
if (diffMinutes < 60) {
return `${diffMinutes}m ago`;
}
const diffHours = Math.round(diffMinutes / 60);
if (diffHours < 24) {
return `${diffHours}h ago`;
}
return `${Math.round(diffHours / 24)}d ago`;
}
export function formatBirthDate(dateStr: string): string {
const date = new Date(dateStr + "T00:00:00");
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
export function computeAge(birthStr: string, deathStr?: string): number {
const birth = new Date(birthStr + "T00:00:00");
const ref = deathStr ? new Date(deathStr + "T00:00:00") : new Date();
let age = ref.getFullYear() - birth.getFullYear();
const monthDiff = ref.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && ref.getDate() < birth.getDate())) {
age--;
}
return age;
}