From abca2e7bff79f1fde8cfecd6a50cfc062ca707a4 Mon Sep 17 00:00:00 2001 From: TyperBody Date: Sat, 26 Sep 2026 02:43:48 +0800 Subject: [PATCH] fix(operation-trace): repair export URL, drop dead action, prune unused keys - Export download failed because the URL builder appended the path to a "/" base, producing a protocol-relative "//api/..." URL that the browser read as host "api"; the request never reached the backend. Resolve the base to the current origin, mirroring the other URL builders. - Remove the orphan ``clear_prune`` action: the branch has no prune/clear route and no route rule maps to it, so it could never be produced. Its i18n key is removed too. - Remove ``tamperedCount``, now unreferenced after the verification badge was split into integrity/chain counters. The operationTrace catalogue is now free of unproduced keys. - Correct the settings controller docstring: only governance, operation logs, filters and export routes exist; there is no on-demand delete. Verified: no producer for clear_prune, classify() can no longer emit it, operationTrace orphan scan returns none, tsc/check-i18n/prettier/py_compile all pass. --- .../pkg/api/http/controller/groups/settings.py | 15 +++++++++------ src/langbot/pkg/api/http/service/settings.py | 6 ------ web/src/app/infra/http/BackendClient.ts | 11 +++++++++-- web/src/i18n/locales/en-US.ts | 2 -- web/src/i18n/locales/es-ES.ts | 2 -- web/src/i18n/locales/ja-JP.ts | 2 -- web/src/i18n/locales/ru-RU.ts | 2 -- web/src/i18n/locales/th-TH.ts | 2 -- web/src/i18n/locales/vi-VN.ts | 2 -- web/src/i18n/locales/zh-Hans.ts | 2 -- web/src/i18n/locales/zh-Hant.ts | 2 -- 11 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/groups/settings.py b/src/langbot/pkg/api/http/controller/groups/settings.py index 72a4826a0..e69e3d704 100644 --- a/src/langbot/pkg/api/http/controller/groups/settings.py +++ b/src/langbot/pkg/api/http/controller/groups/settings.py @@ -2,12 +2,15 @@ Exposes the "成员操作日志溯源" (member operation traceability) surface: -* ``GET /api/v1/settings/governance`` read level + template table -* ``PUT /api/v1/settings/governance`` change level / retention -* ``GET /api/v1/settings/operation-logs`` page through records -* ``GET /api/v1/settings/operation-logs/filters`` available filter values -* ``POST /api/v1/settings/operation-logs/prune`` enforce retention now -* ``DEL /api/v1/settings/operation-logs`` clear records +* ``GET /api/v1/settings/governance`` read level + template table +* ``PUT /api/v1/settings/governance`` change level / retention +* ``GET /api/v1/settings/operation-logs`` page through records +* ``GET /api/v1/settings/operation-logs/filters`` available filter values +* ``GET /api/v1/settings/operation-logs/export`` download the filtered CSV + +Operation records are append-only and there is no route to delete them on +demand: retention is the only deletion path, and it runs automatically when +the governance settings change and from the maintenance loop. Every route requires ``audit.view``, which is granted to the Workspace owner and admin only. Write routes additionally require an owning/admin role so a diff --git a/src/langbot/pkg/api/http/service/settings.py b/src/langbot/pkg/api/http/service/settings.py index 6b56c9820..3b0907851 100644 --- a/src/langbot/pkg/api/http/service/settings.py +++ b/src/langbot/pkg/api/http/service/settings.py @@ -212,12 +212,6 @@ ACTION_RULE_TABLE: typing.Final[tuple[ActionRule, ...]] = ( bucket='audit', resource_type='operation_log', ), - ActionRule( - action='clear_prune', - category='audit', - bucket='write', - resource_type='operation_log', - ), ActionRule( action='settings_update', category='settings', diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index f87b851d7..71812449f 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -1867,12 +1867,19 @@ export class BackendClient extends BaseHttpClient { params.set(key, String(value)); } const suffix = params.toString(); - return `${this.getBaseUrl()}/api/v1/settings/operation-logs/export${ + // A base of "/" means "same origin". Appending the path directly produced + // a protocol-relative "//api/..." URL, which a browser reads as the host + // "api" — the export request then never reached the backend. Resolve the + // base to the current origin first, mirroring the other URL builders here. + const apiBase = + this.instance.defaults.baseURL === '/' || !this.instance.defaults.baseURL + ? window.location.origin + : this.instance.defaults.baseURL.replace(/\/$/, ''); + return `${apiBase}/api/v1/settings/operation-logs/export${ suffix ? `?${suffix}` : '' }`; } - public setPassword( newPassword: string, currentPassword?: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index aea9e7490..75e4e41cd 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -2752,7 +2752,6 @@ const enUS = { redacted: 'Redacted', tamperedBadge: 'Possibly tampered', verifiedBadge: 'Verified', - tamperedCount: '{{count}} records failed verification', integrityFailedCount: '{{count}} hash mismatches', chainFailedCount: '{{count}} broken links', levels: { @@ -2778,7 +2777,6 @@ const enUS = { }, actions: { audit_log_view: 'View operation logs', - clear_prune: 'Prune records by policy', settings_update: 'Update tracing settings', settings_view: 'View tracing settings', member_invite: 'Invite member', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 3713f3927..8981b2852 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -2807,7 +2807,6 @@ const esES = { redacted: 'Oculto', tamperedBadge: 'Posible manipulación', verifiedBadge: 'Verificado', - tamperedCount: '{{count}} registros fallaron la verificación', integrityFailedCount: '{{count}} hashes no coinciden', chainFailedCount: '{{count}} enlaces rotos', levels: { @@ -2833,7 +2832,6 @@ const esES = { }, actions: { audit_log_view: 'Ver registros de operación', - clear_prune: 'Depurar registros según la política', settings_update: 'Actualizar ajustes de trazabilidad', settings_view: 'Ver ajustes de trazabilidad', member_invite: 'Invitar miembro', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 36663dc75..053033ee8 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -2770,7 +2770,6 @@ const jaJP = { redacted: 'マスク済み', tamperedBadge: '改ざんの可能性', verifiedBadge: '検証済み', - tamperedCount: '{{count}} 件が検証に失敗', integrityFailedCount: '{{count}} 件のハッシュ不一致', chainFailedCount: '{{count}} 件のリンク断絶', levels: { @@ -2796,7 +2795,6 @@ const jaJP = { }, actions: { audit_log_view: '操作ログを閲覧', - clear_prune: 'ポリシーで記録を整理', settings_update: 'トレーサビリティ設定を変更', settings_view: 'トレーサビリティ設定を閲覧', member_invite: 'メンバーを招待', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index 67281e234..63448bcfb 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -2777,7 +2777,6 @@ const ruRU = { redacted: 'Скрыто', tamperedBadge: 'Возможна подмена', verifiedBadge: 'Проверено', - tamperedCount: 'Записей с ошибкой проверки: {{count}}', integrityFailedCount: 'Несовпадение хэша: {{count}}', chainFailedCount: 'Разрыв связи: {{count}}', levels: { @@ -2803,7 +2802,6 @@ const ruRU = { }, actions: { audit_log_view: 'Просмотр журнала операций', - clear_prune: 'Очистка записей по политике', settings_update: 'Изменение настроек трассировки', settings_view: 'Просмотр настроек трассировки', member_invite: 'Приглашение участника', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index ca3dcc5f9..debd274e7 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -2704,7 +2704,6 @@ const thTH = { redacted: 'ปิดบังแล้ว', tamperedBadge: 'อาจถูกแก้ไข', verifiedBadge: 'ตรวจสอบผ่าน', - tamperedCount: 'มี {{count}} บันทึกที่ตรวจสอบไม่ผ่าน', integrityFailedCount: 'แฮชไม่ตรงกัน {{count}} รายการ', chainFailedCount: 'ลิงก์ขาด {{count}} รายการ', levels: { @@ -2730,7 +2729,6 @@ const thTH = { }, actions: { audit_log_view: 'ดูบันทึกการดำเนินการ', - clear_prune: 'ล้างบันทึกตามนโยบาย', settings_update: 'แก้ไขการตั้งค่าการติดตาม', settings_view: 'ดูการตั้งค่าการติดตาม', member_invite: 'เชิญสมาชิก', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index a500b6965..e89a11ad2 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -2739,7 +2739,6 @@ const viVN = { redacted: 'Đã ẩn', tamperedBadge: 'Có thể bị sửa', verifiedBadge: 'Đã xác minh', - tamperedCount: '{{count}} bản ghi không vượt qua kiểm tra', integrityFailedCount: '{{count}} mã băm không khớp', chainFailedCount: '{{count}} liên kết bị đứt', levels: { @@ -2765,7 +2764,6 @@ const viVN = { }, actions: { audit_log_view: 'Xem nhật ký thao tác', - clear_prune: 'Dọn bản ghi theo chính sách', settings_update: 'Cập nhật cài đặt truy vết', settings_view: 'Xem cài đặt truy vết', member_invite: 'Mời thành viên', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index a1ab2a8b6..91119b4d4 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -2601,7 +2601,6 @@ const zhHans = { redacted: '已脱敏', tamperedBadge: '可能被篡改', verifiedBadge: '校验通过', - tamperedCount: '{{count}} 条记录校验异常', integrityFailedCount: '{{count}} 条哈希不匹配', chainFailedCount: '{{count}} 条链路断裂', levels: { @@ -2627,7 +2626,6 @@ const zhHans = { }, actions: { audit_log_view: '查看操作日志', - clear_prune: '按策略清理记录', settings_update: '修改溯源设置', settings_view: '查看溯源设置', member_invite: '邀请成员', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 8a62c8a45..a274484b0 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -2602,7 +2602,6 @@ const zhHant = { redacted: '已遮罩', tamperedBadge: '可能被竄改', verifiedBadge: '驗證通過', - tamperedCount: '{{count}} 筆記錄驗證異常', integrityFailedCount: '{{count}} 筆雜湊不符', chainFailedCount: '{{count}} 筆鏈結中斷', levels: { @@ -2628,7 +2627,6 @@ const zhHant = { }, actions: { audit_log_view: '查看操作日誌', - clear_prune: '依策略清理記錄', settings_update: '修改溯源設定', settings_view: '查看溯源設定', member_invite: '邀請成員',