agentworkshop 0.7.11 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{BLzCac9K2.js → CcwJ2mrD2.js} +1 -1
  3. package/.output/public/_nuxt/CvYfzHYz.js +1 -0
  4. package/.output/public/_nuxt/{B5CYgFAD.js → D2JyEL28.js} +4 -4
  5. package/.output/public/_nuxt/{CGx-mtWf.js → D8xY-Yjc.js} +1 -1
  6. package/.output/public/_nuxt/DaE5saZg.js +1 -0
  7. package/.output/public/_nuxt/Dsdy4Ua-.js +1 -0
  8. package/.output/public/_nuxt/{B8WV7GzS.js → E88a-JCG.js} +1 -1
  9. package/.output/public/_nuxt/bAFueD6a.js +1 -0
  10. package/.output/public/_nuxt/builds/latest.json +1 -1
  11. package/.output/public/_nuxt/builds/meta/54e22640-4fd0-4fee-9077-31d4d4bb7969.json +1 -0
  12. package/.output/public/_nuxt/dcw.rpqpOYQ8.css +1 -0
  13. package/.output/public/_nuxt/entry.Bv_GCmPP.css +1 -0
  14. package/.output/public/_nuxt/hb_ceMJP.js +1 -0
  15. package/.output/public/_nuxt/logs.BS1sb41K.css +1 -0
  16. package/.output/public/_nuxt/pages.BkE-AfIb.css +1 -0
  17. package/.output/public/_nuxt/permissions.DTRtZeTw.css +1 -0
  18. package/.output/public/_nuxt/plugins.BcSEwwGb.css +1 -0
  19. package/.output/public/_nuxt/tokens.rW1xHxRW.css +1 -0
  20. package/.output/public/_nuxt/workshop.CbymC2LY.css +1 -0
  21. package/.output/server/chunks/_/nitro.mjs +772 -772
  22. package/.output/server/chunks/build/{dcw-DqjHrl5Z.mjs → dcw-B0dtiHFG.mjs} +43 -43
  23. package/.output/server/chunks/build/dcw-B0dtiHFG.mjs.map +1 -0
  24. package/.output/server/chunks/build/{default-YcUHc1Ev.mjs → default-DdV642BY.mjs} +23 -2
  25. package/.output/server/chunks/build/{default-YcUHc1Ev.mjs.map → default-DdV642BY.mjs.map} +1 -1
  26. package/.output/server/chunks/build/{logs-Co854R4y.mjs → logs-Crf6f1IC.mjs} +34 -34
  27. package/.output/server/chunks/build/{logs-Co854R4y.mjs.map → logs-Crf6f1IC.mjs.map} +1 -1
  28. package/.output/server/chunks/build/{pages-DPC1Vp6w.mjs → pages-BmRUbGUi.mjs} +28 -18
  29. package/.output/server/chunks/build/pages-BmRUbGUi.mjs.map +1 -0
  30. package/.output/server/chunks/build/{permissions-Eu9e6hfZ.mjs → permissions-CxEgXic0.mjs} +12 -12
  31. package/.output/server/chunks/build/{permissions-Eu9e6hfZ.mjs.map → permissions-CxEgXic0.mjs.map} +1 -1
  32. package/.output/server/chunks/build/{plugins-D84mvK9G.mjs → plugins-wLIKHcIE.mjs} +17 -17
  33. package/.output/server/chunks/build/plugins-wLIKHcIE.mjs.map +1 -0
  34. package/.output/server/chunks/build/{tokens-CZpnEDVf.mjs → tokens-BKLzcdMg.mjs} +44 -75
  35. package/.output/server/chunks/build/tokens-BKLzcdMg.mjs.map +1 -0
  36. package/.output/server/chunks/build/{workshop-Crsai2Y9.mjs → workshop-CTcYDq2i.mjs} +21 -16
  37. package/.output/server/chunks/build/{workshop-Crsai2Y9.mjs.map → workshop-CTcYDq2i.mjs.map} +1 -1
  38. package/.output/server/chunks/routes/renderer.mjs +1 -1
  39. package/.output/server/chunks/virtual/entry.mjs +12 -10
  40. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  41. package/.output/server/package.json +1 -1
  42. package/app/assets/css/main.css +161 -0
  43. package/app/components/AwChart.vue +12 -2
  44. package/app/composables/useRouteMeta.ts +8 -1
  45. package/app/pages/dcw/index.vue +1079 -1079
  46. package/app/pages/index.vue +7 -2
  47. package/app/pages/logs/index.vue +621 -621
  48. package/app/pages/permissions/index.vue +330 -327
  49. package/app/pages/plugins/index.vue +6 -3
  50. package/app/pages/tokens.vue +9 -13
  51. package/app/pages/workshop/index.vue +3 -1
  52. package/i18n/locales/en.ts +1944 -1943
  53. package/i18n/locales/zh-CN.ts +1943 -1942
  54. package/package.json +1 -1
  55. package/scripts/_dbg-kicker-check.mjs +11 -0
  56. package/scripts/_dbg-perms-e2e.mjs +3 -3
  57. package/scripts/_dbg-plugins-e2e.mjs +1 -1
  58. package/scripts/_dbg-swatch-check.mjs +10 -0
  59. package/scripts/_dbg-ui-shots.mjs +28 -0
  60. package/server/data/dcw-rollback.json +6 -6
  61. package/server/data/dcws.json +345 -345
  62. package/.output/public/_nuxt/Bckshc9V.js +0 -1
  63. package/.output/public/_nuxt/D7jiode2.js +0 -1
  64. package/.output/public/_nuxt/DEaY_uvR.js +0 -1
  65. package/.output/public/_nuxt/builds/meta/88479a5b-ce6a-400f-9737-1cc9f54b1fe5.json +0 -1
  66. package/.output/public/_nuxt/d_Sz9KGm.js +0 -1
  67. package/.output/public/_nuxt/dcw.DcDjwvqv.css +0 -1
  68. package/.output/public/_nuxt/entry.B03HBJGC.css +0 -1
  69. package/.output/public/_nuxt/logs.BO-BSyjm.css +0 -1
  70. package/.output/public/_nuxt/pages.CytEOfUs.css +0 -1
  71. package/.output/public/_nuxt/permissions.CwZo6t1V.css +0 -1
  72. package/.output/public/_nuxt/plugins.Clsky_kl.css +0 -1
  73. package/.output/public/_nuxt/tJuVvBSl.js +0 -1
  74. package/.output/public/_nuxt/tokens.BPLe9ioQ.css +0 -1
  75. package/.output/public/_nuxt/workshop.BIdoGfoC.css +0 -1
  76. package/.output/server/chunks/build/dcw-DqjHrl5Z.mjs.map +0 -1
  77. package/.output/server/chunks/build/pages-DPC1Vp6w.mjs.map +0 -1
  78. package/.output/server/chunks/build/plugins-D84mvK9G.mjs.map +0 -1
  79. package/.output/server/chunks/build/tokens-CZpnEDVf.mjs.map +0 -1
