Initial commit for TokenWatch
This commit is contained in:
commit
ed5cc23a35
15 files changed
+2903
No files matched your search
+29
@@ -0,0 +1,29 @@
|
||||
import cron from 'node-cron';
|
||||
import { fetchAllQuotas } from './services/providers';
|
||||
import { insertQuota } from './services/db';
|
||||
import { sendNtfyAlert } from './services/ntfy';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const ALERT_THRESHOLD = parseInt(process.env.ALERT_THRESHOLD_PERCENT || '80');
|
||||
|
||||
cron.schedule('*/30 * * * *', async () => {
|
||||
console.log('Running quota check cron job...');
|
||||
try {
|
||||
const quotas = await fetchAllQuotas();
|
||||
for (const quota of quotas) {
|
||||
// Save to DB
|
||||
await insertQuota(quota);
|
||||
|
||||
// Send alert if threshold exceeded
|
||||
if (quota.percentage >= ALERT_THRESHOLD) {
|
||||
await sendNtfyAlert(quota.provider, quota.percentage);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in cron job:', error);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Cron jobs scheduled');
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { getLatestQuotas, getQuotaHistory } from './services/db';
|
||||
import './cron'; // Start the cron jobs
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/api/quotas', async (req, res) => {
|
||||
try {
|
||||
const quotas = await getLatestQuotas();
|
||||
res.json(quotas);
|
||||
} catch (error) {
|
||||
console.error('Error fetching quotas:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/history', async (req, res) => {
|
||||
try {
|
||||
const history = await getQuotaHistory();
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
console.error('Error fetching history:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`TokenWatch server running on port ${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Pool } from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
});
|
||||
|
||||
export interface QuotaEntry {
|
||||
id?: number;
|
||||
provider: string;
|
||||
used: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
timestamp?: Date;
|
||||
}
|
||||
|
||||
export async function initDb() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS quotas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
used NUMERIC NOT NULL,
|
||||
total NUMERIC NOT NULL,
|
||||
percentage NUMERIC NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
console.log('Database initialized');
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertQuota(entry: QuotaEntry) {
|
||||
const query = `
|
||||
INSERT INTO quotas (provider, used, total, percentage)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *;
|
||||
`;
|
||||
const values = [entry.provider, entry.used, entry.total, entry.percentage];
|
||||
const res = await pool.query(query, values);
|
||||
return res.rows[0];
|
||||
}
|
||||
|
||||
export async function getLatestQuotas() {
|
||||
const query = `
|
||||
SELECT DISTINCT ON (provider) *
|
||||
FROM quotas
|
||||
ORDER BY provider, timestamp DESC;
|
||||
`;
|
||||
const res = await pool.query(query);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
export async function getQuotaHistory(days: number = 7) {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM quotas
|
||||
WHERE timestamp > CURRENT_DATE - $1::integer
|
||||
ORDER BY timestamp ASC;
|
||||
`;
|
||||
const res = await pool.query(query, [days]);
|
||||
return res.rows;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export async function sendNtfyAlert(provider: string, percentage: number) {
|
||||
const url = process.env.NTFY_URL || 'https://ntfy.orfel.de/tokenwatch_dummy';
|
||||
const message = `🚨 ${provider} quota is running low! (${percentage.toFixed(1)}% used)`;
|
||||
|
||||
try {
|
||||
await axios.post(url, message, {
|
||||
headers: {
|
||||
'Title': 'TokenWatch Alert',
|
||||
'Tags': 'warning,robot'
|
||||
}
|
||||
});
|
||||
console.log(`ntfy alert sent for ${provider}`);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to send ntfy alert:', error.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import axios from 'axios';
|
||||
import dotenv from 'dotenv';
|
||||
import { QuotaEntry } from './db';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export async function fetchAntigravityQuota(): Promise<QuotaEntry> {
|
||||
try {
|
||||
const url = process.env.ANTIGRAVITY_ENDPOINT || 'https://api.antigravity.google/v1/quota';
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.ANTIGRAVITY_API_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
// Assuming response structure: { used: number, total: number }
|
||||
const used = res.data.used || 0;
|
||||
const total = res.data.total || 1000;
|
||||
const percentage = (used / total) * 100;
|
||||
|
||||
return { provider: 'Antigravity', used, total, percentage };
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching Antigravity quota:', error.message);
|
||||
// Fallback for demonstration / local testing if endpoint is mocked
|
||||
return { provider: 'Antigravity', used: 0, total: 100, percentage: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPoeQuota(): Promise<QuotaEntry> {
|
||||
try {
|
||||
// According to docs, returns available point balance
|
||||
const res = await axios.get('https://api.poe.com/usage/current_balance', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.POE_API_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
// POE logic: 75000 daily points is standard. We might need to adjust based on actual API payload.
|
||||
// Assuming response has: { available: number }
|
||||
const available = res.data.available || 0;
|
||||
const total = 75000;
|
||||
const used = total - available > 0 ? total - available : 0;
|
||||
const percentage = (used / total) * 100;
|
||||
|
||||
return { provider: 'POE', used, total, percentage };
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching POE quota:', error.message);
|
||||
return { provider: 'POE', used: 0, total: 75000, percentage: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMinimaxQuota(): Promise<QuotaEntry> {
|
||||
try {
|
||||
const res = await axios.get('https://api.minimax.io/v1/api/openplatform/coding_plan/remains', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
// Assuming response has { used_quota: number, total_quota: number }
|
||||
const used = res.data.used_quota || 0;
|
||||
const total = res.data.total_quota || 100;
|
||||
const percentage = (used / total) * 100;
|
||||
|
||||
return { provider: 'MiniMax', used, total, percentage };
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching MiniMax quota:', error.message);
|
||||
return { provider: 'MiniMax', used: 0, total: 100, percentage: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllQuotas(): Promise<QuotaEntry[]> {
|
||||
const results = await Promise.all([
|
||||
fetchAntigravityQuota(),
|
||||
fetchPoeQuota(),
|
||||
fetchMinimaxQuota()
|
||||
]);
|
||||
return results;
|
||||
}
|
||||
Reference in new issue
Block a user