···11+{
22+ "lexicon": 1,
33+ "id": "pet.nkp.uptime.check",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "description": "a record representing a single uptime check for a monitored service",
88+ "key": "tid",
99+ "record": {
1010+ "type": "object",
1111+ "required": ["serviceName", "serviceUrl", "checkedAt", "status", "responseTime"],
1212+ "properties": {
1313+ "groupName": {
1414+ "type": "string",
1515+ "description": "optional group or project name that multiple services belong to (e.g., 'wisp.place')",
1616+ "maxLength": 100
1717+ },
1818+ "serviceName": {
1919+ "type": "string",
2020+ "description": "human-readable name of the service being monitored",
2121+ "maxLength": 100
2222+ },
2323+ "region": {
2424+ "type": "string",
2525+ "description": "optional region where the service is located (e.g., 'US East', 'Singapore')",
2626+ "maxLength": 50
2727+ },
2828+ "serviceUrl": {
2929+ "type": "string",
3030+ "format": "uri",
3131+ "description": "URL of the service being checked"
3232+ },
3333+ "checkedAt": {
3434+ "type": "string",
3535+ "format": "datetime",
3636+ "description": "timestamp when the check was performed"
3737+ },
3838+ "status": {
3939+ "type": "string",
4040+ "enum": ["up", "down"],
4141+ "description": "status of the service at check time"
4242+ },
4343+ "responseTime": {
4444+ "type": "integer",
4545+ "description": "response time in milliseconds, -1 if service is down",
4646+ "minimum": -1
4747+ },
4848+ "httpStatus": {
4949+ "type": "integer",
5050+ "description": "HTTP status code if applicable",
5151+ "minimum": 100,
5252+ "maximum": 599
5353+ },
5454+ "errorMessage": {
5555+ "type": "string",
5656+ "description": "error message if the check failed",
5757+ "maxLength": 500
5858+ }
5959+ }
6060+ }
6161+ }
6262+ }
6363+}
+15
package.json
···11+{
22+ "name": "cuteuptime",
33+ "version": "0.1.0",
44+ "private": true,
55+ "scripts": {
66+ "worker:dev": "cd worker && bun run dev",
77+ "worker:start": "cd worker && bun run start",
88+ "web:dev": "cd web && npm run dev",
99+ "web:build": "cd web && npm run build",
1010+ "web:preview": "cd web && npm run preview"
1111+ },
1212+ "dependencies": {
1313+ "@atcute/atproto": "^3.1.9"
1414+ }
1515+}
+148
readme.md
···11+# cuteuptime
22+33+Cute uptime monitoring using your PDS to store events.
44+55+## Project Structure
66+77+- **`worker/`** - Background Bun worker that monitors services and publishes uptime checks
88+- **`web/`** - Static web svelte dashboard that displays uptime statistics
99+- **`lexicon/`** - AT Protocol lexicon definitions for the uptime check record type
1010+1111+## Quick Start
1212+1313+### 1. Configure the Worker
1414+1515+```bash
1616+cd worker
1717+cp config.example.json config.json
1818+```
1919+2020+Edit `config.json`:
2121+2222+```json
2323+{
2424+ "pds": "https://bsky.social",
2525+ "identifier": "your.handle.bsky.social",
2626+ "password": "your-app-password",
2727+ "checkInterval": 300,
2828+ "services": [
2929+ {
3030+ "groupName": "Production",
3131+ "name": "API Server",
3232+ "url": "https://api.example.com/health",
3333+ "method": "GET",
3434+ "timeout": 10000,
3535+ "expectedStatus": 200
3636+ }
3737+ ]
3838+}
3939+```
4040+4141+**Important:** Use an app password, not your main account password. Generate one at: https://bsky.app/settings/app-passwords
4242+4343+### 2. Run the Worker
4444+4545+```bash
4646+cd worker
4747+bun install
4848+bun run dev
4949+```
5050+5151+The worker will:
5252+- Check each service at the configured interval
5353+- Publish results to your AT Protocol PDS
5454+- Continue running until you stop it
5555+5656+### 3. Configure the Web Dashboard
5757+5858+```bash
5959+cd web
6060+cp config.example.json config.json
6161+```
6262+6363+Edit `config.json`:
6464+6565+```json
6666+{
6767+ "pds": "https://bsky.social",
6868+ "did": "did:plc:your-did-here"
6969+}
7070+```
7171+7272+To find your DID, visit: https://bsky.app/profile/[your-handle] and look in the URL or use the AT Protocol explorer.
7373+7474+### 4. Build and Deploy the Web Dashboard
7575+7676+```bash
7777+cd web
7878+npm install
7979+npm run build
8080+```
8181+8282+The built static site will be in `web/dist/`. Deploy it to any static hosting:
8383+8484+- **Wisp Place**: Drag and drop the `dist` folder
8585+- **GitHub Pages**: Push to `gh-pages` branch
8686+- **Netlify**: Drag and drop the `dist` folder
8787+- **Vercel**: Connect your repo and set build directory to `web/dist`
8888+- **Cloudflare Pages**: Connect your repo
8989+9090+## Configuration
9191+9292+### Worker Configuration
9393+9494+| Field | Description |
9595+|-------|-------------|
9696+| `pds` | Your PDS URL (usually `https://bsky.social`) |
9797+| `identifier` | Your AT Protocol handle |
9898+| `password` | Your app password |
9999+| `checkInterval` | Seconds between checks (e.g., 300 = 5 minutes) |
100100+| `services` | Array of services to monitor |
101101+102102+### Service Configuration
103103+104104+| Field | Description |
105105+|-------|-------------|
106106+| `groupName` | Optional group name (e.g., "Production", "Staging") |
107107+| `name` | Service display name |
108108+| `url` | URL to check |
109109+| `method` | HTTP method (GET, POST, etc.) |
110110+| `timeout` | Request timeout in milliseconds |
111111+| `expectedStatus` | Expected HTTP status code (optional) |
112112+113113+### Web Configuration
114114+115115+| Field | Description |
116116+|-------|-------------|
117117+| `pds` | PDS URL to fetch records from |
118118+| `did` | DID of the account publishing uptime checks |
119119+120120+**Note:** The web config is injected at build time, so you need to rebuild after changing it.
121121+122122+## Features
123123+124124+- ✅ Looks cute
125125+- ✅ No database required just use your pds
126126+- ✅ Service grouping support
127127+- ✅ Response time tracking
128128+- ✅ Auto-refresh every x configurable minutes
129129+130130+## Development
131131+132132+### Worker Development
133133+134134+```bash
135135+cd worker
136136+bun run dev
137137+```
138138+139139+### Web Development
140140+141141+```bash
142142+cd web
143143+npm run dev
144144+```
145145+146146+Visit http://localhost:5173 to see the dashboard.
147147+148148+MIT
···11+import { Client, ok, simpleFetchHandler } from '@atcute/client';
22+import type {} from '@atcute/atproto';
33+import type { ActorIdentifier, Did, Handle } from '@atcute/lexicons';
44+import type { UptimeCheck, UptimeCheckRecord } from './types.ts';
55+66+/**
77+ * fetches uptime check records from a PDS for a given DID
88+ *
99+ * @param pds the PDS URL
1010+ * @param did the DID or handle to fetch records for
1111+ * @returns array of uptime check records
1212+ */
1313+export async function fetchUptimeChecks(
1414+ pds: string,
1515+ did: ActorIdentifier,
1616+): Promise<UptimeCheckRecord[]> {
1717+ const handler = simpleFetchHandler({ service: pds });
1818+ const rpc = new Client({ handler });
1919+2020+ // resolve handle to DID if needed
2121+ let resolvedDid: Did;
2222+ if (!did.startsWith('did:')) {
2323+ const handleData = await ok(
2424+ rpc.get('com.atproto.identity.resolveHandle', {
2525+ params: { handle: did as Handle },
2626+ }),
2727+ );
2828+ resolvedDid = handleData.did;
2929+ } else {
3030+ resolvedDid = did as Did;
3131+ }
3232+3333+ // fetch uptime check records
3434+ const response = await ok(
3535+ rpc.get('com.atproto.repo.listRecords', {
3636+ params: {
3737+ repo: resolvedDid,
3838+ collection: 'pet.nkp.uptime.check',
3939+ limit: 100,
4040+ },
4141+ }),
4242+ );
4343+4444+ // transform records into a more usable format
4545+ return response.records.map((record) => ({
4646+ uri: record.uri,
4747+ cid: record.cid,
4848+ value: record.value as unknown as UptimeCheck,
4949+ indexedAt: new Date((record.value as unknown as UptimeCheck).checkedAt),
5050+ }));
5151+}
···11+/**
22+ * uptime check record value
33+ */
44+export interface UptimeCheck {
55+ /** optional group or project name that multiple services belong to */
66+ groupName?: string;
77+ /** human-readable name of the service */
88+ serviceName: string;
99+ /** optional region where the service is located */
1010+ region?: string;
1111+ /** URL that was checked */
1212+ serviceUrl: string;
1313+ /** timestamp when the check was performed */
1414+ checkedAt: string;
1515+ /** status of the service */
1616+ status: 'up' | 'down';
1717+ /** response time in milliseconds, -1 if down */
1818+ responseTime: number;
1919+ /** HTTP status code if applicable */
2020+ httpStatus?: number;
2121+ /** error message if the check failed */
2222+ errorMessage?: string;
2323+}
2424+2525+/**
2626+ * uptime check record from PDS
2727+ */
2828+export interface UptimeCheckRecord {
2929+ uri: string;
3030+ cid: string;
3131+ value: UptimeCheck;
3232+ indexedAt: Date;
3333+}
···11+/**
22+ * configuration for a service to monitor
33+ */
44+export interface ServiceConfig {
55+ /** optional group or project name that multiple services belong to */
66+ group?: string;
77+ /** human-readable name of the service */
88+ name: string;
99+ /** optional region where the service is located */
1010+ region?: string;
1111+ /** URL to check */
1212+ url: string;
1313+ /** optional timeout in milliseconds (default: 5000) */
1414+ timeout?: number;
1515+ /** optional Host header to use (for direct IP checks) */
1616+ host?: string;
1717+}
1818+1919+/**
2020+ * result of an uptime check
2121+ */
2222+export interface UptimeCheck {
2323+ /** optional group or project name that multiple services belong to */
2424+ groupName?: string;
2525+ /** human-readable name of the service */
2626+ serviceName: string;
2727+ /** optional region where the service is located */
2828+ region?: string;
2929+ /** URL that was checked */
3030+ serviceUrl: string;
3131+ /** timestamp when the check was performed */
3232+ checkedAt: string;
3333+ /** status of the service */
3434+ status: 'up' | 'down';
3535+ /** response time in milliseconds, -1 if down */
3636+ responseTime: number;
3737+ /** HTTP status code if applicable */
3838+ httpStatus?: number;
3939+ /** error message if the check failed */
4040+ errorMessage?: string;
4141+}