@@ -1,621 +1,621 @@
1
- <template>
2
- <div class="page">
3
- <!-- 页头:标题 + 手动记录入口 -->
4
- <header class="head">
5
- <div>
6
- <h1 class="h1">
7
- <span class="i-tabler-list-details" />
8
- {{ $t('logs.title') }}
9
- </h1>
10
- <p class="sub">
11
- {{ $t('logs.sub') }}
12
- </p>
13
- </div>
14
- <div class="head-actions">
15
- <span
16
- class="live-dot"
17
- :title="$t('logs.liveHint')"
18
- /><span class="live-txt mono">{{ $t('logs.live') }} {{ opsLog.recent.length }}</span>
19
- <button
20
- class="pill-btn"
21
- @click="openManual"
22
- >
23
- <span class="i-tabler-plus" />
24
- {{ $t('logs.manual') }}
25
- </button>
26
- </div>
27
- </header>
28
-
29
- <!-- 维度筛选:产线 → 产品 → Recipe 级联 + 来源/分类/关键词 -->
30
- <section class="filter-card">
31
- <label class="flt">
32
- <span>{{ $t('logs.fLine') }}</span>
33
- <select
34
- v-model="q.lineId"
35
- class="inp-sel"
36
- @change="q.productId = ''; q.recipeId = ''"
37
- >
38
- <option value="">
39
- {{ $t('logs.all') }}
40
- </option>
41
- <option
42
- v-for="l in dcw.lines"
43
- :key="l.id"
44
- :value="l.id"
45
- >
46
- {{ l.name }}
47
- </option>
48
- </select>
49
- </label>
50
- <label class="flt">
51
- <span>{{ $t('logs.fProduct') }}</span>
52
- <select
53
- v-model="q.productId"
54
- class="inp-sel"
55
- @change="q.recipeId = ''"
56
- >
57
- <option value="">
58
- {{ $t('logs.all') }}
59
- </option>
60
- <option
61
- v-for="p in productsOfLine"
62
- :key="p.id"
63
- :value="p.id"
64
- >
65
- {{ p.name }}
66
- </option>
67
- </select>
68
- </label>
69
- <label class="flt">
70
- <span>Recipe</span>
71
- <select
72
- v-model="q.recipeId"
73
- class="inp-sel"
74
- >
75
- <option value="">
76
- {{ $t('logs.all') }}
77
- </option>
78
- <option
79
- v-for="r in recipesOfScope"
80
- :key="r.id"
81
- :value="r.id"
82
- >
83
- {{ r.name }}
84
- </option>
85
- </select>
86
- </label>
87
- <label class="flt">
88
- <span>{{ $t('logs.fSource') }}</span>
89
- <select
90
- v-model="q.actorKind"
91
- class="inp-sel"
92
- >
93
- <option value="">
94
- {{ $t('logs.all') }}
95
- </option>
96
- <option value="user">
97
- {{ $t('logs.src.user') }}
98
- </option>
99
- <option value="agent">
100
- Agent
101
- </option>
102
- <option value="system">
103
- {{ $t('logs.src.system') }}
104
- </option>
105
- </select>
106
- </label>
107
- <label class="flt">
108
- <span>{{ $t('logs.fKind') }}</span>
109
- <select
110
- v-model="q.kind"
111
- class="inp-sel"
112
- >
113
- <option value="">
114
- {{ $t('logs.all') }}
115
- </option>
116
- <option
117
- v-for="k in KINDS"
118
- :key="k"
119
- :value="k"
120
- >
121
- {{ $t(`logs.kind.${k}`) }}
122
- </option>
123
- </select>
124
- </label>
125
- <label class="flt flt-grow">
126
- <span>{{ $t('logs.fKeyword') }}</span>
127
- <input
128
- v-model="q.text"
129
- class="inp-sel"
130
- type="search"
131
- :placeholder="$t('logs.fKeywordPh')"
132
- @keydown.enter="doQuery"
133
- >
134
- </label>
135
- <button
136
- class="pill-btn"
137
- :disabled="opsLog.loading.list"
138
- @click="doQuery"
139
- >
140
- <span class="i-tabler-search" />
141
- {{ $t('logs.query') }}
142
- </button>
143
- <button
144
- v-if="hasFilter"
145
- class="mini-btn"
146
- @click="resetFilters"
147
- >
148
- {{ $t('logs.reset') }}
149
- </button>
150
- </section>
151
-
152
- <!-- 结果表:时间/来源/操作者/分类/摘要/归属维度/详情 -->
153
- <section class="table-card">
154
- <p
155
- v-if="opsLog.error.list"
156
- class="err"
157
- >
158
- {{ opsLog.error.list }}
159
- </p>
160
- <table class="log-table">
161
- <thead>
162
- <tr>
163
- <th class="th-time">
164
- {{ $t('logs.thTime') }}
165
- </th>
166
- <th class="th-src">
167
- {{ $t('logs.fSource') }}
168
- </th>
169
- <th>{{ $t('logs.thActor') }}</th>
170
- <th class="th-kind">
171
- {{ $t('logs.fKind') }}
172
- </th>
173
- <th>{{ $t('logs.thSummary') }}</th>
174
- <th class="th-scope">
175
- {{ $t('logs.thScope') }}
176
- </th>
177
- <th class="th-detail">
178
- {{ $t('logs.thDetail') }}
179
- </th>
180
- </tr>
181
- </thead>
182
- <tbody>
183
- <tr
184
- v-for="row in opsLog.results"
185
- :key="`${row.id}-${row.at}`"
186
- >
187
- <td class="mono dim">
188
- {{ fmtTime(row.at) }}
189
- </td>
190
- <td>
191
- <span
192
- class="src-badge"
193
- :class="row.actorKind"
194
- >{{ srcLabel(row.actorKind) }}</span>
195
- </td>
196
- <td class="actor">
197
- {{ row.actorName || row.actor || '—' }}
198
- </td>
199
- <td>
200
- <span
201
- class="kind-chip"
202
- :class="row.kind"
203
- >{{ kindLabel(row.kind) }}</span>
204
- </td>
205
- <td class="summary">
206
- {{ row.summary }}
207
- <small class="mono dim action">{{ row.action }}</small>
208
- </td>
209
- <td class="scope">
210
- <span
211
- v-if="lineName(row.lineId)"
212
- class="scope-chip"
213
- :title="$t('logs.fLine')"
214
- >{{ lineName(row.lineId) }}</span>
215
- <span
216
- v-if="productName(row.productId)"
217
- class="scope-chip"
218
- :title="$t('logs.fProduct')"
219
- >{{ productName(row.productId) }}</span>
220
- <span
221
- v-if="recipeName(row.recipeId)"
222
- class="scope-chip"
223
- :title="'Recipe'"
224
- >{{ recipeName(row.recipeId) }}</span>
225
- <span
226
- v-if="!row.lineId && !row.productId && !row.recipeId"
227
- class="dim"
228
- >—</span>
229
- </td>
230
- <td class="detail-cell">
231
- <button
232
- v-if="row.detailJson && row.detailJson !== '{}'"
233
- class="mini-btn"
234
- @click="toggle(row)"
235
- >
236
- {{ expanded === row ? $t('logs.fold') : $t('logs.expand') }}
237
- </button>
238
- <span
239
- v-else
240
- class="dim"
241
- >—</span>
242
- </td>
243
- </tr>
244
- <tr>
245
- <td
246
- v-if="opsLog.results.length === 0"
247
- colspan="7"
248
- class="empty"
249
- >
250
- <template v-if="opsLog.loading.list">
251
- {{ $t('logs.loading') }}
252
- </template>
253
- <template v-else>
254
- {{ $t('logs.empty') }}
255
- </template>
256
- </td>
257
- </tr>
258
- </tbody>
259
- </table>
260
- <pre
261
- v-if="expanded"
262
- class="detail-box mono"
263
- >{{ pretty(expanded.detailJson) }}</pre>
264
- </section>
265
-
266
- <!-- 人工记录弹窗 -->
267
- <Teleport to="body">
268
- <div
269
- v-if="manualOpen"
270
- class="modal-mask"
271
- @click.self="manualOpen = false"
272
- >
273
- <div class="modal">
274
- <h3 class="m-title">
275
- {{ $t('logs.manual') }}
276
- </h3>
277
- <label class="m-f">
278
- <span>{{ $t('logs.mSummary') }} *</span>
279
- <textarea
280
- v-model="manual.summary"
281
- rows="3"
282
- :placeholder="$t('logs.mSummaryPh')"
283
- />
284
- </label>
285
- <div class="m-grid">
286
- <label class="m-f">
287
- <span>{{ $t('logs.fLine') }}</span>
288
- <select
289
- v-model="manual.lineId"
290
- class="inp-sel"
291
- @change="manual.productId = ''; manual.recipeId = ''"
292
- >
293
- <option value="">
294
- {{ $t('logs.all') }}
295
- </option>
296
- <option
297
- v-for="l in dcw.lines"
298
- :key="l.id"
299
- :value="l.id"
300
- >
301
- {{ l.name }}
302
- </option>
303
- </select>
304
- </label>
305
- <label class="m-f">
306
- <span>{{ $t('logs.fProduct') }}</span>
307
- <select
308
- v-model="manual.productId"
309
- class="inp-sel"
310
- @change="manual.recipeId = ''"
311
- >
312
- <option value="">
313
- {{ $t('logs.all') }}
314
- </option>
315
- <option
316
- v-for="p in productsOfLine"
317
- :key="p.id"
318
- :value="p.id"
319
- >
320
- {{ p.name }}
321
- </option>
322
- </select>
323
- </label>
324
- <label class="m-f">
325
- <span>Recipe</span>
326
- <select
327
- v-model="manual.recipeId"
328
- class="inp-sel"
329
- >
330
- <option value="">
331
- {{ $t('logs.all') }}
332
- </option>
333
- <option
334
- v-for="r in recipesOfScope"
335
- :key="r.id"
336
- :value="r.id"
337
- >
338
- {{ r.name }}
339
- </option>
340
- </select>
341
- </label>
342
- </div>
343
- <p
344
- v-if="opsLog.error.post"
345
- class="err"
346
- >
347
- {{ opsLog.error.post }}
348
- </p>
349
- <div class="m-actions">
350
- <button
351
- class="ghost-btn"
352
- @click="manualOpen = false"
353
- >
354
- {{ $t('common.cancel') }}
355
- </button>
356
- <button
357
- class="pill-btn"
358
- :disabled="opsLog.loading.posting || !manual.summary.trim()"
359
- @click="submitManual"
360
- >
361
- {{ $t('logs.mSubmit') }}
362
- </button>
363
- </div>
364
- </div>
365
- </div>
366
- </Teleport>
367
- </div>
368
- </template>
369
-
370
- <script setup lang="ts">
371
- import { computed, onMounted, reactive, ref } from 'vue'
372
- import { message } from 'ant-design-vue'
373
- import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
374
- import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
375
- import { useOpsLog, type OpsLogRow } from '@/app/composables/workshop/useOpsLog'
376
-
377
- const tt = (k: string) => useNuxtApp().$i18n.t(k) as string
378
-
379
- const dcw = useDcwStream()
380
- const ws = useWorkshopWs()
381
- const opsLog = useOpsLog()
382
-
383
- const KINDS = ['write', 'manual', 'alarm', 'line', 'recipe', 'rollback', 'daq', 'system'] as const
384
-
385
- const q = reactive({ lineId: '', productId: '', recipeId: '', actorKind: '', kind: '', text: '' })
386
- const expanded = ref<OpsLogRow | null>(null)
387
- const manualOpen = ref(false)
388
- const manual = reactive({ summary: '', lineId: '', productId: '', recipeId: '' })
389
-
390
- const hasFilter = computed(() => !!(q.lineId || q.productId || q.recipeId || q.actorKind || q.kind || q.text.trim()))
391
- const productsOfLine = computed(() => dcw.products.filter(p => !q.lineId || p.lineId === q.lineId))
392
- const recipesOfScope = computed(() => dcw.recipes.filter(r =>
393
- (!q.lineId || r.lineId === q.lineId) && (!q.productId || r.productId === q.productId)))
394
-
395
- function doQuery(): void {
396
- void opsLog.fetchLogs({
397
- lineId: q.lineId,
398
- productId: q.productId,
399
- recipeId: q.recipeId,
400
- actorKind: q.actorKind,
401
- kind: q.kind,
402
- q: q.text.trim(),
403
- limit: 300,
404
- })
405
- expanded.value = null
406
- }
407
-
408
- function resetFilters(): void {
409
- q.lineId = ''
410
- q.productId = ''
411
- q.recipeId = ''
412
- q.actorKind = ''
413
- q.kind = ''
414
- q.text = ''
415
- doQuery()
416
- }
417
-
418
- function openManual(): void {
419
- manual.summary = ''
420
- manual.lineId = q.lineId
421
- manual.productId = q.productId
422
- manual.recipeId = q.recipeId
423
- manualOpen.value = true
424
- }
425
-
426
- async function submitManual(): Promise<void> {
427
- try {
428
- await opsLog.postManual({
429
- summary: manual.summary.trim(),
430
- lineId: manual.lineId,
431
- productId: manual.productId,
432
- recipeId: manual.recipeId,
433
- })
434
- message.success(tt('logs.mOk'))
435
- manualOpen.value = false
436
- doQuery()
437
- }
438
- catch (err) {
439
- message.error(apiErrorMessage(err))
440
- }
441
- }
442
-
443
- function toggle(row: OpsLogRow): void {
444
- expanded.value = expanded.value === row ? null : row
445
- }
446
-
447
- function pretty(json: string): string {
448
- try {
449
- return JSON.stringify(JSON.parse(json), null, 2)
450
- }
451
- catch {
452
- return json
453
- }
454
- }
455
-
456
- function fmtTime(at: string): string {
457
- const d = new Date(at)
458
- if (!Number.isFinite(d.getTime())) return at
459
- const p = (n: number) => String(n).padStart(2, '0')
460
- return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
461
- }
462
-
463
- function srcLabel(kind: string): string {
464
- return kind === 'agent' ? 'Agent' : kind === 'user' ? tt('logs.src.user') : tt('logs.src.system')
465
- }
466
-
467
- function kindLabel(kind: string): string {
468
- return kind ? tt(`logs.kind.${kind}`) : '—'
469
- }
470
-
471
- function lineName(id: string): string {
472
- return dcw.lines.find(l => l.id === id)?.name ?? ''
473
- }
474
-
475
- function productName(id: string): string {
476
- return dcw.products.find(p => p.id === id)?.name ?? ''
477
- }
478
-
479
- function recipeName(id: string): string {
480
- return dcw.recipes.find(r => r.id === id)?.name ?? ''
481
- }
482
-
483
- onMounted(() => {
484
- ws.ensureConnected()
485
- opsLog.ensureLive()
486
- void dcw.load()
487
- doQuery()
488
- })
489
- </script>
490
-
491
- <script lang="ts">
492
- export default { name: 'OpsLogsPage' }
493
- </script>
494
-
495
- <style scoped>
496
- .page { display: flex; flex-direction: column; gap: 12px; }
497
- .head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
498
- .h1 { display: flex; gap: 8px; align-items: center; margin: 0; font-size: 20px; font-weight: 700; color: var(--ink); }
499
- .sub { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-faint); }
500
- .head-actions { display: flex; gap: 10px; align-items: center; }
501
- .live-dot {
502
- width: 8px; height: 8px; border-radius: 50%;
503
- background: var(--tone-success-dot);
504
- box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone-success-dot) 45%, transparent);
505
- animation: livePulse 2s ease-out infinite;
506
- }
507
- @keyframes livePulse {
508
- 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone-success-dot) 45%, transparent); }
509
- 70% { box-shadow: 0 0 0 8px transparent; }
510
- 100% { box-shadow: 0 0 0 0 transparent; }
511
- }
512
- .live-txt { font-size: 11px; color: var(--ink-faint); }
513
-
514
- .filter-card {
515
- display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end;
516
- padding: 10px 14px;
517
- background: var(--surface-glass);
518
- border: 1px solid var(--glass-line);
519
- border-radius: 10px;
520
- backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
521
- -webkit-backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
522
- }
523
- .flt { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--ink-faint); }
524
- .flt-grow { flex: 1 1 180px; }
525
- .inp-sel {
526
- min-width: 0; padding: 6px 9px; font-size: 12.5px; color: var(--ink);
527
- background: var(--frost-bg);
528
- border: 1px solid var(--glass-line); border-radius: 7px; outline: none;
529
- }
530
- .inp-sel:focus { border-color: color-mix(in srgb, var(--tone-info-dot) 55%, transparent); }
531
- .mini-btn {
532
- padding: 6px 11px; font-size: 12px; color: var(--ink-soft);
533
- background: var(--frost-bg); border: 1px solid var(--glass-line); border-radius: 7px;
534
- cursor: pointer;
535
- }
536
- .mini-btn:hover { border-color: color-mix(in srgb, var(--tone-info-dot) 45%, transparent); }
537
-
538
- .table-card {
539
- background: var(--surface-glass);
540
- border: 1px solid var(--glass-line);
541
- border-radius: 10px;
542
- overflow: hidden;
543
- backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
544
- -webkit-backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
545
- }
546
- .err { margin: 0; padding: 8px 14px; font-size: 12px; color: var(--tone-danger-dot); }
547
- .log-table { width: 100%; font-size: 12.5px; border-collapse: collapse; }
548
- .log-table th {
549
- position: sticky; top: 0; z-index: 1;
550
- padding: 8px 10px; text-align: left; font-weight: 600; color: var(--ink-soft);
551
- background: var(--frost-bg);
552
- border-bottom: 1px solid var(--glass-line);
553
- }
554
- .log-table td {
555
- max-width: 380px; padding: 7px 10px; color: var(--ink-soft);
556
- border-bottom: 1px solid color-mix(in srgb, var(--glass-line) 55%, transparent);
557
- vertical-align: top;
558
- }
559
- .th-time, .th-src, .th-kind { white-space: nowrap; }
560
- .th-detail { width: 72px; }
561
- .actor { white-space: nowrap; }
562
- .summary .action { display: block; font-size: 10px; color: var(--ink-faint); }
563
- .scope { display: flex; flex-wrap: wrap; gap: 4px; max-width: 220px; }
564
- .scope-chip {
565
- padding: 1px 7px; font-size: 10.5px; color: var(--ink-soft);
566
- background: var(--frost-bg); border-radius: 99px; white-space: nowrap;
567
- }
568
- .detail-cell { white-space: nowrap; }
569
- .empty { padding: 22px 0 !important; color: var(--ink-faint); text-align: center; }
570
- .detail-box {
571
- max-height: 260px; margin: 0; padding: 10px 14px; overflow: auto;
572
- font-size: 11.5px; color: var(--ink-soft);
573
- background: var(--frost-bg); border-top: 1px solid var(--glass-line);
574
- }
575
- .src-badge {
576
- display: inline-block; padding: 1px 8px; font-size: 10.5px;
577
- border: 1px solid var(--glass-line); border-radius: 99px; color: var(--ink-faint);
578
- }
579
- .src-badge.agent { color: var(--tone-info-dot); border-color: color-mix(in srgb, var(--tone-info-dot) 40%, transparent); }
580
- .src-badge.user { color: var(--tone-success-dot); border-color: color-mix(in srgb, var(--tone-success-dot) 40%, transparent); }
581
- .kind-chip {
582
- display: inline-block; padding: 1px 7px; font-size: 10.5px;
583
- color: var(--ink-soft); background: var(--frost-bg); border-radius: 5px; white-space: nowrap;
584
- }
585
- .kind-chip.alarm { color: var(--tone-danger-dot); background: var(--tone-danger-bg); }
586
- .kind-chip.write { color: var(--tone-info-dot); }
587
- .kind-chip.rollback { color: var(--tone-warning-dot); }
588
- .kind-chip.manual { color: var(--tone-success-dot); }
589
-
590
- .m-title { margin: 0 0 10px; font-size: 15px; color: var(--ink); }
591
- .modal-mask {
592
- position: fixed; inset: 0; z-index: 60;
593
- display: flex; align-items: center; justify-content: center;
594
- padding: 20px;
595
- background: color-mix(in srgb, var(--ink) 32%, transparent);
596
- backdrop-filter: blur(3px);
597
- -webkit-backdrop-filter: blur(3px);
598
- }
599
- .modal {
600
- width: min(560px, 94vw);
601
- padding: 16px 18px;
602
- background: var(--surface-glass);
603
- border: 1px solid var(--glass-line);
604
- border-radius: 12px;
605
- backdrop-filter: blur(calc(var(--aurora-blur) * 1.4)) saturate(1.2);
606
- -webkit-backdrop-filter: blur(calc(var(--aurora-blur) * 1.4)) saturate(1.2);
607
- }
608
- .m-f { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--ink-faint); }
609
- .m-f textarea, .m-f .inp-sel {
610
- padding: 7px 9px; font-size: 12.5px; color: var(--ink);
611
- background: var(--frost-bg); border: 1px solid var(--glass-line); border-radius: 7px; outline: none;
612
- font-family: inherit; resize: vertical;
613
- }
614
- .m-f textarea:focus, .m-f .inp-sel:focus { border-color: color-mix(in srgb, var(--tone-info-dot) 55%, transparent); }
615
- .m-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
616
- .m-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
617
- .ghost-btn {
618
- padding: 7px 14px; font-size: 12.5px; color: var(--ink-soft);
619
- background: transparent; border: 1px solid var(--glass-line); border-radius: 8px; cursor: pointer;
620
- }
621
- </style>
1
+ <template>
2
+ <div class="page">
3
+ <!-- 页头:标题 + 手动记录入口 -->
4
+ <header class="head">
5
+ <div>
6
+ <p class="aw-kicker">agentworkshop / audit log</p>
7
+ <h1 class="h1">
8
+ {{ $t('logs.title') }}
9
+ </h1>
10
+ <p class="sub">
11
+ {{ $t('logs.sub') }}
12
+ </p>
13
+ </div>
14
+ <div class="head-actions">
15
+ <span
16
+ class="live-dot"
17
+ :title="$t('logs.liveHint')"
18
+ /><span class="live-txt mono">{{ $t('logs.live') }} {{ opsLog.recent.length }}</span>
19
+ <button
20
+ class="pill-btn"
21
+ @click="openManual"
22
+ >
23
+ <span class="i-tabler-plus" />
24
+ {{ $t('logs.manual') }}
25
+ </button>
26
+ </div>
27
+ </header>
28
+
29
+ <!-- 维度筛选:产线 → 产品 → Recipe 级联 + 来源/分类/关键词 -->
30
+ <section class="filter-card">
31
+ <label class="flt">
32
+ <span>{{ $t('logs.fLine') }}</span>
33
+ <select
34
+ v-model="q.lineId"
35
+ class="inp-sel"
36
+ @change="q.productId = ''; q.recipeId = ''"
37
+ >
38
+ <option value="">
39
+ {{ $t('logs.all') }}
40
+ </option>
41
+ <option
42
+ v-for="l in dcw.lines"
43
+ :key="l.id"
44
+ :value="l.id"
45
+ >
46
+ {{ l.name }}
47
+ </option>
48
+ </select>
49
+ </label>
50
+ <label class="flt">
51
+ <span>{{ $t('logs.fProduct') }}</span>
52
+ <select
53
+ v-model="q.productId"
54
+ class="inp-sel"
55
+ @change="q.recipeId = ''"
56
+ >
57
+ <option value="">
58
+ {{ $t('logs.all') }}
59
+ </option>
60
+ <option
61
+ v-for="p in productsOfLine"
62
+ :key="p.id"
63
+ :value="p.id"
64
+ >
65
+ {{ p.name }}
66
+ </option>
67
+ </select>
68
+ </label>
69
+ <label class="flt">
70
+ <span>Recipe</span>
71
+ <select
72
+ v-model="q.recipeId"
73
+ class="inp-sel"
74
+ >
75
+ <option value="">
76
+ {{ $t('logs.all') }}
77
+ </option>
78
+ <option
79
+ v-for="r in recipesOfScope"
80
+ :key="r.id"
81
+ :value="r.id"
82
+ >
83
+ {{ r.name }}
84
+ </option>
85
+ </select>
86
+ </label>
87
+ <label class="flt">
88
+ <span>{{ $t('logs.fSource') }}</span>
89
+ <select
90
+ v-model="q.actorKind"
91
+ class="inp-sel"
92
+ >
93
+ <option value="">
94
+ {{ $t('logs.all') }}
95
+ </option>
96
+ <option value="user">
97
+ {{ $t('logs.src.user') }}
98
+ </option>
99
+ <option value="agent">
100
+ Agent
101
+ </option>
102
+ <option value="system">
103
+ {{ $t('logs.src.system') }}
104
+ </option>
105
+ </select>
106
+ </label>
107
+ <label class="flt">
108
+ <span>{{ $t('logs.fKind') }}</span>
109
+ <select
110
+ v-model="q.kind"
111
+ class="inp-sel"
112
+ >
113
+ <option value="">
114
+ {{ $t('logs.all') }}
115
+ </option>
116
+ <option
117
+ v-for="k in KINDS"
118
+ :key="k"
119
+ :value="k"
120
+ >
121
+ {{ $t(`logs.kind.${k}`) }}
122
+ </option>
123
+ </select>
124
+ </label>
125
+ <label class="flt flt-grow">
126
+ <span>{{ $t('logs.fKeyword') }}</span>
127
+ <input
128
+ v-model="q.text"
129
+ class="inp-sel"
130
+ type="search"
131
+ :placeholder="$t('logs.fKeywordPh')"
132
+ @keydown.enter="doQuery"
133
+ >
134
+ </label>
135
+ <button
136
+ class="pill-btn"
137
+ :disabled="opsLog.loading.list"
138
+ @click="doQuery"
139
+ >
140
+ <span class="i-tabler-search" />
141
+ {{ $t('logs.query') }}
142
+ </button>
143
+ <button
144
+ v-if="hasFilter"
145
+ class="mini-btn"
146
+ @click="resetFilters"
147
+ >
148
+ {{ $t('logs.reset') }}
149
+ </button>
150
+ </section>
151
+
152
+ <!-- 结果表:时间/来源/操作者/分类/摘要/归属维度/详情 -->
153
+ <section class="table-card">
154
+ <p
155
+ v-if="opsLog.error.list"
156
+ class="err"
157
+ >
158
+ {{ opsLog.error.list }}
159
+ </p>
160
+ <table class="log-table">
161
+ <thead>
162
+ <tr>
163
+ <th class="th-time">
164
+ {{ $t('logs.thTime') }}
165
+ </th>
166
+ <th class="th-src">
167
+ {{ $t('logs.fSource') }}
168
+ </th>
169
+ <th>{{ $t('logs.thActor') }}</th>
170
+ <th class="th-kind">
171
+ {{ $t('logs.fKind') }}
172
+ </th>
173
+ <th>{{ $t('logs.thSummary') }}</th>
174
+ <th class="th-scope">
175
+ {{ $t('logs.thScope') }}
176
+ </th>
177
+ <th class="th-detail">
178
+ {{ $t('logs.thDetail') }}
179
+ </th>
180
+ </tr>
181
+ </thead>
182
+ <tbody>
183
+ <tr
184
+ v-for="row in opsLog.results"
185
+ :key="`${row.id}-${row.at}`"
186
+ >
187
+ <td class="mono dim">
188
+ {{ fmtTime(row.at) }}
189
+ </td>
190
+ <td>
191
+ <span
192
+ class="src-badge"
193
+ :class="row.actorKind"
194
+ >{{ srcLabel(row.actorKind) }}</span>
195
+ </td>
196
+ <td class="actor">
197
+ {{ row.actorName || row.actor || '—' }}
198
+ </td>
199
+ <td>
200
+ <span
201
+ class="kind-chip"
202
+ :class="row.kind"
203
+ >{{ kindLabel(row.kind) }}</span>
204
+ </td>
205
+ <td class="summary">
206
+ {{ row.summary }}
207
+ <small class="mono dim action">{{ row.action }}</small>
208
+ </td>
209
+ <td class="scope">
210
+ <span
211
+ v-if="lineName(row.lineId)"
212
+ class="scope-chip"
213
+ :title="$t('logs.fLine')"
214
+ >{{ lineName(row.lineId) }}</span>
215
+ <span
216
+ v-if="productName(row.productId)"
217
+ class="scope-chip"
218
+ :title="$t('logs.fProduct')"
219
+ >{{ productName(row.productId) }}</span>
220
+ <span
221
+ v-if="recipeName(row.recipeId)"
222
+ class="scope-chip"
223
+ :title="'Recipe'"
224
+ >{{ recipeName(row.recipeId) }}</span>
225
+ <span
226
+ v-if="!row.lineId && !row.productId && !row.recipeId"
227
+ class="dim"
228
+ >—</span>
229
+ </td>
230
+ <td class="detail-cell">
231
+ <button
232
+ v-if="row.detailJson && row.detailJson !== '{}'"
233
+ class="mini-btn"
234
+ @click="toggle(row)"
235
+ >
236
+ {{ expanded === row ? $t('logs.fold') : $t('logs.expand') }}
237
+ </button>
238
+ <span
239
+ v-else
240
+ class="dim"
241
+ >—</span>
242
+ </td>
243
+ </tr>
244
+ <tr>
245
+ <td
246
+ v-if="opsLog.results.length === 0"
247
+ colspan="7"
248
+ class="empty"
249
+ >
250
+ <template v-if="opsLog.loading.list">
251
+ {{ $t('logs.loading') }}
252
+ </template>
253
+ <template v-else>
254
+ {{ $t('logs.empty') }}
255
+ </template>
256
+ </td>
257
+ </tr>
258
+ </tbody>
259
+ </table>
260
+ <pre
261
+ v-if="expanded"
262
+ class="detail-box mono"
263
+ >{{ pretty(expanded.detailJson) }}</pre>
264
+ </section>
265
+
266
+ <!-- 人工记录弹窗 -->
267
+ <Teleport to="body">
268
+ <div
269
+ v-if="manualOpen"
270
+ class="modal-mask"
271
+ @click.self="manualOpen = false"
272
+ >
273
+ <div class="modal">
274
+ <h3 class="m-title">
275
+ {{ $t('logs.manual') }}
276
+ </h3>
277
+ <label class="m-f">
278
+ <span>{{ $t('logs.mSummary') }} *</span>
279
+ <textarea
280
+ v-model="manual.summary"
281
+ rows="3"
282
+ :placeholder="$t('logs.mSummaryPh')"
283
+ />
284
+ </label>
285
+ <div class="m-grid">
286
+ <label class="m-f">
287
+ <span>{{ $t('logs.fLine') }}</span>
288
+ <select
289
+ v-model="manual.lineId"
290
+ class="inp-sel"
291
+ @change="manual.productId = ''; manual.recipeId = ''"
292
+ >
293
+ <option value="">
294
+ {{ $t('logs.all') }}
295
+ </option>
296
+ <option
297
+ v-for="l in dcw.lines"
298
+ :key="l.id"
299
+ :value="l.id"
300
+ >
301
+ {{ l.name }}
302
+ </option>
303
+ </select>
304
+ </label>
305
+ <label class="m-f">
306
+ <span>{{ $t('logs.fProduct') }}</span>
307
+ <select
308
+ v-model="manual.productId"
309
+ class="inp-sel"
310
+ @change="manual.recipeId = ''"
311
+ >
312
+ <option value="">
313
+ {{ $t('logs.all') }}
314
+ </option>
315
+ <option
316
+ v-for="p in productsOfLine"
317
+ :key="p.id"
318
+ :value="p.id"
319
+ >
320
+ {{ p.name }}
321
+ </option>
322
+ </select>
323
+ </label>
324
+ <label class="m-f">
325
+ <span>Recipe</span>
326
+ <select
327
+ v-model="manual.recipeId"
328
+ class="inp-sel"
329
+ >
330
+ <option value="">
331
+ {{ $t('logs.all') }}
332
+ </option>
333
+ <option
334
+ v-for="r in recipesOfScope"
335
+ :key="r.id"
336
+ :value="r.id"
337
+ >
338
+ {{ r.name }}
339
+ </option>
340
+ </select>
341
+ </label>
342
+ </div>
343
+ <p
344
+ v-if="opsLog.error.post"
345
+ class="err"
346
+ >
347
+ {{ opsLog.error.post }}
348
+ </p>
349
+ <div class="m-actions">
350
+ <button
351
+ class="ghost-btn"
352
+ @click="manualOpen = false"
353
+ >
354
+ {{ $t('common.cancel') }}
355
+ </button>
356
+ <button
357
+ class="pill-btn"
358
+ :disabled="opsLog.loading.posting || !manual.summary.trim()"
359
+ @click="submitManual"
360
+ >
361
+ {{ $t('logs.mSubmit') }}
362
+ </button>
363
+ </div>
364
+ </div>
365
+ </div>
366
+ </Teleport>
367
+ </div>
368
+ </template>
369
+
370
+ <script setup lang="ts">
371
+ import { computed, onMounted, reactive, ref } from 'vue'
372
+ import { message } from 'ant-design-vue'
373
+ import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
374
+ import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
375
+ import { useOpsLog, type OpsLogRow } from '@/app/composables/workshop/useOpsLog'
376
+
377
+ const tt = (k: string) => useNuxtApp().$i18n.t(k) as string
378
+
379
+ const dcw = useDcwStream()
380
+ const ws = useWorkshopWs()
381
+ const opsLog = useOpsLog()
382
+
383
+ const KINDS = ['write', 'manual', 'alarm', 'line', 'recipe', 'rollback', 'daq', 'system'] as const
384
+
385
+ const q = reactive({ lineId: '', productId: '', recipeId: '', actorKind: '', kind: '', text: '' })
386
+ const expanded = ref<OpsLogRow | null>(null)
387
+ const manualOpen = ref(false)
388
+ const manual = reactive({ summary: '', lineId: '', productId: '', recipeId: '' })
389
+
390
+ const hasFilter = computed(() => !!(q.lineId || q.productId || q.recipeId || q.actorKind || q.kind || q.text.trim()))
391
+ const productsOfLine = computed(() => dcw.products.filter(p => !q.lineId || p.lineId === q.lineId))
392
+ const recipesOfScope = computed(() => dcw.recipes.filter(r =>
393
+ (!q.lineId || r.lineId === q.lineId) && (!q.productId || r.productId === q.productId)))
394
+
395
+ function doQuery(): void {
396
+ void opsLog.fetchLogs({
397
+ lineId: q.lineId,
398
+ productId: q.productId,
399
+ recipeId: q.recipeId,
400
+ actorKind: q.actorKind,
401
+ kind: q.kind,
402
+ q: q.text.trim(),
403
+ limit: 300,
404
+ })
405
+ expanded.value = null
406
+ }
407
+
408
+ function resetFilters(): void {
409
+ q.lineId = ''
410
+ q.productId = ''
411
+ q.recipeId = ''
412
+ q.actorKind = ''
413
+ q.kind = ''
414
+ q.text = ''
415
+ doQuery()
416
+ }
417
+
418
+ function openManual(): void {
419
+ manual.summary = ''
420
+ manual.lineId = q.lineId
421
+ manual.productId = q.productId
422
+ manual.recipeId = q.recipeId
423
+ manualOpen.value = true
424
+ }
425
+
426
+ async function submitManual(): Promise<void> {
427
+ try {
428
+ await opsLog.postManual({
429
+ summary: manual.summary.trim(),
430
+ lineId: manual.lineId,
431
+ productId: manual.productId,
432
+ recipeId: manual.recipeId,
433
+ })
434
+ message.success(tt('logs.mOk'))
435
+ manualOpen.value = false
436
+ doQuery()
437
+ }
438
+ catch (err) {
439
+ message.error(apiErrorMessage(err))
440
+ }
441
+ }
442
+
443
+ function toggle(row: OpsLogRow): void {
444
+ expanded.value = expanded.value === row ? null : row
445
+ }
446
+
447
+ function pretty(json: string): string {
448
+ try {
449
+ return JSON.stringify(JSON.parse(json), null, 2)
450
+ }
451
+ catch {
452
+ return json
453
+ }
454
+ }
455
+
456
+ function fmtTime(at: string): string {
457
+ const d = new Date(at)
458
+ if (!Number.isFinite(d.getTime())) return at
459
+ const p = (n: number) => String(n).padStart(2, '0')
460
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
461
+ }
462
+
463
+ function srcLabel(kind: string): string {
464
+ return kind === 'agent' ? 'Agent' : kind === 'user' ? tt('logs.src.user') : tt('logs.src.system')
465
+ }
466
+
467
+ function kindLabel(kind: string): string {
468
+ return kind ? tt(`logs.kind.${kind}`) : '—'
469
+ }
470
+
471
+ function lineName(id: string): string {
472
+ return dcw.lines.find(l => l.id === id)?.name ?? ''
473
+ }
474
+
475
+ function productName(id: string): string {
476
+ return dcw.products.find(p => p.id === id)?.name ?? ''
477
+ }
478
+
479
+ function recipeName(id: string): string {
480
+ return dcw.recipes.find(r => r.id === id)?.name ?? ''
481
+ }
482
+
483
+ onMounted(() => {
484
+ ws.ensureConnected()
485
+ opsLog.ensureLive()
486
+ void dcw.load()
487
+ doQuery()
488
+ })
489
+ </script>
490
+
491
+ <script lang="ts">
492
+ export default { name: 'OpsLogsPage' }
493
+ </script>
494
+
495
+ <style scoped>
496
+ .page { display: flex; flex-direction: column; gap: 12px; }
497
+ .head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
498
+ .h1 { display: flex; gap: 8px; align-items: center; margin: 0; font-size: 20px; font-weight: 700; color: var(--ink); }
499
+ .sub { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-faint); }
500
+ .head-actions { display: flex; gap: 10px; align-items: center; }
501
+ .live-dot {
502
+ width: 8px; height: 8px; border-radius: 50%;
503
+ background: var(--tone-success-dot);
504
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone-success-dot) 45%, transparent);
505
+ animation: livePulse 2s ease-out infinite;
506
+ }
507
+ @keyframes livePulse {
508
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone-success-dot) 45%, transparent); }
509
+ 70% { box-shadow: 0 0 0 8px transparent; }
510
+ 100% { box-shadow: 0 0 0 0 transparent; }
511
+ }
512
+ .live-txt { font-size: 11px; color: var(--ink-faint); }
513
+
514
+ .filter-card {
515
+ display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end;
516
+ padding: 10px 14px;
517
+ background: var(--surface-glass);
518
+ border: 1px solid var(--glass-line);
519
+ border-radius: 10px;
520
+ backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
521
+ -webkit-backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
522
+ }
523
+ .flt { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--ink-faint); }
524
+ .flt-grow { flex: 1 1 180px; }
525
+ .inp-sel {
526
+ min-width: 0; padding: 6px 9px; font-size: 12.5px; color: var(--ink);
527
+ background: var(--frost-bg);
528
+ border: 1px solid var(--glass-line); border-radius: 7px; outline: none;
529
+ }
530
+ .inp-sel:focus { border-color: color-mix(in srgb, var(--tone-info-dot) 55%, transparent); }
531
+ .mini-btn {
532
+ padding: 6px 11px; font-size: 12px; color: var(--ink-soft);
533
+ background: var(--frost-bg); border: 1px solid var(--glass-line); border-radius: 7px;
534
+ cursor: pointer;
535
+ }
536
+ .mini-btn:hover { border-color: color-mix(in srgb, var(--tone-info-dot) 45%, transparent); }
537
+
538
+ .table-card {
539
+ background: var(--surface-glass);
540
+ border: 1px solid var(--glass-line);
541
+ border-radius: 10px;
542
+ overflow: hidden;
543
+ backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
544
+ -webkit-backdrop-filter: blur(var(--aurora-blur)) saturate(1.15);
545
+ }
546
+ .err { margin: 0; padding: 8px 14px; font-size: 12px; color: var(--tone-danger-dot); }
547
+ .log-table { width: 100%; font-size: 12.5px; border-collapse: collapse; }
548
+ .log-table th {
549
+ position: sticky; top: 0; z-index: 1;
550
+ padding: 8px 10px; text-align: left; font-weight: 600; color: var(--ink-soft);
551
+ background: var(--frost-bg);
552
+ border-bottom: 1px solid var(--glass-line);
553
+ }
554
+ .log-table td {
555
+ max-width: 380px; padding: 7px 10px; color: var(--ink-soft);
556
+ border-bottom: 1px solid color-mix(in srgb, var(--glass-line) 55%, transparent);
557
+ vertical-align: top;
558
+ }
559
+ .th-time, .th-src, .th-kind { white-space: nowrap; }
560
+ .th-detail { width: 72px; }
561
+ .actor { white-space: nowrap; }
562
+ .summary .action { display: block; font-size: 10px; color: var(--ink-faint); }
563
+ .scope { display: flex; flex-wrap: wrap; gap: 4px; max-width: 220px; }
564
+ .scope-chip {
565
+ padding: 1px 7px; font-size: 10.5px; color: var(--ink-soft);
566
+ background: var(--frost-bg); border-radius: 99px; white-space: nowrap;
567
+ }
568
+ .detail-cell { white-space: nowrap; }
569
+ .empty { padding: 22px 0 !important; color: var(--ink-faint); text-align: center; }
570
+ .detail-box {
571
+ max-height: 260px; margin: 0; padding: 10px 14px; overflow: auto;
572
+ font-size: 11.5px; color: var(--ink-soft);
573
+ background: var(--frost-bg); border-top: 1px solid var(--glass-line);
574
+ }
575
+ .src-badge {
576
+ display: inline-block; padding: 1px 8px; font-size: 10.5px;
577
+ border: 1px solid var(--glass-line); border-radius: 99px; color: var(--ink-faint);
578
+ }
579
+ .src-badge.agent { color: var(--tone-info-dot); border-color: color-mix(in srgb, var(--tone-info-dot) 40%, transparent); }
580
+ .src-badge.user { color: var(--tone-success-dot); border-color: color-mix(in srgb, var(--tone-success-dot) 40%, transparent); }
581
+ .kind-chip {
582
+ display: inline-block; padding: 1px 7px; font-size: 10.5px;
583
+ color: var(--ink-soft); background: var(--frost-bg); border-radius: 5px; white-space: nowrap;
584
+ }
585
+ .kind-chip.alarm { color: var(--tone-danger-dot); background: var(--tone-danger-bg); }
586
+ .kind-chip.write { color: var(--tone-info-dot); }
587
+ .kind-chip.rollback { color: var(--tone-warning-dot); }
588
+ .kind-chip.manual { color: var(--tone-success-dot); }
589
+
590
+ .m-title { margin: 0 0 10px; font-size: 15px; color: var(--ink); }
591
+ .modal-mask {
592
+ position: fixed; inset: 0; z-index: 60;
593
+ display: flex; align-items: center; justify-content: center;
594
+ padding: 20px;
595
+ background: color-mix(in srgb, var(--ink) 32%, transparent);
596
+ backdrop-filter: blur(3px);
597
+ -webkit-backdrop-filter: blur(3px);
598
+ }
599
+ .modal {
600
+ width: min(560px, 94vw);
601
+ padding: 16px 18px;
602
+ background: var(--surface-glass);
603
+ border: 1px solid var(--glass-line);
604
+ border-radius: 12px;
605
+ backdrop-filter: blur(calc(var(--aurora-blur) * 1.4)) saturate(1.2);
606
+ -webkit-backdrop-filter: blur(calc(var(--aurora-blur) * 1.4)) saturate(1.2);
607
+ }
608
+ .m-f { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--ink-faint); }
609
+ .m-f textarea, .m-f .inp-sel {
610
+ padding: 7px 9px; font-size: 12.5px; color: var(--ink);
611
+ background: var(--frost-bg); border: 1px solid var(--glass-line); border-radius: 7px; outline: none;
612
+ font-family: inherit; resize: vertical;
613
+ }
614
+ .m-f textarea:focus, .m-f .inp-sel:focus { border-color: color-mix(in srgb, var(--tone-info-dot) 55%, transparent); }
615
+ .m-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
616
+ .m-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
617
+ .ghost-btn {
618
+ padding: 7px 14px; font-size: 12.5px; color: var(--ink-soft);
619
+ background: transparent; border: 1px solid var(--glass-line); border-radius: 8px; cursor: pointer;
620
+ }
621
+ </style>