Initial commit

This commit is contained in:
mstfyldz
2026-05-27 16:47:37 +03:00
commit 3ee41864f4
40 changed files with 9041 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
// Initialize tables
pool.query(`
CREATE TABLE IF NOT EXISTS ping_logs (
id SERIAL PRIMARY KEY,
site_id TEXT NOT NULL,
status TEXT NOT NULL,
ms INTEGER,
code INTEGER,
error TEXT,
ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT)
);
CREATE INDEX IF NOT EXISTS idx_ping_site ON ping_logs(site_id, ts DESC);
CREATE TABLE IF NOT EXISTS pageviews (
id SERIAL PRIMARY KEY,
domain TEXT NOT NULL,
path TEXT NOT NULL,
referrer TEXT,
country TEXT,
city TEXT,
ua TEXT,
device TEXT,
browser TEXT,
os TEXT,
visitor_id TEXT NOT NULL,
ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT)
);
CREATE INDEX IF NOT EXISTS idx_pv_domain ON pageviews(domain, ts DESC);
CREATE INDEX IF NOT EXISTS idx_pv_ts ON pageviews(ts DESC);
`).catch(console.error)
export type PingLog = {
id: number
site_id: string
status: 'up' | 'down' | 'degraded'
ms: number | null
code: number | null
error: string | null
ts: number
}
export async function insertPingLog(log: Omit<PingLog, 'id' | 'ts'>) {
await pool.query(
`INSERT INTO ping_logs (site_id, status, ms, code, error) VALUES ($1, $2, $3, $4, $5)`,
[log.site_id, log.status, log.ms, log.code, log.error]
)
}
export async function getRecentLogs(siteId: string, limit = 100): Promise<PingLog[]> {
const res = await pool.query(
`SELECT * FROM ping_logs WHERE site_id = $1 ORDER BY ts DESC LIMIT $2`,
[siteId, limit]
)
return res.rows.map(r => ({ ...r, ms: Number(r.ms), code: Number(r.code), ts: Number(r.ts) }))
}
export async function getUptimePercent(siteId: string, hours = 24): Promise<number> {
const since = Math.floor(Date.now() / 1000) - hours * 3600
const res = await pool.query(
`SELECT status FROM ping_logs WHERE site_id = $1 AND ts > $2`,
[siteId, since]
)
const rows = res.rows
if (rows.length === 0) return 100
const up = rows.filter(r => r.status === 'up').length
return Math.round((up / rows.length) * 1000) / 10
}
export async function getLastLog(siteId: string): Promise<PingLog | null> {
const res = await pool.query(
`SELECT * FROM ping_logs WHERE site_id = $1 ORDER BY ts DESC LIMIT 1`,
[siteId]
)
if (res.rows.length === 0) return null
const r = res.rows[0]
return { ...r, ms: Number(r.ms), code: Number(r.code), ts: Number(r.ts) }
}
export type Pageview = {
id: number
domain: string
path: string
referrer: string | null
country: string | null
city: string | null
ua: string | null
device: string | null
browser: string | null
os: string | null
visitor_id: string
ts: number
}
export async function insertPageview(pv: Omit<Pageview, 'id' | 'ts'>) {
await pool.query(
`INSERT INTO pageviews (domain, path, referrer, country, city, ua, device, browser, os, visitor_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[pv.domain, pv.path, pv.referrer, pv.country, pv.city, pv.ua, pv.device, pv.browser, pv.os, pv.visitor_id]
)
}
export async function getAnalyticsStats(domain: string, days = 30) {
const since = Math.floor(Date.now() / 1000) - days * 86400
const totalRes = await pool.query(`SELECT COUNT(*) as n FROM pageviews WHERE domain = $1 AND ts > $2`, [domain, since])
const total = Number(totalRes.rows[0].n)
const uniqueRes = await pool.query(`SELECT COUNT(DISTINCT visitor_id) as n FROM pageviews WHERE domain = $1 AND ts > $2`, [domain, since])
const unique = Number(uniqueRes.rows[0].n)
const topPagesRes = await pool.query(`
SELECT path, COUNT(*) as views
FROM pageviews WHERE domain = $1 AND ts > $2
GROUP BY path ORDER BY views DESC LIMIT 10
`, [domain, since])
const topPages = topPagesRes.rows.map(r => ({ path: r.path, views: Number(r.views) }))
const topReferrersRes = await pool.query(`
SELECT referrer, COUNT(*) as visits
FROM pageviews WHERE domain = $1 AND ts > $2 AND referrer IS NOT NULL AND referrer != ''
GROUP BY referrer ORDER BY visits DESC LIMIT 10
`, [domain, since])
const topReferrers = topReferrersRes.rows.map(r => ({ referrer: r.referrer, visits: Number(r.visits) }))
const byDeviceRes = await pool.query(`
SELECT device, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND device IS NOT NULL
GROUP BY device ORDER BY n DESC
`, [domain, since])
const byDevice = byDeviceRes.rows.map(r => ({ device: r.device, n: Number(r.n) }))
const byBrowserRes = await pool.query(`
SELECT browser, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND browser IS NOT NULL
GROUP BY browser ORDER BY n DESC LIMIT 6
`, [domain, since])
const byBrowser = byBrowserRes.rows.map(r => ({ browser: r.browser, n: Number(r.n) }))
const byOsRes = await pool.query(`
SELECT os, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND os IS NOT NULL
GROUP BY os ORDER BY n DESC LIMIT 6
`, [domain, since])
const byOs = byOsRes.rows.map(r => ({ os: r.os, n: Number(r.n) }))
const byCountryRes = await pool.query(`
SELECT country, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND country IS NOT NULL
GROUP BY country ORDER BY n DESC LIMIT 10
`, [domain, since])
const byCountry = byCountryRes.rows.map(r => ({ country: r.country, n: Number(r.n) }))
const dailyRes = await pool.query(`
SELECT to_char(to_timestamp(ts), 'YYYY-MM-DD') as day, COUNT(*) as views, COUNT(DISTINCT visitor_id) as visitors
FROM pageviews WHERE domain = $1 AND ts > $2
GROUP BY day ORDER BY day ASC
`, [domain, since])
const daily = dailyRes.rows.map(r => ({ day: r.day, views: Number(r.views), visitors: Number(r.visitors) }))
return { total, unique, topPages, topReferrers, byDevice, byBrowser, byOs, byCountry, daily }
}
export async function getAllDomains(): Promise<string[]> {
const res = await pool.query(`SELECT DISTINCT domain FROM pageviews ORDER BY domain`)
return res.rows.map(r => r.domain)
}
export default pool