flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/status/src/feed.ts

86 lines2,954 bytesCodeBlame
1/**
2 * The status page's feeds: every public incident update and maintenance
3 * notice, newest first, as Atom (`/feed.xml`) and JSON Feed
4 * (`/feed.json`). No Workers imports, so it is tested under Node.
5 */
6import type { StatusIncident, StatusMaintenance } from "@g1t/contracts";
7
8import { INCIDENT_STATUS } from "./model.ts";
9
10export type FeedItem = {
11 /** Unique and stable: the page's address with the update's anchor. */
12 id: string;
13 url: string;
14 title: string;
15 text: string;
16 at: string;
17};
18
19/** How many items a feed carries. */
20export const FEED_SIZE = 50;
21
22/** Feed items from incidents and maintenance, newest first. */
23export function feedItems(incidents: StatusIncident[], maintenance: StatusMaintenance[], size = FEED_SIZE): FeedItem[] {
24 const items: FeedItem[] = [];
25 for (const incident of incidents) {
26 for (const update of incident.updates) {
27 items.push({
28 id: `${incident.url}#update-${update.id}`,
29 url: `${incident.url}#update-${update.id}`,
30 title: `${INCIDENT_STATUS[update.status]}: ${incident.title}`,
31 text: update.text,
32 at: update.at,
33 });
34 }
35 }
36 for (const m of maintenance) {
37 for (const update of m.updates) {
38 items.push({ id: `${m.url}#update-${update.id}`, url: `${m.url}#update-${update.id}`, title: `Maintenance: ${m.title}`, text: update.text, at: update.at });
39 }
40 }
41 return items.sort((a, b) => b.at.localeCompare(a.at)).slice(0, size);
42}
43
44function xml(text: string): string {
45 return text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]!);
46}
47
48export type FeedOptions = { origin: string; title: string; updated: string };
49
50export function atom(items: FeedItem[], { origin, title, updated }: FeedOptions): string {
51 const entries = items
52 .map(
53 (item) => `<entry>
54<id>${xml(item.id)}</id>
55<title>${xml(item.title)}</title>
56<link rel="alternate" type="text/html" href="${xml(item.url)}"/>
57<updated>${xml(item.at)}</updated>
58<published>${xml(item.at)}</published>
59<content type="text">${xml(item.text)}</content>
60</entry>`,
61 )
62 .join("\n");
63 return `<?xml version="1.0" encoding="utf-8"?>
64<feed xmlns="http://www.w3.org/2005/Atom">
65<id>${xml(`${origin}/`)}</id>
66<title>${xml(title)}</title>
67<link rel="self" type="application/atom+xml" href="${xml(`${origin}/feed.xml`)}"/>
68<link rel="alternate" type="text/html" href="${xml(`${origin}/`)}"/>
69<updated>${xml(items[0]?.at ?? updated)}</updated>
70<author><name>g1t</name></author>
71${entries}
72</feed>
73`;
74}
75
76/** JSON Feed 1.1. */
77export function jsonFeed(items: FeedItem[], { origin, title }: FeedOptions): object {
78 return {
79 version: "https://jsonfeed.org/version/1.1",
80 title,
81 home_page_url: `${origin}/`,
82 feed_url: `${origin}/feed.json`,
83 language: "en",
84 items: items.map((item) => ({ id: item.id, url: item.url, title: item.title, content_text: item.text, date_published: item.at })),
85 };
86}