Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions scripts/coretime-alert/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.state.json
*.log
README.md
lark-stack.yml
.gitignore
Dockerfile
.dockerignore
package-lock.json
3 changes: 3 additions & 0 deletions scripts/coretime-alert/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.state.json
*.log
19 changes: 19 additions & 0 deletions scripts/coretime-alert/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM node:22-alpine

WORKDIR /app

COPY package.json ./
RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force

COPY check.mjs ./

ENV STATE_FILE=/app/state/.state.json \
ALERT_COOLDOWN_HOURS=12 \
LEADIN_ALERT_DAYS=3 \
TOTAL_ALERT_DAYS=7 \
CHECK_INTERVAL_SECONDS=3600

VOLUME ["/app/state"]

# Run the check, then sleep for the configured interval, forever.
CMD ["sh", "-c", "while true; do node check.mjs; sleep ${CHECK_INTERVAL_SECONDS:-3600}; done"]
147 changes: 147 additions & 0 deletions scripts/coretime-alert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# coretime renewal watchdog

Alerts a Discord webhook when Hydration's (Polkadot, task `2034`) or Basilisk's
(Kusama, task `2090`) bulk coretime cores are at risk of lapsing in the current
sale.

Bulk cores are **not leases** — they must be renewed every ~28-day sale cycle or
they fall back to the open market. This watchdog watches the `broker` pallet on
each coretime chain and pings before the renewal window closes.

## what triggers an alert

An alert fires for a chain only when it is **short of its target core count for
the upcoming region** (i.e. some cores are still un-renewed) **and** the deadline
is near:

| condition | default | severity |
|-----------|---------|----------|
| `< TOTAL_ALERT_DAYS` until the region begins (hard deadline — renewal right is lost after) | 7 days | `URGENT` |
| inside the lead-in period with `< LEADIN_ALERT_DAYS` left in it | 3 days | `WARNING` |

"Short" = `secured cores for next region (workplan) < desired`. The alert lists the
pending `broker.renew(core)` calls (with encoded hex) needed to close the gap.

> Note: the cheapest, priority window to renew is actually the **interlude**
> (before lead-in). These thresholds, as requested, only warn once the lead-in is
> closing / the hard deadline is near — so treat an alert as "renew now, you're
> past the ideal window". Lower the thresholds if you want earlier warnings.

## setup

```sh
cd scripts/coretime-alert
npm install
export DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/XXX/YYY'
node check.mjs # one-shot check; alerts if within thresholds
```

### useful invocations

```sh
node check.mjs --dry-run # print the payload instead of POSTing (no webhook needed)
node check.mjs --test # POST a "webhook reachable" message and exit
node check.mjs --force # ignore the cooldown and (re)send any active alert
```

## configuration (env vars)

| var | default | meaning |
|-----|---------|---------|
| `DISCORD_WEBHOOK_URL` | — | **required** (unless `--dry-run`) |
| `DISCORD_WEBHOOK_URL_FILE` | — | alternative source: read the webhook from a file (e.g. a mounted secret) |
| `CHECK_INTERVAL_SECONDS` | `3600` | (docker image only) seconds between checks in the built-in loop |
| `LEADIN_ALERT_DAYS` | `3` | warn when this many days are left in the lead-in period |
| `TOTAL_ALERT_DAYS` | `7` | warn when this many days are left until the region begins |
| `ALERT_COOLDOWN_HOURS` | `12` | minimum gap between repeat alerts for the same condition |
| `HYDRATION_DESIRED_CORES` | `3` | target core count for task 2034 |
| `BASILISK_DESIRED_CORES` | `3` | target core count for task 2090 |
| `STATE_FILE` | `./.state.json` | where the throttle state is persisted |

State persists across runs so a standing condition only re-pings every
`ALERT_COOLDOWN_HOURS`; it re-alerts immediately if severity or shortfall changes,
and clears itself once the cores are renewed.

## running it as a service

It's a one-shot check — schedule it. Hourly is plenty (the relevant deadlines move
in days).

### cron

```cron
0 * * * * cd /path/to/hydration-node/scripts/coretime-alert && DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/XXX/YYY' /usr/bin/node check.mjs >> /var/log/coretime-alert.log 2>&1
```

### systemd timer

`/etc/systemd/system/coretime-alert.service`

```ini
[Unit]
Description=Coretime renewal watchdog
After=network-online.target

[Service]
Type=oneshot
WorkingDirectory=/path/to/hydration-node/scripts/coretime-alert
Environment=DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/XXX/YYY
ExecStart=/usr/bin/node check.mjs
```

`/etc/systemd/system/coretime-alert.timer`

```ini
[Unit]
Description=Run coretime renewal watchdog hourly

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target
```

```sh
systemctl enable --now coretime-alert.timer
```

## docker image / lark deployment

A `Dockerfile` builds `galacticcouncil/coretime-alert`; the image runs the check
on an hourly loop (`CHECK_INTERVAL_SECONDS`).

```sh
docker build -t galacticcouncil/coretime-alert:latest .
docker push galacticcouncil/coretime-alert:latest
# smoke test: one-shot dry-run inside the image
docker run --rm galacticcouncil/coretime-alert:latest node check.mjs --dry-run
```

On the **lark** Swarm it's deployed as the `coretime-alert` stack from
`lark-stack.yml` (single replica, `state` volume, `autoredeploy` on). The webhook
is injected as `DISCORD_WEBHOOK_URL` via `$env` at deploy time and is never stored
in the repo — see the safe-forwarding options below.

### forwarding the webhook safely

`$env:DISCORD_WEBHOOK_URL` resolves against the **swarmpit MCP server's**
environment (defined in `~/.claude.json`), not your interactive shell. Pick one:

- **local deploy file** — copy `lark-stack.yml` outside the repo with the real URL
inlined (`chmod 600`), and deploy that file. Value never enters the repo or chat.
- **MCP server env** — add `DISCORD_WEBHOOK_URL` to the `swarmpit-lark` `env` block
in `~/.claude.json`, then restart so the server inherits it.

Do **not** paste the URL into a chat/agent prompt — it ends up in transcripts.
Rotate the webhook in Discord if it ever leaks.

## notes

- Endpoints have built-in fallbacks per chain; a failed assessment posts a (throttled)
`🔌 check failed` notice so the watchdog never dies silently.
- Renewed cores get **new core indices** each cycle (the assignment to the task is
preserved), so the listed core numbers change after every renewal — expected.
- Adding another para: append an entry to `CHAINS` in `check.mjs` with its task id,
relay, and coretime endpoints.
Loading
Loading