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>
42 lines
1.3 KiB
TypeScript
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;
|
|
}
|