agentworkshop 0.7.11 → 0.7.13

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