mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
feat: migrate to Nuxt 4 (Nitro) and Tailwind CSS v3
This commit is contained in:
parent
f4959eebf8
commit
b139712a91
166 changed files with 19790 additions and 5539 deletions
107
server/lib/lib/audit.ts
Normal file
107
server/lib/lib/audit.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { log } from "./log";
|
||||
|
||||
export interface AuditEntry {
|
||||
id?: number;
|
||||
ts: number;
|
||||
actorId?: string;
|
||||
actorLogin?: string;
|
||||
/** Machine-readable action, e.g. "session.login", "group.update", "invite.create". */
|
||||
action: string;
|
||||
targetType?: string;
|
||||
targetId?: string;
|
||||
groupId?: string;
|
||||
/** Free-form metadata. Never include secrets or message bodies. */
|
||||
detail?: Record<string, unknown>;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
const COLUMNS =
|
||||
"id, ts, actor_id, actor_login, action, target_type, target_id, group_id, detail, ip";
|
||||
|
||||
interface AuditRow {
|
||||
id: number;
|
||||
ts: number;
|
||||
actor_id: string | null;
|
||||
actor_login: string | null;
|
||||
action: string;
|
||||
target_type: string | null;
|
||||
target_id: string | null;
|
||||
group_id: string | null;
|
||||
detail: string | null;
|
||||
ip: string | null;
|
||||
}
|
||||
|
||||
function toEntry(r: AuditRow): AuditEntry {
|
||||
return {
|
||||
id: r.id,
|
||||
ts: r.ts,
|
||||
actorId: r.actor_id ?? undefined,
|
||||
actorLogin: r.actor_login ?? undefined,
|
||||
action: r.action,
|
||||
targetType: r.target_type ?? undefined,
|
||||
targetId: r.target_id ?? undefined,
|
||||
groupId: r.group_id ?? undefined,
|
||||
detail: r.detail ? (JSON.parse(r.detail) as Record<string, unknown>) : undefined,
|
||||
ip: r.ip ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Best-effort write; failures never break the business flow. */
|
||||
export async function recordAudit(db: D1Database, entry: AuditEntry): Promise<void> {
|
||||
try {
|
||||
await db
|
||||
.prepare(
|
||||
`INSERT INTO audit_logs (ts, actor_id, actor_login, action, target_type, target_id, group_id, detail, ip)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.bind(
|
||||
entry.ts,
|
||||
entry.actorId ?? null,
|
||||
entry.actorLogin ?? null,
|
||||
entry.action,
|
||||
entry.targetType ?? null,
|
||||
entry.targetId ?? null,
|
||||
entry.groupId ?? null,
|
||||
entry.detail ? JSON.stringify(entry.detail) : null,
|
||||
entry.ip ?? null,
|
||||
)
|
||||
.run();
|
||||
} catch (err) {
|
||||
log.warn({ err, action: entry.action }, "Failed to record audit entry");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAuditLog(
|
||||
db: D1Database,
|
||||
opts: { groupId?: string; limit?: number } = {},
|
||||
): Promise<AuditEntry[]> {
|
||||
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
|
||||
try {
|
||||
if (opts.groupId) {
|
||||
const { results } = await db
|
||||
.prepare(`SELECT ${COLUMNS} FROM audit_logs WHERE group_id = ? ORDER BY ts DESC LIMIT ?`)
|
||||
.bind(opts.groupId, limit)
|
||||
.all<AuditRow>();
|
||||
return results.map(toEntry);
|
||||
}
|
||||
const { results } = await db
|
||||
.prepare(`SELECT ${COLUMNS} FROM audit_logs ORDER BY ts DESC LIMIT ?`)
|
||||
.bind(limit)
|
||||
.all<AuditRow>();
|
||||
return results.map(toEntry);
|
||||
} catch (err) {
|
||||
log.warn({ err }, "Failed to load audit log");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function pruneAuditLogs(db: D1Database, retentionDays: number): Promise<number> {
|
||||
const cutoff = Date.now() - retentionDays * 86400_000;
|
||||
try {
|
||||
const { meta } = await db.prepare("DELETE FROM audit_logs WHERE ts < ?").bind(cutoff).run();
|
||||
return meta.changes;
|
||||
} catch (err) {
|
||||
log.warn({ err }, "Failed to prune audit logs");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
63
server/lib/lib/i18n.ts
Normal file
63
server/lib/lib/i18n.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { en } from "./locales/en";
|
||||
import { zh } from "./locales/zh";
|
||||
|
||||
type NestedStrings = { [key: string]: string | NestedStrings };
|
||||
export type Translations = NestedStrings;
|
||||
|
||||
function getByPath(obj: Record<string, unknown>, path: string): string | undefined {
|
||||
const parts = path.split(".");
|
||||
let current: unknown = obj;
|
||||
for (const part of parts) {
|
||||
if (current == null || typeof current !== "object") return undefined;
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
return typeof current === "string" ? current : undefined;
|
||||
}
|
||||
|
||||
function interpolate(template: string, params: Record<string, string | number>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (_, key: string) => {
|
||||
return key in params ? String(params[key]) : `{${key}}`;
|
||||
});
|
||||
}
|
||||
|
||||
const cache = new Map<string, Translations>();
|
||||
|
||||
export async function loadTranslations(
|
||||
lang: string,
|
||||
kv?: { get<T>(key: string, type: "json"): Promise<T | null> },
|
||||
): Promise<Translations> {
|
||||
if (lang === "en") return en;
|
||||
if (lang === "zh") return zh;
|
||||
|
||||
const cached = cache.get(lang);
|
||||
if (cached) return cached;
|
||||
|
||||
if (!kv) return en;
|
||||
|
||||
try {
|
||||
const stored = await kv.get<Partial<Translations>>(`i18n:${lang}`, "json");
|
||||
if (stored) {
|
||||
const merged = { ...en, ...stored } as Translations;
|
||||
cache.set(lang, merged);
|
||||
return merged;
|
||||
}
|
||||
} catch {
|
||||
// KV read failed, fall back to EN
|
||||
}
|
||||
|
||||
return en;
|
||||
}
|
||||
|
||||
export function t(
|
||||
key: string,
|
||||
params?: Record<string, string | number>,
|
||||
lang?: string | null,
|
||||
translations?: Translations,
|
||||
): string {
|
||||
const dict = translations ?? en;
|
||||
const raw =
|
||||
getByPath(dict as Record<string, unknown>, key) ??
|
||||
getByPath(en as Record<string, unknown>, key) ??
|
||||
key;
|
||||
return params ? interpolate(raw, params) : raw;
|
||||
}
|
||||
208
server/lib/lib/locales/en.ts
Normal file
208
server/lib/lib/locales/en.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
export const en = {
|
||||
actions: {
|
||||
opened: "Opened",
|
||||
closed: "Closed",
|
||||
reopened: "Reopened",
|
||||
synchronized: "Synchronized",
|
||||
edited: "Edited",
|
||||
labeled: "Labeled",
|
||||
unlabeled: "Unlabeled",
|
||||
assigned: "Assigned",
|
||||
unassigned: "Unassigned",
|
||||
converted_to_draft: "Converted to Draft",
|
||||
ready_for_review: "Ready for Review",
|
||||
completed: "Completed",
|
||||
published: "Published",
|
||||
created: "Created",
|
||||
deleted: "Deleted",
|
||||
started: "Started",
|
||||
added: "Added",
|
||||
removed: "Removed",
|
||||
submitted: "Submitted",
|
||||
dismissed: "Dismissed",
|
||||
approved: "Approved",
|
||||
changes_requested: "Changes Requested",
|
||||
answered: "Answered",
|
||||
unanswered: "Unanswered",
|
||||
pinned: "Pinned",
|
||||
unpinned: "Unpinned",
|
||||
transferred: "Transferred",
|
||||
publicized: "made public",
|
||||
privatized: "made private",
|
||||
locked: "Locked",
|
||||
unlocked: "Unlocked",
|
||||
renamed: "Renamed",
|
||||
archived: "Archived",
|
||||
unarchived: "Unarchived",
|
||||
fixed: "Fixed",
|
||||
appeared_in_branch: "Appeared in Branch",
|
||||
reopened_by_user: "Reopened by User",
|
||||
closed_by_user: "Closed by User",
|
||||
},
|
||||
fields: {
|
||||
branch: "Branch",
|
||||
changes: "Changes",
|
||||
labels: "Labels",
|
||||
assignees: "Assignees",
|
||||
milestone: "Milestone",
|
||||
status: "Status",
|
||||
run: "Run",
|
||||
job: "Job",
|
||||
workflow: "Workflow",
|
||||
context: "Context",
|
||||
duration: "Duration",
|
||||
type: "Type",
|
||||
name: "Name",
|
||||
description: "Description",
|
||||
file: "File",
|
||||
commit: "Commit",
|
||||
environment: "Environment",
|
||||
url: "URL",
|
||||
service: "Service",
|
||||
severity: "Severity",
|
||||
rule: "Rule",
|
||||
package: "Package",
|
||||
vulnerable_range: "Vulnerable Range",
|
||||
summary: "Summary",
|
||||
progress: "Progress",
|
||||
due: "Due",
|
||||
number: "Number",
|
||||
color: "Color",
|
||||
label: "Label",
|
||||
transferred: "Transferred",
|
||||
renamed: "Renamed",
|
||||
details: "Details",
|
||||
branch_tag: "Branch/Tag",
|
||||
},
|
||||
common: {
|
||||
footer: "{repo}",
|
||||
unknown: "unknown",
|
||||
no_message: "no message",
|
||||
repository: "repository",
|
||||
untitled: "Untitled",
|
||||
github: "GitHub",
|
||||
and_n_more: "... and {count} more commits",
|
||||
n_files: "{count} files",
|
||||
},
|
||||
events: {
|
||||
push: {
|
||||
force_push: "**Force push**",
|
||||
branch_created: "Branch created",
|
||||
commits_pushed: "**{count}** commit{s} pushed to {ref}",
|
||||
view_comparison: "[View comparison]({url})",
|
||||
added: "+{count} added",
|
||||
removed: "-{count} removed",
|
||||
modified: "~{count} modified",
|
||||
title: "{repo}: Pushed {count} commit{s}",
|
||||
},
|
||||
pr: {
|
||||
action_pr: "{emoji}**{action}** pull request",
|
||||
title: "{repo}#{number}: {title}",
|
||||
},
|
||||
issues: {
|
||||
action_issue: "{emoji}**{action}** issue",
|
||||
title: "{repo}#{number}: {title}",
|
||||
},
|
||||
issue_comment: {
|
||||
title: "{repo}#{number}: {title}",
|
||||
action_comment: "{emoji}**{action}** comment",
|
||||
},
|
||||
workflow_run: {
|
||||
title: "{repo}: {name} — {conclusion}",
|
||||
},
|
||||
workflow_job: {
|
||||
title: "{repo}: Job {name} — {conclusion}",
|
||||
},
|
||||
status: {
|
||||
title: "{repo}: {context} — {state}",
|
||||
},
|
||||
ping: {
|
||||
title: "{repo}: Webhook ping",
|
||||
},
|
||||
release: {
|
||||
action_release: "{emoji}**{action}** release `{tag}`",
|
||||
title: "{repo}: {name}",
|
||||
},
|
||||
create: {
|
||||
title: "{repo}: {emoji}Created {type} {ref}",
|
||||
},
|
||||
delete: {
|
||||
title: "{repo}: {emoji}Deleted {type} {ref}",
|
||||
},
|
||||
star: {
|
||||
starred: "Starred",
|
||||
unstarred: "Unstarred",
|
||||
title: "{repo}: {emoji}{label}",
|
||||
},
|
||||
fork: {
|
||||
title: "{repo}: {emoji}Forked to {forkee}",
|
||||
},
|
||||
check_run: {
|
||||
title: "{repo}: {name} — {conclusion}",
|
||||
},
|
||||
check_suite: {
|
||||
title: "{repo}: Check suite {conclusion}",
|
||||
},
|
||||
pr_review: {
|
||||
action_review: "{emoji}**{action}** review",
|
||||
title: "{repo}#{number}: {title}",
|
||||
},
|
||||
pr_review_comment: {
|
||||
action_inline: "{emoji}**{action}** inline comment",
|
||||
title: "{repo}#{number}: {title}",
|
||||
line: " (line {position})",
|
||||
},
|
||||
commit_comment: {
|
||||
action_comment: "{emoji}**{action}**",
|
||||
title: "{repo}: Comment on commit {sha}",
|
||||
},
|
||||
deployment: {
|
||||
title: "{repo}: Deployment to `{env}` — {state}",
|
||||
},
|
||||
member: {
|
||||
title: "{repo}: {emoji}{action} collaborator: {name}",
|
||||
},
|
||||
label: {
|
||||
title: "{repo}: {emoji}Label {action}: {name}",
|
||||
},
|
||||
milestone: {
|
||||
title: "{repo}: {emoji}Milestone {action}: {title}",
|
||||
},
|
||||
discussion: {
|
||||
title: "{repo}#{number}: {title}",
|
||||
action_discussion: "{emoji}{action} discussion{category}",
|
||||
},
|
||||
discussion_comment: {
|
||||
title: "{repo}#{number}: {title}",
|
||||
action_comment: "{emoji}**{action}** comment",
|
||||
},
|
||||
repository: {
|
||||
title: "{repo}: {emoji}Repository {action}",
|
||||
open: "Open repository",
|
||||
public: "public",
|
||||
private: "private",
|
||||
internal: "internal",
|
||||
is_fork: "This is a fork",
|
||||
visibility: "Visibility",
|
||||
},
|
||||
code_scanning: {
|
||||
title: "{repo}: Code Scanning {action}",
|
||||
},
|
||||
dependabot: {
|
||||
title: "{repo}: Dependabot {action}",
|
||||
},
|
||||
generic: {
|
||||
title: "{repo}: {event}{action}",
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
title_fallback: "Custom message",
|
||||
},
|
||||
log: {
|
||||
title: "{repo}: {event}{action}",
|
||||
routes: "Routes",
|
||||
delivery: "Delivery",
|
||||
route_ok: "✅ {route} → {target}",
|
||||
route_fail: "❌ {route} → {target}: {error}",
|
||||
},
|
||||
};
|
||||
208
server/lib/lib/locales/zh.ts
Normal file
208
server/lib/lib/locales/zh.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
export const zh = {
|
||||
actions: {
|
||||
opened: "已打开",
|
||||
closed: "已关闭",
|
||||
reopened: "重新打开",
|
||||
synchronized: "已同步",
|
||||
edited: "已编辑",
|
||||
labeled: "已添加标签",
|
||||
unlabeled: "已移除标签",
|
||||
assigned: "已分配",
|
||||
unassigned: "已取消分配",
|
||||
converted_to_draft: "已转为草稿",
|
||||
ready_for_review: "可供审查",
|
||||
completed: "已完成",
|
||||
published: "已发布",
|
||||
created: "已创建",
|
||||
deleted: "已删除",
|
||||
started: "已开始",
|
||||
added: "已添加",
|
||||
removed: "已移除",
|
||||
submitted: "已提交",
|
||||
dismissed: "已忽略",
|
||||
approved: "已批准",
|
||||
changes_requested: "请求修改",
|
||||
answered: "已回答",
|
||||
unanswered: "未回答",
|
||||
pinned: "已置顶",
|
||||
unpinned: "已取消置顶",
|
||||
transferred: "已转移",
|
||||
publicized: "已公开",
|
||||
privatized: "已设为私有",
|
||||
locked: "已锁定",
|
||||
unlocked: "已解锁",
|
||||
renamed: "已重命名",
|
||||
archived: "已归档",
|
||||
unarchived: "已取消归档",
|
||||
fixed: "已修复",
|
||||
appeared_in_branch: "出现在分支中",
|
||||
reopened_by_user: "用户重新打开",
|
||||
closed_by_user: "用户关闭",
|
||||
},
|
||||
fields: {
|
||||
branch: "分支",
|
||||
changes: "变更",
|
||||
labels: "标签",
|
||||
assignees: "指派人",
|
||||
milestone: "里程碑",
|
||||
status: "状态",
|
||||
run: "运行",
|
||||
job: "作业",
|
||||
workflow: "工作流",
|
||||
context: "上下文",
|
||||
duration: "耗时",
|
||||
type: "类型",
|
||||
name: "名称",
|
||||
description: "描述",
|
||||
file: "文件",
|
||||
commit: "提交",
|
||||
environment: "环境",
|
||||
url: "链接",
|
||||
service: "服务",
|
||||
severity: "严重程度",
|
||||
rule: "规则",
|
||||
package: "包",
|
||||
vulnerable_range: "受影响版本",
|
||||
summary: "摘要",
|
||||
progress: "进度",
|
||||
due: "截止日期",
|
||||
number: "编号",
|
||||
color: "颜色",
|
||||
label: "标签",
|
||||
transferred: "已转移",
|
||||
renamed: "已重命名",
|
||||
details: "详情",
|
||||
branch_tag: "分支/标签",
|
||||
},
|
||||
common: {
|
||||
footer: "{repo}",
|
||||
unknown: "未知",
|
||||
no_message: "无消息",
|
||||
repository: "仓库",
|
||||
untitled: "无标题",
|
||||
github: "GitHub",
|
||||
and_n_more: "... 还有 {count} 个提交",
|
||||
n_files: "{count} 个文件",
|
||||
},
|
||||
events: {
|
||||
push: {
|
||||
force_push: "**强制推送**",
|
||||
branch_created: "分支已创建",
|
||||
commits_pushed: "**{count}** 个提交已推送到 {ref}",
|
||||
view_comparison: "[查看比较]({url})",
|
||||
added: "+{count} 新增",
|
||||
removed: "-{count} 删除",
|
||||
modified: "~{count} 修改",
|
||||
title: "{repo}: 推送了 {count} 个提交",
|
||||
},
|
||||
pr: {
|
||||
action_pr: "{emoji}**{action}** 拉取请求",
|
||||
title: "{repo}#{number}: {title}",
|
||||
},
|
||||
issues: {
|
||||
action_issue: "{emoji}**{action}** 议题",
|
||||
title: "{repo}#{number}: {title}",
|
||||
},
|
||||
issue_comment: {
|
||||
title: "{repo}#{number}: {title}",
|
||||
action_comment: "{emoji}**{action}** 评论",
|
||||
},
|
||||
workflow_run: {
|
||||
title: "{repo}: {name} — {conclusion}",
|
||||
},
|
||||
workflow_job: {
|
||||
title: "{repo}: 作业 {name} — {conclusion}",
|
||||
},
|
||||
status: {
|
||||
title: "{repo}: {context} — {state}",
|
||||
},
|
||||
ping: {
|
||||
title: "{repo}: Webhook ping",
|
||||
},
|
||||
release: {
|
||||
action_release: "{emoji}**{action}** 发布 `{tag}`",
|
||||
title: "{repo}: {name}",
|
||||
},
|
||||
create: {
|
||||
title: "{repo}: {emoji}已创建{type} {ref}",
|
||||
},
|
||||
delete: {
|
||||
title: "{repo}: {emoji}已删除{type} {ref}",
|
||||
},
|
||||
star: {
|
||||
starred: "已加星标",
|
||||
unstarred: "已取消星标",
|
||||
title: "{repo}: {emoji}{label}",
|
||||
},
|
||||
fork: {
|
||||
title: "{repo}: {emoji}复刻到 {forkee}",
|
||||
},
|
||||
check_run: {
|
||||
title: "{repo}: {name} — {conclusion}",
|
||||
},
|
||||
check_suite: {
|
||||
title: "{repo}: 检查套件 {conclusion}",
|
||||
},
|
||||
pr_review: {
|
||||
action_review: "{emoji}**{action}** 审查",
|
||||
title: "{repo}#{number}: {title}",
|
||||
},
|
||||
pr_review_comment: {
|
||||
action_inline: "{emoji}**{action}** 行内评论",
|
||||
title: "{repo}#{number}: {title}",
|
||||
line: " (第 {position} 行)",
|
||||
},
|
||||
commit_comment: {
|
||||
action_comment: "{emoji}**{action}**",
|
||||
title: "{repo}: 提交 {sha} 的评论",
|
||||
},
|
||||
deployment: {
|
||||
title: "{repo}: 部署到 `{env}` — {state}",
|
||||
},
|
||||
member: {
|
||||
title: "{repo}: {emoji}{action} 协作者: {name}",
|
||||
},
|
||||
label: {
|
||||
title: "{repo}: {emoji}标签 {action}: {name}",
|
||||
},
|
||||
milestone: {
|
||||
title: "{repo}: {emoji}里程碑 {action}: {title}",
|
||||
},
|
||||
discussion: {
|
||||
title: "{repo}#{number}: {title}",
|
||||
action_discussion: "{emoji}{action} 讨论{category}",
|
||||
},
|
||||
discussion_comment: {
|
||||
title: "{repo}#{number}: {title}",
|
||||
action_comment: "{emoji}**{action}** 评论",
|
||||
},
|
||||
repository: {
|
||||
title: "{repo}: {emoji}仓库 {action}",
|
||||
open: "打开仓库",
|
||||
public: "公开",
|
||||
private: "私有",
|
||||
internal: "内部",
|
||||
is_fork: "这是 Fork 仓库",
|
||||
visibility: "可见性",
|
||||
},
|
||||
code_scanning: {
|
||||
title: "{repo}: 代码扫描 {action}",
|
||||
},
|
||||
dependabot: {
|
||||
title: "{repo}: Dependabot {action}",
|
||||
},
|
||||
generic: {
|
||||
title: "{repo}: {event}{action}",
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
title_fallback: "自定义消息",
|
||||
},
|
||||
log: {
|
||||
title: "{repo}: {event}{action}",
|
||||
routes: "路由",
|
||||
delivery: "投递",
|
||||
route_ok: "✅ {route} → {target}",
|
||||
route_fail: "❌ {route} → {target}: {error}",
|
||||
},
|
||||
};
|
||||
10
server/lib/lib/log.ts
Normal file
10
server/lib/lib/log.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const log = {
|
||||
info: (msg: string | object, ...args: unknown[]): void =>
|
||||
console.log(JSON.stringify({ level: "info", msg, ...args })),
|
||||
warn: (msg: string | object, ...args: unknown[]): void =>
|
||||
console.warn(JSON.stringify({ level: "warn", msg, ...args })),
|
||||
error: (msg: string | object, ...args: unknown[]): void =>
|
||||
console.error(JSON.stringify({ level: "error", msg, ...args })),
|
||||
fatal: (msg: string | object, ...args: unknown[]): void =>
|
||||
console.error(JSON.stringify({ level: "fatal", msg, ...args })),
|
||||
};
|
||||
132
server/lib/lib/send-log.ts
Normal file
132
server/lib/lib/send-log.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { log } from "./log";
|
||||
|
||||
export interface SendRecord {
|
||||
id?: number;
|
||||
ts: number;
|
||||
routeId: string;
|
||||
groupId?: string;
|
||||
event: string;
|
||||
repo?: string;
|
||||
target: string;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
status?: number;
|
||||
messageId?: string;
|
||||
deliveryId?: string;
|
||||
platform?: string;
|
||||
actor?: string;
|
||||
action?: string;
|
||||
durationMs?: number;
|
||||
errorCode?: string;
|
||||
attempts?: number;
|
||||
detail?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const COLUMNS =
|
||||
"id, ts, route_id, group_id, event, repo, target, ok, error, status, message_id, delivery_id, platform, actor, action, duration_ms, error_code, attempts, detail";
|
||||
|
||||
interface LogRow {
|
||||
id: number;
|
||||
ts: number;
|
||||
route_id: string;
|
||||
group_id: string | null;
|
||||
event: string;
|
||||
repo: string | null;
|
||||
target: string;
|
||||
ok: number;
|
||||
error: string | null;
|
||||
status: number | null;
|
||||
message_id: string | null;
|
||||
delivery_id: string | null;
|
||||
platform: string | null;
|
||||
actor: string | null;
|
||||
action: string | null;
|
||||
duration_ms: number | null;
|
||||
error_code: string | null;
|
||||
attempts: number | null;
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
function toRecord(r: LogRow): SendRecord {
|
||||
return {
|
||||
id: r.id,
|
||||
ts: r.ts,
|
||||
routeId: r.route_id,
|
||||
groupId: r.group_id ?? undefined,
|
||||
event: r.event,
|
||||
repo: r.repo ?? undefined,
|
||||
target: r.target,
|
||||
ok: r.ok === 1,
|
||||
error: r.error ?? undefined,
|
||||
status: r.status ?? undefined,
|
||||
messageId: r.message_id ?? undefined,
|
||||
deliveryId: r.delivery_id ?? undefined,
|
||||
platform: r.platform ?? undefined,
|
||||
actor: r.actor ?? undefined,
|
||||
action: r.action ?? undefined,
|
||||
durationMs: r.duration_ms ?? undefined,
|
||||
errorCode: r.error_code ?? undefined,
|
||||
attempts: r.attempts ?? undefined,
|
||||
detail: r.detail ? (JSON.parse(r.detail) as Record<string, unknown>) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function recordSend(db: D1Database, record: SendRecord): Promise<void> {
|
||||
try {
|
||||
await db
|
||||
.prepare(
|
||||
`INSERT INTO send_logs (ts, route_id, group_id, event, repo, target, ok, error, status, message_id, delivery_id, platform, actor, action, duration_ms, error_code, attempts, detail)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.bind(
|
||||
record.ts,
|
||||
record.routeId,
|
||||
record.groupId ?? null,
|
||||
record.event,
|
||||
record.repo ?? null,
|
||||
record.target,
|
||||
record.ok ? 1 : 0,
|
||||
record.error ?? null,
|
||||
record.status ?? null,
|
||||
record.messageId ?? null,
|
||||
record.deliveryId ?? null,
|
||||
record.platform ?? null,
|
||||
record.actor ?? null,
|
||||
record.action ?? null,
|
||||
record.durationMs ?? null,
|
||||
record.errorCode ?? null,
|
||||
record.attempts ?? null,
|
||||
record.detail ? JSON.stringify(record.detail) : null,
|
||||
)
|
||||
.run();
|
||||
} catch (err) {
|
||||
log.warn({ err }, "Failed to record send log");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSendLog(db: D1Database, limit = 50): Promise<SendRecord[]> {
|
||||
try {
|
||||
const { results } = await db
|
||||
.prepare(`SELECT ${COLUMNS} FROM send_logs ORDER BY ts DESC LIMIT ?`)
|
||||
.bind(limit)
|
||||
.all<LogRow>();
|
||||
return results.map(toRecord);
|
||||
} catch (err) {
|
||||
log.warn({ err }, "Failed to load send log");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSendLogById(db: D1Database, id: number): Promise<SendRecord | null> {
|
||||
try {
|
||||
const { results } = await db
|
||||
.prepare(`SELECT ${COLUMNS} FROM send_logs WHERE id = ? LIMIT 1`)
|
||||
.bind(id)
|
||||
.all<LogRow>();
|
||||
const row = results[0];
|
||||
return row ? toRecord(row) : null;
|
||||
} catch (err) {
|
||||
log.warn({ err, id }, "Failed to load send log entry");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue