77th EV Calendar Notify Service
A TypeScript + Node.js service that polls the 77th JSOC events API, exposes its events through a REST API, stores them in SQLite, publishes an ICS feed, and sends change notifications through an Apprise server.
Requirements
- Node.js 22.9 or newer
- npm
Run
npm install
Copy-Item .env.example .env
npm run dev
The service requests https://77th-jsoc.com/service.php?action=get_events for each calendar month touched by today through the next 60 days. It sets year, month, and timezone=-120 on every request and polls every 15 minutes. The API's uid is the event identity in SQLite, the REST API, and ICS UID; the event title remains the display summary.
Each successful poll stores source events in data/events.sqlite (or the path configured by DB_PATH). When opening the older title-keyed database schema, the service preserves its old table under an events_legacy_title_* name and starts a silent UID-based baseline. If the events table is empty, the poll is treated as an initial snapshot and sends no notifications. Later additions, changes, and removals are queued durably in SQLite and sent through Apprise. Failed sends remain queued for retry. Events removed from the source remain in SQLite as cancelled ICS entries.
Apprise and notification configuration
Copy .env.example to .env and set the Apprise host, ntfy host/topic, and Discord webhook. dc_webhook accepts WEBHOOK_ID/WEBHOOK_TOKEN or the full Discord webhook URL. ntfy credentials are optional, but set both username and password if authentication is required. Leave notification_mock=false to send; set it to true to log and consume notifications without sending.
The service posts a JSON payload to Apprise /notify with both the ntfy and Discord URLs. Apprise supports multiple destination URLs in one request. Apprise API
Set ADMIN_TOKEN to a long random secret to enable the protected admin endpoints. Authenticate with an Authorization: Bearer <token> header. If the header is absent, ?token=<token> is also accepted. A present but invalid header is rejected without falling back to the query parameter. Query parameters may be recorded in browser history and server/proxy logs, so the header is preferred. If ADMIN_TOKEN is unset, admin endpoints return 503.
A .env file is optional at startup; without one, the server still runs, but notification sends will remain queued until the notification settings are configured. PORT defaults to 3000; DB_PATH defaults to ./data/events.sqlite.
For production, run npm run build followed by npm start, and keep the process running with your preferred process manager or service host.
Docker
Build and start the calendar service and Apprise with Docker Compose:
Copy-Item .env.example .env
# Edit .env and configure Apprise, ntfy, and Discord settings.
docker compose up -d --build
To attach the calendar app to an existing reverse-proxy Docker network, copy compose.override.yaml.example to compose.override.yaml on the server and set PROXY_NETWORK in .env to that network’s name. Compose automatically merges the local override file. The network must already exist; Apprise stays on the private Compose network while the calendar app joins both networks.
Copy-Item compose.override.yaml.example compose.override.yaml
# Set PROXY_NETWORK in .env to your existing network's name.
docker compose up -d --build
The calendar service listens on port 3000; set HOST_PORT in the shell or Compose .env file to change the host port. Apprise listens on the private Compose network at apprise:8000 and is not published directly to the internet. SQLite data is stored in the persistent event-data Docker volume. Follow both services with docker compose logs -f, stop them with docker compose down, and remove the database volume only when you intend to delete the stored events and notification history (docker compose down -v).
To build and run without Compose:
docker build -t 77th-ev-cal-ntfy-srv .
docker run -d --name 77th-ev-cal-ntfy-srv --restart unless-stopped \
--env-file .env -p 3000:3000 -v event-data:/app/data \
77th-ev-cal-ntfy-srv
Endpoints
GET /health— service and polling statusGET /api/events— latest active events and polling statusPOST /api/admin/refresh— trigger an immediate poll (Bearer token required)POST /api/admin/notifications/test— send a test notification to ntfy and Discord (Bearer token required)GET /calendar.ics— active events and cancellation entries for calendar subscription
On startup, the service sends a notification to both ntfy and Discord. The test endpoint also sends to both configured destinations. If notification_mock=true, notifications are only logged and the test endpoint reports mocked.
Example admin request:
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" https://YOUR-HOST/api/admin/notifications/test