send notifs at launch

This commit is contained in:
Shinuwa 2026-09-07 14:32:06 +02:00
parent 9226295072
commit 4680b3ac44
3 changed files with 143 additions and 71 deletions

View file

@ -1,4 +1,5 @@
NTFY_URL=https://ntfy.sh NTFY_URL=https://ntfy.sh
NTFY_TOPIC= NTFY_TOPIC=
NTFY_TOKEN=
POLL_INTERVAL_SECONDS=60 POLL_INTERVAL_SECONDS=60

View file

@ -1,6 +1,6 @@
# Codex Reset Alerts 🚀 # Codex Reset Alerts 🚀
Petit watcher Node.js qui surveille ton quota **OpenAI Codex** et envoie une notification **ntfy** dès que ton quota est de nouveau disponible. Petit watcher Node.js qui surveille ton quota **OpenAI Codex** et envoie des notifications **ntfy** au lancement, puis dès que ton quota est de nouveau disponible.
L'idée est simple : lorsque Codex indique que ton quota est épuisé, tu lances le watcher et tu peux passer à autre chose. Il surveille le quota en arrière-plan, t'envoie une notification lors du reset réel, puis s'arrête automatiquement. L'idée est simple : lorsque Codex indique que ton quota est épuisé, tu lances le watcher et tu peux passer à autre chose. Il surveille le quota en arrière-plan, t'envoie une notification lors du reset réel, puis s'arrête automatiquement.
@ -42,7 +42,7 @@ Le watcher utilise deux mécanismes :
Le timestamp `resetsAt` fourni par Codex est également surveillé, mais il n'est **pas considéré comme une garantie** : le quota peut être réinitialisé avant la date annoncée. Le timestamp `resetsAt` fourni par Codex est également surveillé, mais il n'est **pas considéré comme une garantie** : le quota peut être réinitialisé avant la date annoncée.
La notification n'est envoyée que lorsqu'un passage réel de **quota épuisé → quota disponible** est détecté. Une notification de démarrage est envoyée dès le lancement. La notification de reset n'est envoyée que lorsqu'un passage réel de **quota épuisé → quota disponible** est détecté.
## Prérequis ## Prérequis
@ -86,6 +86,7 @@ Crée un fichier `.env` à la racine :
```env ```env
NTFY_URL=https://ntfy.sh NTFY_URL=https://ntfy.sh
NTFY_TOPIC=mon-topic-secret NTFY_TOPIC=mon-topic-secret
NTFY_TOKEN=
POLL_INTERVAL_SECONDS=60 POLL_INTERVAL_SECONDS=60
``` ```
@ -102,6 +103,12 @@ https://ntfy.sh
Tu peux également utiliser une instance ntfy auto-hébergée. Tu peux également utiliser une instance ntfy auto-hébergée.
### `NTFY_TOKEN`
Token d'accès pour un serveur ntfy privé.
Laisse vide si ton topic ne demande pas d'authentification.
### `NTFY_TOPIC` ### `NTFY_TOPIC`
Topic sur lequel la notification sera envoyée. Topic sur lequel la notification sera envoyée.
@ -133,6 +140,7 @@ npm start
Si le quota est effectivement épuisé : Si le quota est effectivement épuisé :
```text ```text
📱 Notification ntfy de démarrage envoyée.
🔌 Connexion à Codex App Server... 🔌 Connexion à Codex App Server...
✅ Connecté. ✅ Connecté.
@ -198,8 +206,9 @@ Tu peux vérifier manuellement que ton téléphone reçoit bien les notification
curl \ curl \
-H "Title: Test Codex" \ -H "Title: Test Codex" \
-H "Tags: robot" \ -H "Tags: robot" \
-H "Authorization: Bearer TON_TOKEN" \
-d "Hello depuis Codex Reset Alerts 🚀" \ -d "Hello depuis Codex Reset Alerts 🚀" \
https://ntfy.sh/TON_TOPIC https://ntfy.shinuwa.fr/TON_TOPIC
``` ```
## Lancer le watcher en arrière-plan ## Lancer le watcher en arrière-plan

114
index.js
View file

