mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
chore: auto-fix lint & formatting [skip ci]
This commit is contained in:
parent
df0edbb581
commit
3bcc98efc8
9 changed files with 152 additions and 77 deletions
|
|
@ -58,11 +58,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-if="view === 'overview'"
|
||||
class="btn btn-ghost btn-sm"
|
||||
@click="refreshOverview"
|
||||
>
|
||||
<button v-if="view === 'overview'" class="btn btn-ghost btn-sm" @click="refreshOverview">
|
||||
{{ t("metrics.refresh") }}
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="toggle">{{ t("app.langToggle") }}</button>
|
||||
|
|
@ -126,7 +122,9 @@
|
|||
<button class="btn btn-ghost btn-sm" @click="exitGroup">
|
||||
{{ t("group.back") }}
|
||||
</button>
|
||||
<span class="kpi-label">{{ t("group.routesIn", { name: selectedGroup.name }) }}</span>
|
||||
<span class="kpi-label">{{
|
||||
t("group.routesIn", { name: selectedGroup.name })
|
||||
}}</span>
|
||||
<span class="kpi">{{ groupRoutes.length }}</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
|
|
@ -155,7 +153,11 @@
|
|||
|
||||
<section v-if="!groupRoutesLoading && !groupRoutes.length" class="empty">
|
||||
<p>{{ t("routes.emptyGroup") }}</p>
|
||||
<button v-if="canEditRoutes(selectedGroup.id)" class="btn btn-accent" @click="openNew">
|
||||
<button
|
||||
v-if="canEditRoutes(selectedGroup.id)"
|
||||
class="btn btn-accent"
|
||||
@click="openNew"
|
||||
>
|
||||
{{ t("routes.createFirst") }}
|
||||
</button>
|
||||
</section>
|
||||
|
|
@ -331,7 +333,13 @@ type View = "overview" | "groups" | "logs" | "audit" | "metrics";
|
|||
const view = computed<View | null>(() => {
|
||||
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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
</span>
|
||||
|
|
@ -232,4 +235,4 @@ function cycleSort(): void {
|
|||
</article>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -18,12 +18,19 @@
|
|||
class="route-badge"
|
||||
:class="tg.platform === 'telegram' ? 'route-badge-tg' : 'route-badge-dc'"
|
||||
>
|
||||
<span class="route-badge-dot" :class="tg.platform === 'telegram' ? 'bg-info' : 'bg-accent'"></span>
|
||||
<span
|
||||
class="route-badge-dot"
|
||||
:class="tg.platform === 'telegram' ? 'bg-info' : 'bg-accent'"
|
||||
></span>
|
||||
{{ tg.platform === "telegram" ? "Telegram" : "Discord" }}
|
||||
</span>
|
||||
<span v-if="route.fallback" class="route-badge route-badge-fallback">{{ t("route.fallback") }}</span>
|
||||
<span v-if="route.fallback" class="route-badge route-badge-fallback">{{
|
||||
t("route.fallback")
|
||||
}}</span>
|
||||
<span v-if="route.stop" class="route-badge route-badge-stop">{{ t("route.stop") }}</span>
|
||||
<span v-if="route.discordRoleIds?.length" class="route-badge route-badge-role">@roles</span>
|
||||
<span v-if="route.discordRoleIds?.length" class="route-badge route-badge-role"
|
||||
>@roles</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -34,7 +41,9 @@
|
|||
class="route-chip"
|
||||
:class="{ exclude: f.exclude }"
|
||||
>
|
||||
<span class="route-chip-type">{{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}</span>
|
||||
<span class="route-chip-type"
|
||||
>{{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}</span
|
||||
>
|
||||
<span class="route-chip-val">{{ fmtMatch(f.match) }}</span>
|
||||
</span>
|
||||
<span v-if="!route.filters.length" class="route-chip route-chip-empty">
|
||||
|
|
@ -67,7 +76,18 @@
|
|||
:title="t('route.moveUp')"
|
||||
@click="$emit('move', route, -1)"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m18 15-6-6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="route-action-btn"
|
||||
|
|
@ -75,21 +95,56 @@
|
|||
:title="t('route.moveDown')"
|
||||
@click="$emit('move', route, 1)"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="route-action-btn"
|
||||
:title="t('routeEditor.editTitle')"
|
||||
@click="$emit('edit', route)"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||
<path d="m15 5 4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="route-action-btn route-action-btn-danger"
|
||||
:title="t('routeEditor.close')"
|
||||
@click="$emit('delete', route)"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -118,4 +173,4 @@ function onToggle(event: Event): void {
|
|||
const next = { ...props.route, enabled: (event.target as HTMLInputElement).checked };
|
||||
emit("toggle", next);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -102,9 +102,9 @@
|
|||
:placeholder="t('routeEditor.matchPlaceholder')"
|
||||
/>
|
||||
<label class="inline">
|
||||
<input v-model="f.exclude" type="checkbox" /><span
|
||||
>{{ t("routeEditor.not") }}</span
|
||||
>
|
||||
<input v-model="f.exclude" type="checkbox" /><span>{{
|
||||
t("routeEditor.not")
|
||||
}}</span>
|
||||
</label>
|
||||
<button type="button" class="icon-btn danger" @click="form.filters.splice(i, 1)">
|
||||
✕
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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` | 单次投递的全部发送日志(按分组过滤) |
|
||||
|
||||
## 校验
|
||||
|
||||
|
|
|
|||
|
|
@ -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<LogRow>();
|
||||
return results.map(toRecord);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ const EMPTY: DeliveryMetrics = {
|
|||
recentFailures: [],
|
||||
};
|
||||
|
||||
export async function getDeliveryMetrics(db: D1Database, groupId?: string): Promise<DeliveryMetrics> {
|
||||
export async function getDeliveryMetrics(
|
||||
db: D1Database,
|
||||
groupId?: string,
|
||||
): Promise<DeliveryMetrics> {
|
||||
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<StatusRow>();
|
||||
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<AvgRow>();
|
||||
const attempts = await bindGroup(
|
||||
db.prepare(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue