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

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1/**
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}