@ -26,6 +26,8 @@ loadEnv();
const NTFY_URL = process.env.NTFY_URL ?? "https://ntfy.sh"; const NTFY_URL = process.env.NTFY_URL ?? "https://ntfy.sh";
const NTFY_TOPIC = process.env.NTFY_TOPIC; const NTFY_TOPIC = process.env.NTFY_TOPIC;
const NTFY_TOKEN = process.env.NTFY_TOKEN;
const TEST_NTFY = process.argv.includes("--test-ntfy");
// Toutes les 60 secondes par défaut. // Toutes les 60 secondes par défaut.
const POLL_INTERVAL_MS = const POLL_INTERVAL_MS =
@ -36,17 +38,10 @@ if (!NTFY_TOPIC) {
process.exit(1); process.exit(1);
} }
const codex = spawn("codex", ["app-server", "--stdio"], {
stdio: ["pipe", "pipe", "inherit"],
});
const lines = readline.createInterface({
input: codex.stdout,
crlfDelay: Infinity,
});
let nextId = 1; let nextId = 1;
const pending = new Map(); const pending = new Map();
let codex = null;
let lines = null;
let previousState = null; let previousState = null;
let refreshInProgress = false; let refreshInProgress = false;
@ -120,22 +115,26 @@ function formatDuration(ms) {
.join(" "); .join(" ");
} }
async function sendResetNotification(oldState, newState) { async function sendNtfyNotification({
title,
tags,
priority = "default",
body,
}) {
const url = const url =
`${NTFY_URL.replace(/\/$/, "")}/${encodeURIComponent(NTFY_TOPIC)}`; `${NTFY_URL.replace(/\/$/, "")}/${encodeURIComponent(NTFY_TOPIC)}`;
const headers = {
Title: title,
Tags: tags,
Priority: priority,
"Content-Type": "text/plain; charset=utf-8",
...(NTFY_TOKEN ? { Authorization: `Bearer ${NTFY_TOKEN}` } : {}),
};
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
headers: { headers,
Title: "Codex disponible", body,
Tags: "robot,white_check_mark",
Priority: "high",
"Content-Type": "text/plain; charset=utf-8",
},
body:
`🚀 Ton quota Codex est de nouveau disponible !\n\n` +
`Avant : ${oldState.usedPercent}% utilisé\n` +
`Maintenant : ${newState.usedPercent}% utilisé`,
}); });
if (!response.ok) { if (!response.ok) {
@ -143,6 +142,29 @@ async function sendResetNotification(oldState, newState) {
} }
} }
async function sendStartupNotification() {
await sendNtfyNotification({
title: "Codex Reset Alerts lancé",
tags: "robot,eyes",
priority: "default",
body:
`👀 Codex Reset Alerts vient de démarrer.\n\n` +
`Surveillance du quota Codex en cours.`,
});
}
async function sendResetNotification(oldState, newState) {
await sendNtfyNotification({
title: "Codex disponible",
tags: "robot,white_check_mark",
priority: "high",
body:
`🚀 Ton quota Codex est de nouveau disponible !\n\n` +
`Avant : ${oldState.usedPercent}% utilisé\n` +
`Maintenant : ${newState.usedPercent}% utilisé`,
});
}
function stop(exitCode = 0) { function stop(exitCode = 0) {
if (shuttingDown) return; if (shuttingDown) return;
@ -157,7 +179,7 @@ function stop(exitCode = 0) {
} }
try { try {
codex.kill(); codex?.kill();
} catch { } catch {
// Rien. // Rien.
} }
@ -337,6 +359,16 @@ async function refreshQuota(source = "polling") {
} }
} }
function startCodexServer() {
codex = spawn("codex", ["app-server", "--stdio"], {
stdio: ["pipe", "pipe", "inherit"],
});
lines = readline.createInterface({
input: codex.stdout,
crlfDelay: Infinity,
});
lines.on("line", async (line) => { lines.on("line", async (line) => {
let message; let message;
@ -379,11 +411,32 @@ lines.on("line", async (line) => {
} }
}); });
codex.on("exit", (code) => {
if (!shuttingDown && code !== 0) {
console.error(
`❌ Codex App Server s'est arrêté avec le code ${code}`
);
}
});
}
async function main() { async function main() {
try {
await sendStartupNotification();
console.log("📱 Notification ntfy de démarrage envoyée.");
} catch (error) {
console.error(
"❌ Échec de la notification ntfy de démarrage :",
error.message
);
}
console.log( console.log(
"🔌 Connexion à Codex App Server..." "🔌 Connexion à Codex App Server..."
); );
startCodexServer();
await request("initialize", { await request("initialize", {
clientInfo: { clientInfo: {
name: "codex-reset-alerts", name: "codex-reset-alerts",
@ -428,15 +481,24 @@ process.on("SIGTERM", () => {
stop(0); stop(0);
}); });
codex.on("exit", (code) => { if (TEST_NTFY) {
if (!shuttingDown && code !== 0) { sendResetNotification(
{ usedPercent: 100 },
{ usedPercent: 0 }
)
.then(() => {
console.log("📱 Notification ntfy de test envoyée.");
})
.catch((error) => {
console.error( console.error(
`❌ Codex App Server s'est arrêté avec le code ${code}` "❌ Échec de la notification ntfy de test :",
error.message
); );
} process.exitCode = 1;
}); });
} else {
main().catch((error) => { main().catch((error) => {
console.error("❌", error); console.error("❌", error);
stop(1); stop(1);
}); });
}