my own status page
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: init

+763
+5
.gitignore
··· 1 + node_modules/ 2 + dist/ 3 + .wrangler/ 4 + .dev.vars 5 + src/version.ts
+197
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "infra-status", 7 + "devDependencies": { 8 + "@cloudflare/workers-types": "^4.20250224.0", 9 + "typescript": "^5.7.3", 10 + "wrangler": "^4.5.1", 11 + }, 12 + }, 13 + }, 14 + "packages": { 15 + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], 16 + 17 + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.15.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw=="], 18 + 19 + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260301.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ=="], 20 + 21 + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260301.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ=="], 22 + 23 + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260301.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ=="], 24 + 25 + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260301.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw=="], 26 + 27 + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260301.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A=="], 28 + 29 + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260306.1", "", {}, "sha512-1gtiB0nm0Uji6VKHprvL1ZyFtdHZSR907lU2fbBioMurJAF4tQPoafJFJp4oeViUiMVUEqkp0Eh0dcbcKoHoow=="], 30 + 31 + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], 32 + 33 + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], 34 + 35 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], 36 + 37 + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], 38 + 39 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], 40 + 41 + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], 42 + 43 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], 44 + 45 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], 46 + 47 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], 48 + 49 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], 50 + 51 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], 52 + 53 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], 54 + 55 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], 56 + 57 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], 58 + 59 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], 60 + 61 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], 62 + 63 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], 64 + 65 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], 66 + 67 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], 68 + 69 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], 70 + 71 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], 72 + 73 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], 74 + 75 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], 76 + 77 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], 78 + 79 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], 80 + 81 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], 82 + 83 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], 84 + 85 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], 86 + 87 + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], 88 + 89 + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], 90 + 91 + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], 92 + 93 + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], 94 + 95 + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], 96 + 97 + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], 98 + 99 + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], 100 + 101 + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], 102 + 103 + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], 104 + 105 + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], 106 + 107 + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], 108 + 109 + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], 110 + 111 + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], 112 + 113 + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], 114 + 115 + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], 116 + 117 + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], 118 + 119 + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], 120 + 121 + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], 122 + 123 + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], 124 + 125 + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], 126 + 127 + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], 128 + 129 + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], 130 + 131 + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], 132 + 133 + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], 134 + 135 + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], 136 + 137 + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 138 + 139 + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 140 + 141 + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], 142 + 143 + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], 144 + 145 + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], 146 + 147 + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], 148 + 149 + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], 150 + 151 + "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], 152 + 153 + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], 154 + 155 + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 156 + 157 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 158 + 159 + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], 160 + 161 + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], 162 + 163 + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 164 + 165 + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], 166 + 167 + "miniflare": ["miniflare@4.20260301.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260301.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog=="], 168 + 169 + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], 170 + 171 + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 172 + 173 + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], 174 + 175 + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], 176 + 177 + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], 178 + 179 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 180 + 181 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 182 + 183 + "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], 184 + 185 + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], 186 + 187 + "workerd": ["workerd@1.20260301.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260301.1", "@cloudflare/workerd-darwin-arm64": "1.20260301.1", "@cloudflare/workerd-linux-64": "1.20260301.1", "@cloudflare/workerd-linux-arm64": "1.20260301.1", "@cloudflare/workerd-windows-64": "1.20260301.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw=="], 188 + 189 + "wrangler": ["wrangler@4.71.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.15.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260301.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260226.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-j6pSGAncOLNQDRzqtp0EqzYj52CldDP7uz/C9cxVrIgqa5p+cc0b4pIwnapZZAGv9E1Loa3tmPD0aXonH7KTkw=="], 190 + 191 + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], 192 + 193 + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], 194 + 195 + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], 196 + } 197 + }
+9
migrations/0001_init.sql
··· 1 + CREATE TABLE pings ( 2 + id INTEGER PRIMARY KEY AUTOINCREMENT, 3 + service_id TEXT NOT NULL, 4 + timestamp INTEGER NOT NULL, 5 + status TEXT NOT NULL CHECK (status IN ('up', 'degraded', 'down')), 6 + latency_ms INTEGER 7 + ); 8 + 9 + CREATE INDEX idx_pings_service_ts ON pings (service_id, timestamp DESC);
+16
package.json
··· 1 + { 2 + "name": "infra-status", 3 + "private": true, 4 + "scripts": { 5 + "prebuild": "echo \"export const COMMIT_SHA = \\\"$(git rev-parse --short HEAD)\\\";\" > src/version.ts", 6 + "dev": "bun run prebuild && wrangler dev", 7 + "deploy": "bun run prebuild && wrangler deploy", 8 + "db:migrate": "wrangler d1 migrations apply status-db", 9 + "db:migrate:local": "wrangler d1 migrations apply status-db --local" 10 + }, 11 + "devDependencies": { 12 + "@cloudflare/workers-types": "^4.20250224.0", 13 + "typescript": "^5.7.3", 14 + "wrangler": "^4.5.1" 15 + } 16 + }
+98
src/db.ts
··· 1 + export async function insertPing( 2 + db: D1Database, 3 + service_id: string, 4 + status: string, 5 + latency_ms: number, 6 + ): Promise<void> { 7 + await db 8 + .prepare( 9 + "INSERT INTO pings (service_id, timestamp, status, latency_ms) VALUES (?, ?, ?, ?)", 10 + ) 11 + .bind(service_id, Math.floor(Date.now() / 1000), status, latency_ms) 12 + .run(); 13 + } 14 + 15 + export async function getLatestPing( 16 + db: D1Database, 17 + service_id: string, 18 + ): Promise<{ status: string; latency_ms: number | null } | null> { 19 + const row = await db 20 + .prepare( 21 + "SELECT status, latency_ms FROM pings WHERE service_id = ? ORDER BY timestamp DESC LIMIT 1", 22 + ) 23 + .bind(service_id) 24 + .first(); 25 + if (!row) return null; 26 + return { status: row.status as string, latency_ms: row.latency_ms as number | null }; 27 + } 28 + 29 + export async function getUptime7d( 30 + db: D1Database, 31 + service_id: string, 32 + ): Promise<number> { 33 + const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60; 34 + const total = await db 35 + .prepare( 36 + "SELECT COUNT(*) as count FROM pings WHERE service_id = ? AND timestamp >= ?", 37 + ) 38 + .bind(service_id, since) 39 + .first<{ count: number }>(); 40 + const up = await db 41 + .prepare( 42 + "SELECT COUNT(*) as count FROM pings WHERE service_id = ? AND timestamp >= ? AND status = 'up'", 43 + ) 44 + .bind(service_id, since) 45 + .first<{ count: number }>(); 46 + 47 + if (!total || total.count === 0) return 100; 48 + return Math.round(((up?.count ?? 0) / total.count) * 10000) / 100; 49 + } 50 + 51 + export async function getUptimeBuckets( 52 + db: D1Database, 53 + service_id: string, 54 + window_hours: number, 55 + ): Promise<{ timestamp: number; status: "up" | "degraded" | "down" }[]> { 56 + const since = Math.floor(Date.now() / 1000) - window_hours * 60 * 60; 57 + const rows = await db 58 + .prepare( 59 + `SELECT 60 + (timestamp / 3600) * 3600 AS bucket, 61 + status, 62 + COUNT(*) AS cnt 63 + FROM pings 64 + WHERE service_id = ? AND timestamp >= ? 65 + GROUP BY bucket, status 66 + ORDER BY bucket ASC`, 67 + ) 68 + .bind(service_id, since) 69 + .all(); 70 + 71 + const bucketMap = new Map<number, Map<string, number>>(); 72 + for (const row of rows.results) { 73 + const b = row.bucket as number; 74 + if (!bucketMap.has(b)) bucketMap.set(b, new Map()); 75 + bucketMap.get(b)!.set(row.status as string, row.cnt as number); 76 + } 77 + 78 + const result: { timestamp: number; status: "up" | "degraded" | "down" }[] = []; 79 + for (const [bucket, counts] of bucketMap) { 80 + let status: "up" | "degraded" | "down" = "up"; 81 + if (counts.has("down")) status = "down"; 82 + else if (counts.has("degraded")) status = "degraded"; 83 + result.push({ timestamp: bucket, status }); 84 + } 85 + 86 + return result; 87 + } 88 + 89 + export async function pruneOldPings( 90 + db: D1Database, 91 + days: number, 92 + ): Promise<void> { 93 + const cutoff = Math.floor(Date.now() / 1000) - days * 24 * 60 * 60; 94 + await db 95 + .prepare("DELETE FROM pings WHERE timestamp < ?") 96 + .bind(cutoff) 97 + .run(); 98 + }
+32
src/health.ts
··· 1 + import type { Service } from "./types"; 2 + 3 + interface HealthResult { 4 + status: "up" | "degraded" | "down"; 5 + latency_ms: number; 6 + } 7 + 8 + export async function checkHealth(service: Service): Promise<HealthResult> { 9 + if (!service.health_url) { 10 + return { status: "unknown" as "down", latency_ms: 0 }; 11 + } 12 + 13 + const start = Date.now(); 14 + try { 15 + const res = await fetch(service.health_url, { 16 + method: "GET", 17 + signal: AbortSignal.timeout(10_000), 18 + redirect: "follow", 19 + }); 20 + const latency_ms = Date.now() - start; 21 + 22 + if (res.status >= 200 && res.status < 300) { 23 + return { status: "up", latency_ms }; 24 + } 25 + if (res.status >= 500) { 26 + return { status: "degraded", latency_ms }; 27 + } 28 + return { status: "down", latency_ms }; 29 + } catch { 30 + return { status: "down", latency_ms: Date.now() - start }; 31 + } 32 + }
+52
src/index.ts
··· 1 + import type { Env } from "./types"; 2 + import { getManifest } from "./manifest"; 3 + import { checkHealth } from "./health"; 4 + import { insertPing, pruneOldPings } from "./db"; 5 + import { handleStatus } from "./routes/status"; 6 + import { handleUptime } from "./routes/uptime"; 7 + import { handleBadge, handleOverallBadge } from "./routes/badge"; 8 + import { handleIndex } from "./routes/index"; 9 + 10 + export default { 11 + async fetch(request: Request, env: Env): Promise<Response> { 12 + const url = new URL(request.url); 13 + const path = url.pathname; 14 + 15 + if (path === "/" || path === "") { 16 + return handleIndex(env); 17 + } 18 + 19 + if (path === "/api/status") { 20 + return handleStatus(env); 21 + } 22 + 23 + const uptimeMatch = path.match(/^\/api\/uptime\/(.+)$/); 24 + if (uptimeMatch) { 25 + return handleUptime(env, uptimeMatch[1], url); 26 + } 27 + 28 + if (path === "/badge") { 29 + return handleOverallBadge(env); 30 + } 31 + 32 + const badgeMatch = path.match(/^\/badge\/(.+)$/); 33 + if (badgeMatch) { 34 + return handleBadge(env, badgeMatch[1]); 35 + } 36 + 37 + return new Response("Not Found", { status: 404 }); 38 + }, 39 + 40 + async scheduled(_controller: ScheduledController, env: Env): Promise<void> { 41 + const manifest = await getManifest(env); 42 + 43 + const checks = manifest.map(async (svc) => { 44 + if (!svc.health_url) return; 45 + const result = await checkHealth(svc); 46 + await insertPing(env.DB, svc.name, result.status, result.latency_ms); 47 + }); 48 + 49 + await Promise.all(checks); 50 + await pruneOldPings(env.DB, 90); 51 + }, 52 + } satisfies ExportedHandler<Env>;
+20
src/manifest.ts
··· 1 + import type { Env, ServicesManifest } from "./types"; 2 + 3 + const MANIFEST_URL = "https://infra.dunkirk.sh/services.json"; 4 + const KV_KEY = "services_manifest"; 5 + const TTL_SECONDS = 300; 6 + 7 + export async function getManifest(env: Env): Promise<ServicesManifest> { 8 + const cached = await env.KV.get(KV_KEY, "json"); 9 + if (cached) return cached as ServicesManifest; 10 + 11 + const res = await fetch(MANIFEST_URL); 12 + if (!res.ok) throw new Error(`Failed to fetch manifest: ${res.status}`); 13 + 14 + const manifest: ServicesManifest = await res.json(); 15 + await env.KV.put(KV_KEY, JSON.stringify(manifest), { 16 + expirationTtl: TTL_SECONDS, 17 + }); 18 + 19 + return manifest; 20 + }
+122
src/routes/badge.ts
··· 1 + import type { Env } from "../types"; 2 + import { getManifest } from "../manifest"; 3 + import { getLatestPing, getUptime7d } from "../db"; 4 + 5 + const COLORS: Record<string, string> = { 6 + up: "#3cc068", 7 + degraded: "#f0ad4e", 8 + down: "#e05d44", 9 + unknown: "#9f9f9f", 10 + }; 11 + 12 + const STATUS_LABELS: Record<string, string> = { 13 + up: "operational", 14 + degraded: "degraded", 15 + down: "down", 16 + unknown: "unknown", 17 + }; 18 + 19 + // Verdana character width table at 11px (from shields.io) 20 + const WIDTHS: Record<string, number> = { 21 + " ": 3.3, "!": 4.2, '"': 5.2, "#": 7.8, $: 6.3, "%": 9.5, "&": 7.6, 22 + "'": 2.8, "(": 4.2, ")": 4.2, "*": 6.3, "+": 7.8, ",": 3.5, "-": 4.4, 23 + ".": 3.5, "/": 4.8, "0": 6.3, "1": 6.3, "2": 6.3, "3": 6.3, "4": 6.3, 24 + "5": 6.3, "6": 6.3, "7": 6.3, "8": 6.3, "9": 6.3, ":": 4.2, ";": 4.2, 25 + "<": 7.8, "=": 7.8, ">": 7.8, "?": 5.6, "@": 10.3, A: 7.3, B: 7.0, 26 + C: 6.7, D: 7.6, E: 6.2, F: 5.7, G: 7.6, H: 7.6, I: 4.2, J: 4.2, 27 + K: 7.0, L: 6.0, M: 8.9, N: 7.6, O: 7.6, P: 6.2, Q: 7.6, R: 7.0, 28 + S: 6.7, T: 6.2, U: 7.6, V: 7.0, W: 9.5, X: 6.5, Y: 6.2, Z: 6.7, 29 + a: 5.8, b: 6.5, c: 5.0, d: 6.5, e: 5.8, f: 3.9, g: 6.5, h: 6.5, 30 + i: 3.0, j: 3.6, k: 6.1, l: 3.0, m: 9.5, n: 6.5, o: 6.2, p: 6.5, 31 + q: 6.5, r: 4.6, s: 5.2, t: 4.2, u: 6.5, v: 5.8, w: 8.4, x: 5.6, 32 + y: 5.8, z: 5.0, "|": 4.2, 33 + }; 34 + 35 + function textWidth(s: string): number { 36 + let w = 0; 37 + for (const c of s) w += WIDTHS[c] ?? 6.5; 38 + return w; 39 + } 40 + 41 + function makeBadge(label: string, status: string, uptime: number): string { 42 + const color = COLORS[status] ?? COLORS.unknown; 43 + const statusLabel = STATUS_LABELS[status] ?? "unknown"; 44 + const value = `${statusLabel} ${uptime}%`; 45 + 46 + const pad = 20; 47 + const labelW = Math.round(textWidth(label) + pad); 48 + const valueW = Math.round(textWidth(value) + pad); 49 + const total = labelW + valueW; 50 + const labelX = labelW / 2; 51 + const valueX = labelW + valueW / 2; 52 + 53 + return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${total}" height="20" role="img"> 54 + <title>${label}: ${value}</title> 55 + <linearGradient id="s" x2="0" y2="100%"> 56 + <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> 57 + <stop offset="1" stop-opacity=".1"/> 58 + </linearGradient> 59 + <clipPath id="r"><rect width="${total}" height="20" rx="3" fill="#fff"/></clipPath> 60 + <g clip-path="url(#r)"> 61 + <rect width="${labelW}" height="20" fill="#555"/> 62 + <rect x="${labelW}" width="${valueW}" height="20" fill="${color}"/> 63 + <rect width="${total}" height="20" fill="url(#s)"/> 64 + </g> 65 + <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"> 66 + <text aria-hidden="true" x="${labelX}" y="15" fill="#010101" fill-opacity=".3">${esc(label)}</text> 67 + <text x="${labelX}" y="14">${esc(label)}</text> 68 + <text aria-hidden="true" x="${valueX}" y="15" fill="#010101" fill-opacity=".3">${esc(value)}</text> 69 + <text x="${valueX}" y="14">${esc(value)}</text> 70 + </g> 71 + </svg>`; 72 + } 73 + 74 + function esc(s: string): string { 75 + return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 76 + } 77 + 78 + const BADGE_HEADERS = { 79 + "Content-Type": "image/svg+xml", 80 + "Cache-Control": "no-cache, no-store, must-revalidate", 81 + "Access-Control-Allow-Origin": "*", 82 + }; 83 + 84 + export async function handleBadge( 85 + env: Env, 86 + serviceId: string, 87 + ): Promise<Response> { 88 + const ping = await getLatestPing(env.DB, serviceId); 89 + const uptime = await getUptime7d(env.DB, serviceId); 90 + const status = (ping?.status as string) ?? "unknown"; 91 + 92 + return new Response(makeBadge(serviceId, status, uptime), { 93 + headers: BADGE_HEADERS, 94 + }); 95 + } 96 + 97 + export async function handleOverallBadge(env: Env): Promise<Response> { 98 + const manifest = await getManifest(env); 99 + const monitored = manifest.filter((s) => s.health_url !== null); 100 + 101 + let worst: string = "up"; 102 + let totalUptime = 0; 103 + 104 + for (const svc of monitored) { 105 + const ping = await getLatestPing(env.DB, svc.name); 106 + const uptime = await getUptime7d(env.DB, svc.name); 107 + totalUptime += uptime; 108 + const s = (ping?.status as string) ?? "unknown"; 109 + if (s === "down") worst = "down"; 110 + else if (s === "degraded" && worst !== "down") worst = "degraded"; 111 + else if (s === "unknown" && worst === "up") worst = "unknown"; 112 + } 113 + 114 + const avgUptime = 115 + monitored.length > 0 116 + ? Math.round((totalUptime / monitored.length) * 100) / 100 117 + : 100; 118 + 119 + return new Response(makeBadge("infra", worst, avgUptime), { 120 + headers: BADGE_HEADERS, 121 + }); 122 + }
+94
src/routes/index.ts
··· 1 + import type { Env } from "../types"; 2 + import { getManifest } from "../manifest"; 3 + import { getLatestPing, getUptime7d } from "../db"; 4 + import { COMMIT_SHA } from "../version"; 5 + 6 + export async function handleIndex(env: Env): Promise<Response> { 7 + const manifest = await getManifest(env); 8 + 9 + const services = await Promise.all( 10 + manifest.map(async (svc) => { 11 + const ping = await getLatestPing(env.DB, svc.name); 12 + const uptime = await getUptime7d(env.DB, svc.name); 13 + return { 14 + name: svc.name, 15 + description: svc.description, 16 + url: `https://${svc.domain}`, 17 + status: ping?.status ?? "unknown", 18 + latency_ms: ping?.latency_ms ?? null, 19 + uptime_7d: uptime, 20 + has_health: svc.health_url !== null, 21 + }; 22 + }), 23 + ); 24 + 25 + const allUp = services.every( 26 + (s) => s.status === "up" || s.status === "unknown", 27 + ); 28 + 29 + const html = `<!DOCTYPE html> 30 + <html lang="en"> 31 + <head> 32 + <meta charset="utf-8"> 33 + <meta name="viewport" content="width=device-width, initial-scale=1"> 34 + <title>infra.dunkirk.sh</title> 35 + <style> 36 + * { margin: 0; padding: 0; box-sizing: border-box; } 37 + html { min-height: 100%; } 38 + body { font-family: -apple-system, system-ui, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 640px; margin: 0 auto; min-height: 100vh; display: flex; flex-direction: column; } 39 + h1 { font-size: 1.1rem; font-weight: 500; margin-bottom: 0.25rem; } 40 + .overall { font-size: 0.85rem; color: #8b949e; margin-bottom: 2rem; } 41 + .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; } 42 + .dot.up { background: #2ecc71; } 43 + .dot.degraded { background: #f39c12; } 44 + .dot.down { background: #e74c3c; } 45 + .dot.unknown { background: #8b949e; } 46 + .service { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #21262d; } 47 + .service:last-child { border-bottom: none; } 48 + .svc-left { display: flex; align-items: center; gap: 0.25rem; } 49 + .svc-name { font-size: 0.85rem; } 50 + .svc-name a { color: #c9d1d9; text-decoration: none; } 51 + .svc-name a:hover { text-decoration: underline; } 52 + .svc-right { font-size: 0.75rem; color: #8b949e; display: flex; gap: 0; flex-shrink: 0; } 53 + .uptime { width: 3.5rem; text-align: right; } 54 + .latency { width: 3rem; text-align: right; } 55 + footer { margin-top: auto; padding-top: 1rem; border-top: 1px solid #21262d; font-size: 0.7rem; color: #8b949e; display: flex; justify-content: space-between; } 56 + footer a { color: #8b949e; text-decoration: none; } 57 + footer a:hover { text-decoration: underline; } 58 + </style> 59 + </head> 60 + <body> 61 + <h1>infra.dunkirk.sh</h1> 62 + <p class="overall"><span class="dot ${allUp ? "up" : "degraded"}"></span>${allUp ? "All systems operational" : "Some systems degraded"}</p> 63 + ${services 64 + .map( 65 + (s) => `<div class="service"> 66 + <div class="svc-left"> 67 + <span class="dot ${s.status}"></span> 68 + <span class="svc-name"><a href="${esc(s.url)}">${esc(s.name)}</a></span> 69 + </div> 70 + <div class="svc-right"> 71 + ${s.has_health ? `<span class="uptime">${s.uptime_7d}%</span><span class="latency">${s.latency_ms !== null ? s.latency_ms + "ms" : "—"}</span>` : `<span class="latency">no health check</span>`} 72 + </div> 73 + </div>`, 74 + ) 75 + .join("\n")} 76 + <footer><span>checked every 5 min</span><a href="https://github.com/taciturnaxolotl/status/commit/${COMMIT_SHA}">${COMMIT_SHA}</a></footer> 77 + </body> 78 + </html>`; 79 + 80 + return new Response(html, { 81 + headers: { 82 + "Content-Type": "text/html; charset=utf-8", 83 + "Cache-Control": "public, max-age=30", 84 + }, 85 + }); 86 + } 87 + 88 + function esc(s: string): string { 89 + return s 90 + .replace(/&/g, "&amp;") 91 + .replace(/</g, "&lt;") 92 + .replace(/>/g, "&gt;") 93 + .replace(/"/g, "&quot;"); 94 + }
+31
src/routes/status.ts
··· 1 + import type { Env, StatusResponse } from "../types"; 2 + import { getManifest } from "../manifest"; 3 + import { getLatestPing, getUptime7d } from "../db"; 4 + 5 + export async function handleStatus(env: Env): Promise<Response> { 6 + const manifest = await getManifest(env); 7 + 8 + const services = await Promise.all( 9 + manifest.map(async (svc) => { 10 + const ping = await getLatestPing(env.DB, svc.name); 11 + const uptime = await getUptime7d(env.DB, svc.name); 12 + return { 13 + id: svc.name, 14 + name: svc.name, 15 + status: (ping?.status ?? "unknown") as 16 + | "up" 17 + | "degraded" 18 + | "down" 19 + | "unknown", 20 + latency_ms: ping?.latency_ms ?? null, 21 + uptime_7d: uptime, 22 + }; 23 + }), 24 + ); 25 + 26 + const ok = services.every((s) => s.status === "up" || s.status === "unknown"); 27 + 28 + return Response.json({ ok, services } satisfies StatusResponse, { 29 + headers: { "Access-Control-Allow-Origin": "*" }, 30 + }); 31 + }
+18
src/routes/uptime.ts
··· 1 + import type { Env, UptimeResponse } from "../types"; 2 + import { getUptimeBuckets } from "../db"; 3 + 4 + export async function handleUptime( 5 + env: Env, 6 + serviceId: string, 7 + url: URL, 8 + ): Promise<Response> { 9 + const windowParam = url.searchParams.get("window"); 10 + const window_hours = windowParam ? parseInt(windowParam, 10) * 24 : 90 * 24; 11 + 12 + const buckets = await getUptimeBuckets(env.DB, serviceId, window_hours); 13 + 14 + return Response.json( 15 + { service_id: serviceId, window_hours, buckets } satisfies UptimeResponse, 16 + { headers: { "Access-Control-Allow-Origin": "*" } }, 17 + ); 18 + }
+41
src/types.ts
··· 1 + export interface Service { 2 + name: string; 3 + description: string; 4 + domain: string; 5 + health_url: string | null; 6 + port: number; 7 + repository: string; 8 + runtime: string; 9 + data: { 10 + files: string[]; 11 + postgres: string | null; 12 + sqlite: string | null; 13 + }; 14 + } 15 + 16 + export type ServicesManifest = Service[]; 17 + 18 + export interface StatusResponse { 19 + ok: boolean; 20 + services: { 21 + id: string; 22 + name: string; 23 + status: "up" | "degraded" | "down" | "unknown"; 24 + latency_ms: number | null; 25 + uptime_7d: number; 26 + }[]; 27 + } 28 + 29 + export interface UptimeResponse { 30 + service_id: string; 31 + window_hours: number; 32 + buckets: { 33 + timestamp: number; 34 + status: "up" | "degraded" | "down"; 35 + }[]; 36 + } 37 + 38 + export interface Env { 39 + DB: D1Database; 40 + KV: KVNamespace; 41 + }
+12
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "strict": true, 7 + "noEmit": true, 8 + "lib": ["ES2022"], 9 + "types": ["@cloudflare/workers-types"] 10 + }, 11 + "include": ["src"] 12 + }
+16
wrangler.toml
··· 1 + name = "infra-status" 2 + main = "src/index.ts" 3 + compatibility_date = "2024-12-01" 4 + 5 + [triggers] 6 + crons = ["*/5 * * * *"] 7 + 8 + [[d1_databases]] 9 + binding = "DB" 10 + database_name = "status-db" 11 + database_id = "5987304c-9684-462e-a7dc-d6c67e7f6923" 12 + migrations_dir = "./migrations" 13 + 14 + [[kv_namespaces]] 15 + binding = "KV" 16 + id = "c4daf2e998ea45e1aad9efb6636b66df"