feat: add cron and manual trigger

This commit is contained in:
2026-07-24 11:29:24 +05:30
parent 7207fcfab3
commit cc7ec85a5d
4 changed files with 169 additions and 128 deletions

View File

@@ -1,10 +1,13 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "daily-digest", "name": "daily-digest",
"dependencies": { "dependencies": {
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"eventsource": "^4.1.0",
"node-cron": "^4.6.0",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
@@ -23,6 +26,12 @@
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
"eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="],
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
"node-cron": ["node-cron@4.6.0", "", {}, "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],

124
fx.ts Normal file
View File

@@ -0,0 +1,124 @@
import { $ } from "bun"
const API =
"https://api.frankfurter.app/latest?from=USD&to=INR"
export async function getUSD() {
const res = await fetch(API)
if (!res.ok) {
throw new Error(`Exchange API failed: ${res.status}`)
}
const data = await res.json()
const rate = (data as { rates: { INR: number } }).rates.INR
if (typeof rate !== "number") {
throw new Error("Invalid exchange rate")
}
const helions = (rate / 100).toFixed(2)
return `**📈 Exchange Rate**
$1 = ₹${rate.toFixed(2)} = ⚙${helions}
`
}
export async function getUsage() {
//
// CPU LOAD
//
const loadavg = await $`cat /proc/loadavg`.text()
const [load1] = loadavg.trim().split(" ")
//
// MEMORY
//
const memRaw = await $`free -b`.text()
const memLine = memRaw
.split("\n")
.find((line) => line.startsWith("Mem:"))
if (!memLine) {
throw new Error("Failed to parse memory info")
}
const [
,
totalMem,
usedMem,
] = memLine.trim().split(/\s+/)
//
// DISK
//
const diskRaw = await $`df -B1 /`.text()
const diskLine = diskRaw
.split("\n")[1]
if (!diskLine) {
throw new Error("Failed to parse disk info")
}
const [
,
totalDisk,
usedDisk,
] = diskLine.trim().split(/\s+/)
const temp = await $`vcgencmd measure_temp`.text()
const tempData = temp.split("=")?.[1]?.replace("'", "°")
//
// HELPERS
//
function formatBytesIEC(bytes: number) {
const units = ["B", "KiB", "MiB", "GiB", "TiB"]
let i = 0
let value = bytes
while (value >= 1024 && i < units.length - 1) {
value /= 1024
i++
}
return `${value.toFixed(1)} ${units[i]}`
}
//
// OUTPUT
//
return `**💾 Usage**
• **CPU:** ${load1}
• **Memory:** ${formatBytesIEC(Number(usedMem))} / ${formatBytesIEC(Number(totalMem))}
• **Disk:** ${formatBytesIEC(Number(usedDisk))} / ${formatBytesIEC(Number(totalDisk))}
• **Temperature:** ${tempData}
`
}
export async function getWeather() {
const res = await fetch(`https://wttr.in/Bengaluru?format="%c%C| %f | %h | %m | %w | %p"`)
const data = await res.text()
const [weather, temp, humidity, moon, winds, preci] = data.replace(/\"/g, '').split("|")
return `**🌥️ Weather Report**
• **Weather:** ${weather}
• **Temperature:** ${temp}
• **Humidity:** ${humidity}
• **Moon Phase:** ${moon}
• **Winds:** ${winds}
• **Precipitation:** ${preci}
`
}

160
index.ts
View File

@@ -1,6 +1,9 @@
import { $ } from "bun" import { $ } from "bun"
import cron from "node-cron"
import dayjs from "dayjs" import dayjs from "dayjs"
import "dayjs/locale/es" import "dayjs/locale/es"
import { getUSD, getUsage, getWeather } from "./fx"
import { EventSource } from "eventsource"
const TOPIC = "daily-digest" const TOPIC = "daily-digest"
const TOKEN = process.env.NTFY_TOKEN const TOKEN = process.env.NTFY_TOKEN
@@ -10,130 +13,6 @@ if (!TOKEN) {
process.exit(1) process.exit(1)
} }
const API =
"https://api.frankfurter.app/latest?from=USD&to=INR"
async function getUSD() {
const res = await fetch(API)
if (!res.ok) {
throw new Error(`Exchange API failed: ${res.status}`)
}
const data = await res.json()
const rate = data.rates.INR
if (typeof rate !== "number") {
throw new Error("Invalid exchange rate")
}
const helions = (rate / 100).toFixed(2)
return `**📈 Exchange Rate**
$1 = ₹${rate.toFixed(2)} = ⚙${helions}
`
}
async function getUsage() {
//
// CPU LOAD
//
const loadavg = await $`cat /proc/loadavg`.text()
const [load1] = loadavg.trim().split(" ")
//
// MEMORY
//
const memRaw = await $`free -b`.text()
const memLine = memRaw
.split("\n")
.find((line) => line.startsWith("Mem:"))
if (!memLine) {
throw new Error("Failed to parse memory info")
}
const [
,
totalMem,
usedMem,
] = memLine.trim().split(/\s+/)
//
// DISK
//
const diskRaw = await $`df -B1 /`.text()
const diskLine = diskRaw
.split("\n")[1]
if (!diskLine) {
throw new Error("Failed to parse disk info")
}
const [
,
totalDisk,
usedDisk,
] = diskLine.trim().split(/\s+/)
const temp = await $`vcgencmd measure_temp`.text()
const tempData = temp.split("=")[1].replace("'", "°")
//
// HELPERS
//
function formatBytesIEC(bytes: number) {
const units = ["B", "KiB", "MiB", "GiB", "TiB"]
let i = 0
let value = bytes
while (value >= 1024 && i < units.length - 1) {
value /= 1024
i++
}
return `${value.toFixed(1)} ${units[i]}`
}
//
// OUTPUT
//
return `**💾 Usage**
• **CPU:** ${load1}
• **Memory:** ${formatBytesIEC(Number(usedMem))} / ${formatBytesIEC(Number(totalMem))}
• **Disk:** ${formatBytesIEC(Number(usedDisk))} / ${formatBytesIEC(Number(totalDisk))}
• **Temperature:** ${tempData}
`
}
async function getWeather() {
const res = await fetch(`https://wttr.in/Bengaluru?format="%c%C| %f | %h | %m | %w | %p"`)
const data = await res.text()
const [weather, temp, humidity, moon, winds, preci] = data.replace(/\"/g, '').split("|")
return `**🌥️ Weather Report**
• **Weather:** ${weather}
• **Temperature:** ${temp}
• **Humidity:** ${humidity}
• **Moon Phase:** ${moon}
• **Winds:** ${winds}
• **Precipitation:** ${preci}
`
console.log(data)
}
async function main() { async function main() {
@@ -175,7 +54,34 @@ ${usage}
console.log("Notification sent at " + dayjs().format("YYYY-MM-dd HH:mm:ss")) console.log("Notification sent at " + dayjs().format("YYYY-MM-dd HH:mm:ss"))
} }
main().catch((err) => { cron.schedule("0 7 * * *", () => {
console.error(err) main().catch((err) => {
process.exit(1) console.error(err)
})
}) })
const es = new EventSource("https://ntfy.hy-pr.link/daily-digest/sse", {
fetch: (input, init) =>
fetch(input, {
...init,
headers: {
...init?.headers,
Authorization: `Bearer ${TOKEN}`,
},
}),
});
es.onmessage = (e) => {
const msg = JSON.parse(e.data).message as string
if (msg === "/digest") {
main().catch((err) => {
console.error(err)
})
}
};
es.onerror = (e) => {
console.error(e);
};

View File

@@ -10,6 +10,8 @@
"typescript": "^5" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"dayjs": "^1.11.20" "dayjs": "^1.11.20",
"eventsource": "^4.1.0",
"node-cron": "^4.6.0"
} }
} }