diff --git a/app/components/ConsolePage.vue b/app/components/ConsolePage.vue
index 1bc701f..e0c68c5 100644
--- a/app/components/ConsolePage.vue
+++ b/app/components/ConsolePage.vue
@@ -58,11 +58,7 @@
-
@@ -155,7 +153,11 @@
{{ t("routes.emptyGroup") }}
-
+
{{ t("routes.createFirst") }}
@@ -331,7 +333,13 @@ type View = "overview" | "groups" | "logs" | "audit" | "metrics";
const view = computed
(() => {
const seg = route.path.split("/").filter(Boolean)[1];
if (!seg) return "overview";
- if (seg === "overview" || seg === "groups" || seg === "logs" || seg === "audit" || seg === "metrics")
+ if (
+ seg === "overview" ||
+ seg === "groups" ||
+ seg === "logs" ||
+ seg === "audit" ||
+ seg === "metrics"
+ )
return seg;
return null;
});
diff --git a/app/components/MetricsPanel.vue b/app/components/MetricsPanel.vue
index da80665..ef8711b 100644
--- a/app/components/MetricsPanel.vue
+++ b/app/components/MetricsPanel.vue
@@ -208,7 +208,10 @@ function cycleSort(): void {
v-for="s in metrics.byStatus"
:key="s.status"
class="metric-status"
- :class="{ ok: !s.status.startsWith('5') && s.status !== '0', bad: s.status.startsWith('5') || s.status === '0' }"
+ :class="{
+ ok: !s.status.startsWith('5') && s.status !== '0',
+ bad: s.status.startsWith('5') || s.status === '0',
+ }"
>
{{ s.status }} · {{ s.count }}
@@ -232,4 +235,4 @@ function cycleSort(): void {
-
\ No newline at end of file
+
diff --git a/app/components/RouteCard.vue b/app/components/RouteCard.vue
index ba37805..a26ebe2 100644
--- a/app/components/RouteCard.vue
+++ b/app/components/RouteCard.vue
@@ -18,12 +18,19 @@
class="route-badge"
:class="tg.platform === 'telegram' ? 'route-badge-tg' : 'route-badge-dc'"
>
-
+
{{ tg.platform === "telegram" ? "Telegram" : "Discord" }}
- {{ t("route.fallback") }}
+ {{
+ t("route.fallback")
+ }}
{{ t("route.stop") }}
- @roles
+ @roles
@@ -34,7 +41,9 @@
class="route-chip"
:class="{ exclude: f.exclude }"
>
- {{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}
+ {{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}
{{ fmtMatch(f.match) }}
@@ -67,7 +76,18 @@
:title="t('route.moveUp')"
@click="$emit('move', route, -1)"
>
-
+
-
+
-
+
-
+
@@ -118,4 +173,4 @@ function onToggle(event: Event): void {
const next = { ...props.route, enabled: (event.target as HTMLInputElement).checked };
emit("toggle", next);
}
-
\ No newline at end of file
+
diff --git a/app/components/RouteEditor.vue b/app/components/RouteEditor.vue
index 8fbf739..5ecda29 100644
--- a/app/components/RouteEditor.vue
+++ b/app/components/RouteEditor.vue
@@ -102,9 +102,9 @@
:placeholder="t('routeEditor.matchPlaceholder')"
/>
✕
diff --git a/docs/api/admin.md b/docs/api/admin.md
index 0c1df3a..73e13e3 100644
--- a/docs/api/admin.md
+++ b/docs/api/admin.md
@@ -6,31 +6,31 @@ The console itself is served at `/admin`; its tabs are deep-linkable via the URL
## Endpoints
-| Endpoint | Description |
-| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| `GET /admin` | Config console UI |
-| `GET /admin/login` | Start admin sign-in (GitHub OAuth) |
-| `GET /admin/logout` | Sign out and destroy the session |
-| `GET /admin/invite?token=…` | Accept a group invite (browser page) |
-| `GET /admin/api/me` | Current session, scope, groups, and roles |
-| `GET /admin/api/routes` | List routes (scoped to access) |
-| `PUT /admin/api/routes` | Replace routes (owner/admin per group) |
-| `GET /admin/api/groups` | List groups + the signed-in user's role in each |
-| `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
-| `GET /admin/api/groups/:id/routes` | List a group's routes |
-| `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
-| `PUT /admin/api/groups/:id/rename` | Rename a group (owner); routes, webhook secret and invites follow |
-| `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
-| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
-| `DELETE /admin/api/invites/:token` | Revoke an invite (owner) |
-| `GET /admin/api/groups/:id/webhook` | Group webhook endpoint info (owner) |
-| `POST /admin/api/groups/:id/webhook/regenerate` | Generate/regenerate the group webhook secret (owner) |
-| `DELETE /admin/api/groups/:id/webhook` | Disable the group webhook ingress (owner) |
-| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
-| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
-| `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
+| Endpoint | Description |
+| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `GET /admin` | Config console UI |
+| `GET /admin/login` | Start admin sign-in (GitHub OAuth) |
+| `GET /admin/logout` | Sign out and destroy the session |
+| `GET /admin/invite?token=…` | Accept a group invite (browser page) |
+| `GET /admin/api/me` | Current session, scope, groups, and roles |
+| `GET /admin/api/routes` | List routes (scoped to access) |
+| `PUT /admin/api/routes` | Replace routes (owner/admin per group) |
+| `GET /admin/api/groups` | List groups + the signed-in user's role in each |
+| `PUT /admin/api/groups` | Replace groups (super: all; owner: own only) |
+| `GET /admin/api/groups/:id/routes` | List a group's routes |
+| `PUT /admin/api/groups/:id/routes` | Replace a group's routes (owner/admin) |
+| `PUT /admin/api/groups/:id/rename` | Rename a group (owner); routes, webhook secret and invites follow |
+| `GET /admin/api/groups/:id/invites` | List pending invites (owner) |
+| `POST /admin/api/groups/:id/invites` | Create an invite link (owner) |
+| `DELETE /admin/api/invites/:token` | Revoke an invite (owner) |
+| `GET /admin/api/groups/:id/webhook` | Group webhook endpoint info (owner) |
+| `POST /admin/api/groups/:id/webhook/regenerate` | Generate/regenerate the group webhook secret (owner) |
+| `DELETE /admin/api/groups/:id/webhook` | Disable the group webhook ingress (owner) |
+| `GET /admin/api/logs` | Send logs (scoped to accessible routes) |
+| `GET /admin/api/logs/:id` | Single send-log entry (scoped) |
+| `GET /admin/api/audit` | Audit log (scoped to accessible groups) |
| `GET /admin/api/metrics` | Delivery stats (totals, failure rate, per platform/event/status, recent failures); optional `?groupId=` scope; recent failures scoped to accessible groups for non-super |
-| `GET /admin/api/delivery/:deliveryId` | All send-log attempts for one delivery (group-scoped) |
+| `GET /admin/api/delivery/:deliveryId` | All send-log attempts for one delivery (group-scoped) |
## Validation
diff --git a/docs/zh/api/admin.md b/docs/zh/api/admin.md
index 1f82fd2..a559d7c 100644
--- a/docs/zh/api/admin.md
+++ b/docs/zh/api/admin.md
@@ -6,31 +6,31 @@
## 端点
-| 端点 | 说明 |
-| ----------------------------------------------- | ------------------------------------------------------------------------------------ |
-| `GET /admin` | 配置控制台页面 |
-| `GET /admin/login` | 开始管理员登录(GitHub OAuth) |
-| `GET /admin/logout` | 退出登录并销毁会话 |
-| `GET /admin/invite?token=…` | 接受分组邀请(浏览器页面) |
-| `GET /admin/api/me` | 当前会话、权限范围、分组与角色 |
-| `GET /admin/api/routes` | 列出路由(按权限过滤) |
-| `PUT /admin/api/routes` | 替换路由(按分组 owner/admin) |
-| `GET /admin/api/groups` | 列出分组 + 当前用户在各分组的角色 |
-| `PUT /admin/api/groups` | 替换分组(超级管理员全部;owner 仅自己的) |
-| `GET /admin/api/groups/:id/routes` | 列出某分组的路由 |
-| `PUT /admin/api/groups/:id/routes` | 替换某分组的路由(owner/admin) |
-| `PUT /admin/api/groups/:id/rename` | 重命名分组(owner);路由、webhook secret 与邀请自动跟随 |
-| `GET /admin/api/groups/:id/invites` | 列出待处理的邀请(owner) |
-| `POST /admin/api/groups/:id/invites` | 创建邀请链接(owner) |
-| `DELETE /admin/api/invites/:token` | 撤销邀请(owner) |
-| `GET /admin/api/groups/:id/webhook` | 分组 webhook 端点信息(owner) |
-| `POST /admin/api/groups/:id/webhook/regenerate` | 生成/重新生成分组 webhook secret(owner) |
-| `DELETE /admin/api/groups/:id/webhook` | 停用分组 webhook 入口(owner) |
-| `GET /admin/api/logs` | 发送日志(按可访问的路由过滤) |
-| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) |
-| `GET /admin/api/audit` | 审计日志(按可访问的分组过滤) |
+| 端点 | 说明 |
+| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
+| `GET /admin` | 配置控制台页面 |
+| `GET /admin/login` | 开始管理员登录(GitHub OAuth) |
+| `GET /admin/logout` | 退出登录并销毁会话 |
+| `GET /admin/invite?token=…` | 接受分组邀请(浏览器页面) |
+| `GET /admin/api/me` | 当前会话、权限范围、分组与角色 |
+| `GET /admin/api/routes` | 列出路由(按权限过滤) |
+| `PUT /admin/api/routes` | 替换路由(按分组 owner/admin) |
+| `GET /admin/api/groups` | 列出分组 + 当前用户在各分组的角色 |
+| `PUT /admin/api/groups` | 替换分组(超级管理员全部;owner 仅自己的) |
+| `GET /admin/api/groups/:id/routes` | 列出某分组的路由 |
+| `PUT /admin/api/groups/:id/routes` | 替换某分组的路由(owner/admin) |
+| `PUT /admin/api/groups/:id/rename` | 重命名分组(owner);路由、webhook secret 与邀请自动跟随 |
+| `GET /admin/api/groups/:id/invites` | 列出待处理的邀请(owner) |
+| `POST /admin/api/groups/:id/invites` | 创建邀请链接(owner) |
+| `DELETE /admin/api/invites/:token` | 撤销邀请(owner) |
+| `GET /admin/api/groups/:id/webhook` | 分组 webhook 端点信息(owner) |
+| `POST /admin/api/groups/:id/webhook/regenerate` | 生成/重新生成分组 webhook secret(owner) |
+| `DELETE /admin/api/groups/:id/webhook` | 停用分组 webhook 入口(owner) |
+| `GET /admin/api/logs` | 发送日志(按可访问的路由过滤) |
+| `GET /admin/api/logs/:id` | 单条发送日志(按权限过滤) |
+| `GET /admin/api/audit` | 审计日志(按可访问的分组过滤) |
| `GET /admin/api/metrics` | 投递统计(总计、失败率、按平台/事件/状态、最近失败);可选 `?groupId=` 按分组过滤;非超管按可访问分组过滤最近失败 |
-| `GET /admin/api/delivery/:deliveryId` | 单次投递的全部发送日志(按分组过滤) |
+| `GET /admin/api/delivery/:deliveryId` | 单次投递的全部发送日志(按分组过滤) |
## 校验
diff --git a/server/lib/lib/send-log.ts b/server/lib/lib/send-log.ts
index 843ca74..e7dc4b1 100644
--- a/server/lib/lib/send-log.ts
+++ b/server/lib/lib/send-log.ts
@@ -159,7 +159,9 @@ export async function getFailedSendLog(
`SELECT ${COLUMNS} FROM send_logs WHERE ok = 0 AND group_id = ? ORDER BY ts DESC LIMIT ?`,
)
.bind(groupId, limit)
- : db.prepare(`SELECT ${COLUMNS} FROM send_logs WHERE ok = 0 ORDER BY ts DESC LIMIT ?`).bind(limit);
+ : db
+ .prepare(`SELECT ${COLUMNS} FROM send_logs WHERE ok = 0 ORDER BY ts DESC LIMIT ?`)
+ .bind(limit);
const { results } = await stmt.all();
return results.map(toRecord);
} catch (err) {
diff --git a/server/lib/observability/metrics.ts b/server/lib/observability/metrics.ts
index 9c12c7a..848830a 100644
--- a/server/lib/observability/metrics.ts
+++ b/server/lib/observability/metrics.ts
@@ -54,7 +54,10 @@ const EMPTY: DeliveryMetrics = {
recentFailures: [],
};
-export async function getDeliveryMetrics(db: D1Database, groupId?: string): Promise {
+export async function getDeliveryMetrics(
+ db: D1Database,
+ groupId?: string,
+): Promise {
const where = groupId ? " WHERE group_id = ?" : "";
const and = groupId ? " AND group_id = ?" : "";
const bindGroup = (stmt: D1PreparedStatement): D1PreparedStatement =>
@@ -86,7 +89,9 @@ export async function getDeliveryMetrics(db: D1Database, groupId?: string): Prom
),
).all();
const duration = await bindGroup(
- db.prepare(`SELECT AVG(duration_ms) AS avg FROM send_logs WHERE duration_ms IS NOT NULL${and}`),
+ db.prepare(
+ `SELECT AVG(duration_ms) AS avg FROM send_logs WHERE duration_ms IS NOT NULL${and}`,
+ ),
).all();
const attempts = await bindGroup(
db.prepare(
diff --git a/tests/formatter.test.ts b/tests/formatter.test.ts
index e8a6601..9865669 100644
--- a/tests/formatter.test.ts
+++ b/tests/formatter.test.ts
@@ -32,14 +32,16 @@ describe("message title spec", () => {
sender,
}),
);
- expect(msg.title).toBe(
- "acme/widget: Pushed 1 commit to main",
- );
+ expect(msg.title).toBe("acme/widget: Pushed 1 commit to main");
expect(msg.description).toBe(
"[`abcd123`](https://github.com/acme/widget/commit/abcd1234ef) fix stuff",
);
expect(msg.fields).toEqual([
- { name: "Changes", value: "[View comparison](https://github.com/acme/widget/compare/abc...def):", inline: false },
+ {
+ name: "Changes",
+ value: "[View comparison](https://github.com/acme/widget/compare/abc...def):",
+ inline: false,
+ },
]);
});