feat: added stats
11f3e4cd
2 file(s) · +122 −3
| 1 | 1 | import { Hono } from "hono"; |
|
| 2 | + | import { getSystemStats, formatStats } from "./stats"; |
|
| 2 | 3 | ||
| 3 | 4 | const app = new Hono(); |
|
| 4 | 5 | ||
| 5 | 6 | app.get("/", async (c) => { |
|
| 6 | 7 | const txt = await Bun.file("./src/site.txt").text(); |
|
| 8 | + | const stats = await getSystemStats(); |
|
| 9 | + | const statsBox = formatStats(stats); |
|
| 10 | + | const fullContent = `${txt}\n\n${statsBox}\n`; |
|
| 7 | 11 | const userAgent = c.req.header("User-Agent") || ""; |
|
| 8 | 12 | console.log(userAgent); |
|
| 9 | 13 | const isCrawler = /bot/i.test(userAgent); |
|
| 28 | 32 | <meta name="twitter:image" content="/og.png"> |
|
| 29 | 33 | </head> |
|
| 30 | 34 | <body> |
|
| 31 | - | ${txt} |
|
| 35 | + | ${fullContent} |
|
| 32 | 36 | </body> |
|
| 33 | 37 | </html> |
|
| 34 | 38 | `); |
|
| 35 | 39 | } |
|
| 36 | 40 | ||
| 37 | - | return c.text(txt); |
|
| 41 | + | return c.text(fullContent); |
|
| 38 | 42 | }); |
|
| 39 | 43 | ||
| 40 | 44 | app.get("/og.png", async (c) => { |
|
| 46 | 50 | }); |
|
| 47 | 51 | }); |
|
| 48 | 52 | ||
| 49 | - | export default app; |
|
| 53 | + | export default { |
|
| 54 | + | port: 4321, |
|
| 55 | + | fetch: app.fetch, |
|
| 56 | + | }; |
|
| 1 | + | import { $ } from "bun"; |
|
| 2 | + | ||
| 3 | + | export async function getSystemStats() { |
|
| 4 | + | const stats: Record<string, string> = {}; |
|
| 5 | + | ||
| 6 | + | try { |
|
| 7 | + | // OS Name |
|
| 8 | + | const osName = await $`uname -s`.text(); |
|
| 9 | + | stats.os = osName.trim(); |
|
| 10 | + | ||
| 11 | + | // OS Version |
|
| 12 | + | const osVersion = await $`uname -r`.text(); |
|
| 13 | + | stats.version = osVersion.trim(); |
|
| 14 | + | ||
| 15 | + | // Architecture |
|
| 16 | + | const arch = await $`uname -m`.text(); |
|
| 17 | + | stats.arch = arch.trim(); |
|
| 18 | + | ||
| 19 | + | // Uptime |
|
| 20 | + | const uptimeSeconds = await $`sysctl -n kern.boottime`.text(); |
|
| 21 | + | const bootMatch = uptimeSeconds.match(/sec = (\d+)/); |
|
| 22 | + | if (bootMatch) { |
|
| 23 | + | const bootTime = Number.parseInt(bootMatch[1]); |
|
| 24 | + | const now = Math.floor(Date.now() / 1000); |
|
| 25 | + | const uptime = now - bootTime; |
|
| 26 | + | const days = Math.floor(uptime / 86400); |
|
| 27 | + | const hours = Math.floor((uptime % 86400) / 3600); |
|
| 28 | + | const minutes = Math.floor((uptime % 3600) / 60); |
|
| 29 | + | stats.uptime = `${days}d ${hours}h ${minutes}m`; |
|
| 30 | + | } |
|
| 31 | + | ||
| 32 | + | // CPU Info |
|
| 33 | + | const cpuBrand = await $`sysctl -n machdep.cpu.brand_string`.text(); |
|
| 34 | + | stats.cpu = cpuBrand.trim(); |
|
| 35 | + | ||
| 36 | + | // CPU Cores |
|
| 37 | + | const cpuCores = await $`sysctl -n hw.ncpu`.text(); |
|
| 38 | + | stats.cores = cpuCores.trim(); |
|
| 39 | + | ||
| 40 | + | // Memory |
|
| 41 | + | const memBytes = await $`sysctl -n hw.memsize`.text(); |
|
| 42 | + | const memGB = ( |
|
| 43 | + | Number.parseInt(memBytes.trim()) / |
|
| 44 | + | 1024 / |
|
| 45 | + | 1024 / |
|
| 46 | + | 1024 |
|
| 47 | + | ).toFixed(1); |
|
| 48 | + | stats.memory = `${memGB} GB`; |
|
| 49 | + | ||
| 50 | + | // Shell |
|
| 51 | + | const shell = process.env.SHELL || "unknown"; |
|
| 52 | + | stats.shell = shell.split("/").pop() || shell; |
|
| 53 | + | ||
| 54 | + | // Bun version |
|
| 55 | + | const bunVersion = Bun.version; |
|
| 56 | + | stats.bun = bunVersion; |
|
| 57 | + | } catch (error) { |
|
| 58 | + | console.error("Error fetching system stats:", error); |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | return stats; |
|
| 62 | + | } |
|
| 63 | + | ||
| 64 | + | export function formatStats(stats: Record<string, string>): string { |
|
| 65 | + | const labels: Record<string, string> = { |
|
| 66 | + | os: "OS", |
|
| 67 | + | version: "Kernel", |
|
| 68 | + | arch: "Arch", |
|
| 69 | + | uptime: "Uptime", |
|
| 70 | + | cpu: "CPU", |
|
| 71 | + | cores: "Cores", |
|
| 72 | + | memory: "Memory", |
|
| 73 | + | shell: "Shell", |
|
| 74 | + | bun: "Bun", |
|
| 75 | + | }; |
|
| 76 | + | ||
| 77 | + | // Calculate the actual content width |
|
| 78 | + | const maxLabelLength = Math.max( |
|
| 79 | + | ...Object.values(labels).map((l) => l.length), |
|
| 80 | + | ); |
|
| 81 | + | const maxValueLength = Math.max(...Object.values(stats).map((v) => v.length)); |
|
| 82 | + | ||
| 83 | + | const headerText = "message made possible thanks to"; |
|
| 84 | + | // Content width: " LABEL : VALUE " (2 spaces + label + space + colon + space + value + 2 spaces) |
|
| 85 | + | const contentWidth = 2 + maxLabelLength + 3 + maxValueLength + 2; |
|
| 86 | + | const boxWidth = Math.max(contentWidth, headerText.length + 4); |
|
| 87 | + | ||
| 88 | + | const lines: string[] = []; |
|
| 89 | + | lines.push(` ╭${"─".repeat(boxWidth)}╮`); |
|
| 90 | + | ||
| 91 | + | // Center the header |
|
| 92 | + | const headerPadding = Math.floor((boxWidth - headerText.length) / 2); |
|
| 93 | + | const headerRightPadding = boxWidth - headerPadding - headerText.length; |
|
| 94 | + | lines.push( |
|
| 95 | + | ` │${" ".repeat(headerPadding)}${headerText}${" ".repeat(headerRightPadding)}│`, |
|
| 96 | + | ); |
|
| 97 | + | lines.push(` ├${"─".repeat(boxWidth)}┤`); |
|
| 98 | + | ||
| 99 | + | // Add stats rows |
|
| 100 | + | for (const [key, value] of Object.entries(stats)) { |
|
| 101 | + | const label = labels[key] || key; |
|
| 102 | + | const paddedLabel = label.padEnd(maxLabelLength); |
|
| 103 | + | const paddedValue = value.padEnd(maxValueLength); |
|
| 104 | + | const content = ` ${paddedLabel} : ${paddedValue} `; |
|
| 105 | + | const rightPadding = boxWidth - content.length; |
|
| 106 | + | lines.push(` │${content}${" ".repeat(rightPadding)}│`); |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | lines.push(` ╰${"─".repeat(boxWidth)}╯`); |
|
| 110 | + | ||
| 111 | + | return lines.join("\n"); |
|
| 112 | + | } |