pinokiod 7.3.0 → 7.3.3
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.
- package/kernel/api/github/index.js +444 -0
- package/kernel/api/index.js +199 -11
- package/kernel/api/process/index.js +124 -44
- package/kernel/api/shell_run_template.js +273 -0
- package/kernel/api/uri/index.js +51 -0
- package/kernel/bin/{conda-python.js → conda-pins.js} +23 -0
- package/kernel/bin/conda.js +15 -5
- package/kernel/bin/git.js +9 -10
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/bin/index.js +5 -2
- package/kernel/bin/zip.js +9 -1
- package/kernel/connect/providers/github/README.md +5 -4
- package/kernel/environment.js +195 -92
- package/kernel/git.js +98 -19
- package/kernel/gitconfig_template +7 -0
- package/kernel/gpu/amd.js +72 -0
- package/kernel/gpu/apple.js +8 -0
- package/kernel/gpu/common.js +12 -0
- package/kernel/gpu/intel.js +47 -0
- package/kernel/gpu/nvidia.js +8 -0
- package/kernel/index.js +11 -1
- package/kernel/managed_skills.js +871 -0
- package/kernel/plugin.js +6 -58
- package/kernel/plugin_sources.js +316 -0
- package/kernel/resource_usage/gpu.js +349 -0
- package/kernel/resource_usage/index.js +322 -0
- package/kernel/resource_usage/macos_footprint.js +197 -0
- package/kernel/resource_usage/preferences.js +92 -0
- package/kernel/resource_usage/process_tree.js +303 -0
- package/kernel/scripts/git/create +4 -4
- package/kernel/scripts/git/fork +7 -8
- package/kernel/shell.js +23 -2
- package/kernel/shells.js +41 -0
- package/kernel/sysinfo.js +62 -9
- package/kernel/util.js +60 -0
- package/package.json +1 -1
- package/server/index.js +984 -156
- package/server/lib/app_log_report.js +543 -0
- package/server/lib/content_validation.js +55 -33
- package/server/lib/launcher_instruction_bootstrap.js +4 -96
- package/server/lib/terminal_session_helpers.js +0 -3
- package/server/public/common.js +77 -31
- package/server/public/create-launcher.js +4 -32
- package/server/public/logs.js +1428 -0
- package/server/public/nav.js +7 -0
- package/server/public/plugin-detail.js +93 -10
- package/server/public/privacy_filter_worker.js +391 -0
- package/server/public/style.css +1104 -154
- package/server/public/task-launcher.js +8 -29
- package/server/public/universal-launcher.css +8 -6
- package/server/public/universal-launcher.js +3 -27
- package/server/routes/apps.js +195 -1
- package/server/views/app.ejs +3041 -717
- package/server/views/autolaunch.ejs +917 -0
- package/server/views/bootstrap.ejs +7 -1
- package/server/views/d.ejs +408 -65
- package/server/views/editor.ejs +85 -19
- package/server/views/index.ejs +661 -111
- package/server/views/init/index.ejs +1 -1
- package/server/views/install.ejs +1 -1
- package/server/views/logs.ejs +164 -86
- package/server/views/net.ejs +7 -1
- package/server/views/partials/d_terminal_column.ejs +2 -2
- package/server/views/partials/d_terminal_options.ejs +0 -8
- package/server/views/partials/fs_status.ejs +47 -0
- package/server/views/partials/home_action_modal.ejs +86 -0
- package/server/views/partials/home_run_menu.ejs +87 -0
- package/server/views/partials/main_sidebar.ejs +2 -0
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/plugin_detail.ejs +19 -4
- package/server/views/plugins.ejs +201 -3
- package/server/views/pre.ejs +1 -1
- package/server/views/pro.ejs +1 -1
- package/server/views/shell.ejs +40 -18
- package/server/views/skills.ejs +506 -0
- package/server/views/terminal.ejs +45 -19
- package/spec/INSTRUCTION_SYNC.md +20 -10
- package/system/plugin/antigravity-cli/antigravity.png +0 -0
- package/system/plugin/antigravity-cli/common.js +155 -0
- package/system/plugin/antigravity-cli/install.js +272 -0
- package/system/plugin/antigravity-cli/pinokio.js +13 -0
- package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
- package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
- package/system/plugin/claude/claude.png +0 -0
- package/system/plugin/claude/pinokio.js +47 -0
- package/system/plugin/claude-auto/claude.png +0 -0
- package/system/plugin/claude-auto/pinokio.js +58 -0
- package/system/plugin/claude-desktop/icon.jpeg +0 -0
- package/system/plugin/claude-desktop/pinokio.js +23 -0
- package/system/plugin/codex/openai.webp +0 -0
- package/system/plugin/codex/pinokio.js +42 -0
- package/system/plugin/codex-auto/openai.webp +0 -0
- package/system/plugin/codex-auto/pinokio.js +49 -0
- package/system/plugin/codex-desktop/icon.png +0 -0
- package/system/plugin/codex-desktop/pinokio.js +23 -0
- package/system/plugin/crush/crush.png +0 -0
- package/system/plugin/crush/pinokio.js +15 -0
- package/system/plugin/cursor/cursor.jpeg +0 -0
- package/system/plugin/cursor/pinokio.js +23 -0
- package/system/plugin/qwen/pinokio.js +34 -0
- package/system/plugin/qwen/qwen.png +0 -0
- package/system/plugin/vscode/pinokio.js +20 -0
- package/system/plugin/vscode/vscode.png +0 -0
- package/system/plugin/windsurf/pinokio.js +23 -0
- package/system/plugin/windsurf/windsurf.png +0 -0
- package/test/antigravity-cli-plugin.test.js +185 -0
- package/test/app-api.test.js +239 -0
- package/test/app-log-report.test.js +67 -0
- package/test/environment-cache-preflight.test.js +98 -0
- package/test/git-bin.test.js +59 -0
- package/test/git-defaults.test.js +97 -0
- package/test/github-api.test.js +158 -0
- package/test/github-connection.test.js +117 -0
- package/test/huggingface-bin.test.js +25 -0
- package/test/managed-skills.test.js +351 -0
- package/test/plugin-action-functions.test.js +337 -0
- package/test/plugin-dev-iframe.test.js +17 -0
- package/test/plugin-sources.test.js +203 -0
- package/test/privacy-filter-worker-heuristics.test.js +69 -0
- package/test/process-wait.test.js +169 -0
- package/test/script-api.test.js +97 -0
- package/test/shell-api.test.js +134 -0
- package/test/shell-run-template.test.js +209 -0
- package/test/storage-api.test.js +137 -0
- package/test/uri-api.test.js +100 -0
package/server/public/logs.js
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
const LOGS_SIDEBAR_WIDTH_KEY = 'pinokio.logs.sidebar-width'
|
|
5
5
|
const LOGS_SIDEBAR_MIN_WIDTH = 220
|
|
6
6
|
const LOGS_SIDEBAR_MAX_WIDTH = 560
|
|
7
|
+
const DRAFT_BODY_TARGET_BYTES = 750 * 1024
|
|
8
|
+
const DRAFT_IMPORT_FIELD_LIMIT_BYTES = 1024 * 1024
|
|
9
|
+
const DRAFT_TITLE_MAX_LENGTH = 120
|
|
10
|
+
const DRAFT_TITLE_DISPLAY_LENGTH = 96
|
|
11
|
+
const DRAFT_TITLE_RECENT_WINDOW_MS = 48 * 60 * 60 * 1000
|
|
12
|
+
const DRAFT_SECTION_MODES = [
|
|
13
|
+
{ value: 'full', label: 'Full section' },
|
|
14
|
+
{ value: 'last-2000', label: 'Last 2000 lines', lines: 2000 },
|
|
15
|
+
{ value: 'last-1000', label: 'Last 1000 lines', lines: 1000 },
|
|
16
|
+
{ value: 'last-500', label: 'Last 500 lines', lines: 500 },
|
|
17
|
+
{ value: 'exclude', label: 'Exclude' }
|
|
18
|
+
]
|
|
19
|
+
const DRAFT_TITLE_FAILURE_PATTERN = /\b(?:error|exception|failed|failure|fatal|cannot|can't|can not|not found|denied|timeout|timed out|refused|unavailable|invalid|missing|abort|aborted|panic|overflow|crash|crashed)\b/i
|
|
20
|
+
const DRAFT_TITLE_STRONG_PATTERN = /\b(?:[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception)|ERROR|FATAL|ERR!|exit code\s+\d+)\b/i
|
|
21
|
+
const DRAFT_TITLE_STACK_PATTERN = /^\s*(?:File\s+"[^"]+",\s+line\s+\d+|at\s+\S+|from\s+\S+\s+import\s+|return\s+|await\s+|sys\.exit\b)/i
|
|
22
|
+
const DRAFT_TITLE_NOISE_PATTERN = /^\s*(?:<<PINOKIO_SHELL>>|={6,}|-{6,}|\[api\s+local\.set\]|The default interactive shell is now|To update your account|For more details, please visit)/i
|
|
23
|
+
const textEncoder = typeof TextEncoder === 'function' ? new TextEncoder() : null
|
|
7
24
|
|
|
8
25
|
const safeJsonParse = (value) => {
|
|
9
26
|
try {
|
|
@@ -13,6 +30,130 @@
|
|
|
13
30
|
}
|
|
14
31
|
}
|
|
15
32
|
|
|
33
|
+
const humanBytes = (value) => {
|
|
34
|
+
const units = ['B', 'KB', 'MB', 'GB']
|
|
35
|
+
let size = Number(value) || 0
|
|
36
|
+
let index = 0
|
|
37
|
+
while (size >= 1024 && index < units.length - 1) {
|
|
38
|
+
size /= 1024
|
|
39
|
+
index += 1
|
|
40
|
+
}
|
|
41
|
+
return `${size.toFixed(index === 0 ? 0 : 1)} ${units[index]}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const textByteLength = (value) => {
|
|
45
|
+
const text = String(value || '')
|
|
46
|
+
if (textEncoder) {
|
|
47
|
+
return textEncoder.encode(text).length
|
|
48
|
+
}
|
|
49
|
+
return unescape(encodeURIComponent(text)).length
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const withQueryParam = (href, key, value) => {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(href, window.location.origin)
|
|
55
|
+
url.searchParams.set(key, value)
|
|
56
|
+
return `${url.pathname}${url.search}${url.hash}`
|
|
57
|
+
} catch (_) {
|
|
58
|
+
return href
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class PrivacyFilterClient {
|
|
63
|
+
constructor() {
|
|
64
|
+
this.worker = null
|
|
65
|
+
this.nextId = 1
|
|
66
|
+
this.pending = new Map()
|
|
67
|
+
this.runtimePromise = null
|
|
68
|
+
}
|
|
69
|
+
getWorker() {
|
|
70
|
+
if (this.worker) {
|
|
71
|
+
return this.worker
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
this.worker = new Worker('/privacy_filter_worker.js', { type: 'module' })
|
|
75
|
+
} catch (_) {
|
|
76
|
+
this.worker = new Worker('/privacy_filter_worker.js')
|
|
77
|
+
}
|
|
78
|
+
this.worker.addEventListener('message', (event) => this.handleMessage(event.data || {}))
|
|
79
|
+
this.worker.addEventListener('error', (event) => {
|
|
80
|
+
const error = new Error(event.message || 'Privacy filter worker failed.')
|
|
81
|
+
for (const pending of this.pending.values()) {
|
|
82
|
+
pending.reject(error)
|
|
83
|
+
}
|
|
84
|
+
this.pending.clear()
|
|
85
|
+
this.worker = null
|
|
86
|
+
})
|
|
87
|
+
return this.worker
|
|
88
|
+
}
|
|
89
|
+
handleMessage(message) {
|
|
90
|
+
if (message.type === 'download') {
|
|
91
|
+
for (const pending of this.pending.values()) {
|
|
92
|
+
pending.onProgress(message)
|
|
93
|
+
}
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
const pending = this.pending.get(message.id)
|
|
97
|
+
if (!pending) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (message.type === 'chunk') {
|
|
101
|
+
pending.onProgress(message)
|
|
102
|
+
} else if (message.type === 'fallback') {
|
|
103
|
+
pending.onProgress(message)
|
|
104
|
+
} else if (message.type === 'result') {
|
|
105
|
+
this.pending.delete(message.id)
|
|
106
|
+
pending.resolve(message)
|
|
107
|
+
} else if (message.type === 'error') {
|
|
108
|
+
this.pending.delete(message.id)
|
|
109
|
+
pending.reject(new Error(message.message || 'Privacy filter failed.'))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
detectRuntime() {
|
|
113
|
+
if (this.runtimePromise) {
|
|
114
|
+
return this.runtimePromise
|
|
115
|
+
}
|
|
116
|
+
this.runtimePromise = (async () => {
|
|
117
|
+
try {
|
|
118
|
+
if (navigator.gpu && typeof navigator.gpu.requestAdapter === 'function') {
|
|
119
|
+
const adapter = await navigator.gpu.requestAdapter()
|
|
120
|
+
if (adapter) {
|
|
121
|
+
return {
|
|
122
|
+
device: 'webgpu',
|
|
123
|
+
dtype: adapter.features && adapter.features.has('shader-f16') ? 'q4f16' : 'q4'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (_) {}
|
|
128
|
+
return { device: 'wasm', dtype: 'q8' }
|
|
129
|
+
})()
|
|
130
|
+
return this.runtimePromise
|
|
131
|
+
}
|
|
132
|
+
async filter(text, onProgress) {
|
|
133
|
+
const runtime = await this.detectRuntime()
|
|
134
|
+
const id = this.nextId
|
|
135
|
+
this.nextId += 1
|
|
136
|
+
const worker = this.getWorker()
|
|
137
|
+
if (typeof onProgress === 'function') {
|
|
138
|
+
onProgress({ type: 'runtime', ...runtime })
|
|
139
|
+
}
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
this.pending.set(id, {
|
|
142
|
+
resolve,
|
|
143
|
+
reject,
|
|
144
|
+
onProgress: typeof onProgress === 'function' ? onProgress : () => {}
|
|
145
|
+
})
|
|
146
|
+
worker.postMessage({
|
|
147
|
+
type: 'filter',
|
|
148
|
+
id,
|
|
149
|
+
text,
|
|
150
|
+
device: runtime.device,
|
|
151
|
+
dtype: runtime.dtype
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
16
157
|
class LogsZipControls {
|
|
17
158
|
constructor(options) {
|
|
18
159
|
this.button = options.button
|
|
@@ -68,6 +209,1198 @@
|
|
|
68
209
|
}
|
|
69
210
|
}
|
|
70
211
|
|
|
212
|
+
class LogsLatestReport {
|
|
213
|
+
constructor(options) {
|
|
214
|
+
this.reportUrl = options.reportUrl || ''
|
|
215
|
+
this.rawReportUrl = this.reportUrl ? withQueryParam(this.reportUrl, 'redaction', 'none') : ''
|
|
216
|
+
this.draftUrl = options.draftUrl || ''
|
|
217
|
+
this.registryBase = options.registryBase || 'https://pinokio.co'
|
|
218
|
+
this.statusEl = options.statusEl
|
|
219
|
+
this.outputEl = options.outputEl
|
|
220
|
+
this.copyButton = options.copyButton
|
|
221
|
+
this.createDraftButton = options.createDraftButton
|
|
222
|
+
this.draftTitleInput = options.draftTitleInput
|
|
223
|
+
this.draftTitleNoteEl = options.draftTitleNoteEl
|
|
224
|
+
this.runFilterButton = options.runFilterButton
|
|
225
|
+
this.refreshButton = options.refreshButton
|
|
226
|
+
this.reportFilesEl = options.reportFilesEl
|
|
227
|
+
this.reportGeneratedEl = options.reportGeneratedEl
|
|
228
|
+
this.reportSectionsEl = options.reportSectionsEl
|
|
229
|
+
this.draftSizeBadgeEl = options.draftSizeBadgeEl
|
|
230
|
+
this.draftMeterFillEl = options.draftMeterFillEl
|
|
231
|
+
this.draftStatusEl = options.draftStatusEl
|
|
232
|
+
this.reviewListEl = options.reviewListEl
|
|
233
|
+
this.reviewFiltersEl = options.reviewFiltersEl
|
|
234
|
+
this.reviewCountEl = options.reviewCountEl
|
|
235
|
+
this.privacyFilter = options.privacyFilter || new PrivacyFilterClient()
|
|
236
|
+
this.report = null
|
|
237
|
+
this.rawMarkdown = ''
|
|
238
|
+
this.reviewMarkdown = ''
|
|
239
|
+
this.currentMarkdown = ''
|
|
240
|
+
this.sectionModes = new Map()
|
|
241
|
+
this.draftBodyBytes = 0
|
|
242
|
+
this.draftPayloadBytes = 0
|
|
243
|
+
this.draftOversized = false
|
|
244
|
+
this.importingDraft = false
|
|
245
|
+
this.draftTitleEdited = false
|
|
246
|
+
this.draftTitleSuggestion = null
|
|
247
|
+
this.redactionItems = []
|
|
248
|
+
this.renderedRedactionItems = []
|
|
249
|
+
this.filterChunks = 0
|
|
250
|
+
this.redactionHasRun = false
|
|
251
|
+
this.filtering = false
|
|
252
|
+
this.activeRedactionFilter = 'all'
|
|
253
|
+
this.selectedRedactionId = null
|
|
254
|
+
this.filterToken = 0
|
|
255
|
+
this.loading = false
|
|
256
|
+
|
|
257
|
+
if (this.copyButton) {
|
|
258
|
+
this.copyButton.addEventListener('click', () => this.copy())
|
|
259
|
+
}
|
|
260
|
+
if (this.createDraftButton) {
|
|
261
|
+
this.createDraftButton.addEventListener('click', () => this.createDraft())
|
|
262
|
+
}
|
|
263
|
+
if (this.draftTitleInput) {
|
|
264
|
+
this.draftTitleInput.addEventListener('input', () => {
|
|
265
|
+
this.draftTitleEdited = true
|
|
266
|
+
this.updateDraftTitleNote()
|
|
267
|
+
this.updateDraftSizeReview()
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
if (this.runFilterButton) {
|
|
271
|
+
this.runFilterButton.addEventListener('click', () => this.filterReport())
|
|
272
|
+
}
|
|
273
|
+
if (this.refreshButton) {
|
|
274
|
+
this.refreshButton.addEventListener('click', () => this.load(true))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
setStatus(message, isError) {
|
|
278
|
+
if (!this.statusEl) return
|
|
279
|
+
this.statusEl.textContent = message || ''
|
|
280
|
+
this.statusEl.classList.toggle('is-error', Boolean(isError))
|
|
281
|
+
}
|
|
282
|
+
setBusy(isBusy) {
|
|
283
|
+
if (!this.refreshButton) return
|
|
284
|
+
this.refreshButton.disabled = Boolean(isBusy)
|
|
285
|
+
this.refreshButton.classList.toggle('is-busy', Boolean(isBusy))
|
|
286
|
+
}
|
|
287
|
+
formatGenerated(value) {
|
|
288
|
+
if (!value) return '--'
|
|
289
|
+
const date = new Date(value)
|
|
290
|
+
if (Number.isNaN(date.getTime())) return String(value)
|
|
291
|
+
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
292
|
+
}
|
|
293
|
+
clearReportFiles() {
|
|
294
|
+
if (!this.reportFilesEl) return
|
|
295
|
+
while (this.reportFilesEl.firstChild) {
|
|
296
|
+
this.reportFilesEl.removeChild(this.reportFilesEl.firstChild)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
sectionKey(section, index) {
|
|
300
|
+
return String((section && (section.file || section.script || section.source)) || `section-${index}`)
|
|
301
|
+
}
|
|
302
|
+
sectionMode(section, index) {
|
|
303
|
+
const key = this.sectionKey(section, index)
|
|
304
|
+
return this.sectionModes.get(key) || 'full'
|
|
305
|
+
}
|
|
306
|
+
setSectionMode(section, index, mode) {
|
|
307
|
+
const key = this.sectionKey(section, index)
|
|
308
|
+
const valid = DRAFT_SECTION_MODES.some((item) => item.value === mode)
|
|
309
|
+
this.sectionModes.set(key, valid ? mode : 'full')
|
|
310
|
+
this.rebuildDraftPreview(true)
|
|
311
|
+
this.renderReportFiles(this.report && this.report.sections)
|
|
312
|
+
}
|
|
313
|
+
ensureSectionModes(sections) {
|
|
314
|
+
const keys = new Set()
|
|
315
|
+
;(sections || []).forEach((section, index) => {
|
|
316
|
+
const key = this.sectionKey(section, index)
|
|
317
|
+
keys.add(key)
|
|
318
|
+
if (!this.sectionModes.has(key)) {
|
|
319
|
+
this.sectionModes.set(key, 'full')
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
for (const key of Array.from(this.sectionModes.keys())) {
|
|
323
|
+
if (!keys.has(key)) {
|
|
324
|
+
this.sectionModes.delete(key)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
prepareSection(section, index) {
|
|
329
|
+
const mode = this.sectionMode(section, index)
|
|
330
|
+
if (mode === 'exclude') {
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
const config = DRAFT_SECTION_MODES.find((item) => item.value === mode)
|
|
334
|
+
const text = String((section && section.text) || '')
|
|
335
|
+
const lines = text ? text.split(/\r?\n/) : []
|
|
336
|
+
if (config && config.lines && lines.length > config.lines) {
|
|
337
|
+
const omitted = lines.length - config.lines
|
|
338
|
+
return {
|
|
339
|
+
text: `[Older ${omitted.toLocaleString()} lines omitted by user. Showing the last ${config.lines.toLocaleString()} lines.]\n${lines.slice(-config.lines).join('\n')}`,
|
|
340
|
+
includedLines: config.lines,
|
|
341
|
+
omittedLines: omitted,
|
|
342
|
+
mode
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
text,
|
|
347
|
+
includedLines: lines.length,
|
|
348
|
+
omittedLines: 0,
|
|
349
|
+
mode
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
includedSectionCount(sections) {
|
|
353
|
+
return (sections || []).reduce((total, section, index) => {
|
|
354
|
+
return total + (this.prepareSection(section, index) ? 1 : 0)
|
|
355
|
+
}, 0)
|
|
356
|
+
}
|
|
357
|
+
updateSectionCount(sections) {
|
|
358
|
+
if (!this.reportSectionsEl) return
|
|
359
|
+
const total = Array.isArray(sections) ? sections.length : 0
|
|
360
|
+
const included = this.includedSectionCount(sections || [])
|
|
361
|
+
this.reportSectionsEl.textContent = included === total ? String(total) : `${included} / ${total}`
|
|
362
|
+
}
|
|
363
|
+
defaultDraftTitle() {
|
|
364
|
+
const payload = this.report || {}
|
|
365
|
+
const appTitle = payload.title || payload.app_id || 'Pinokio app'
|
|
366
|
+
return this.truncateDraftTitle(`Issue report: ${appTitle}`, DRAFT_TITLE_MAX_LENGTH)
|
|
367
|
+
}
|
|
368
|
+
truncateDraftTitle(value, maxLength = DRAFT_TITLE_DISPLAY_LENGTH) {
|
|
369
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim()
|
|
370
|
+
if (text.length <= maxLength) {
|
|
371
|
+
return text
|
|
372
|
+
}
|
|
373
|
+
return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`
|
|
374
|
+
}
|
|
375
|
+
draftTitleValue() {
|
|
376
|
+
if (this.draftTitleInput) {
|
|
377
|
+
return this.truncateDraftTitle(this.draftTitleInput.value, DRAFT_TITLE_MAX_LENGTH)
|
|
378
|
+
}
|
|
379
|
+
return (this.draftTitleSuggestion && this.draftTitleSuggestion.title) || this.defaultDraftTitle()
|
|
380
|
+
}
|
|
381
|
+
hasDraftTitle() {
|
|
382
|
+
return Boolean(this.draftTitleValue())
|
|
383
|
+
}
|
|
384
|
+
clearDraftTitle() {
|
|
385
|
+
this.draftTitleEdited = false
|
|
386
|
+
this.draftTitleSuggestion = null
|
|
387
|
+
if (this.draftTitleInput) {
|
|
388
|
+
this.draftTitleInput.value = ''
|
|
389
|
+
this.draftTitleInput.disabled = true
|
|
390
|
+
}
|
|
391
|
+
this.updateDraftTitleNote()
|
|
392
|
+
}
|
|
393
|
+
updateDraftTitleNote() {
|
|
394
|
+
if (!this.draftTitleNoteEl) return
|
|
395
|
+
let note = ''
|
|
396
|
+
if (this.draftTitleInput && !this.draftTitleInput.disabled && !this.draftTitleInput.value.trim()) {
|
|
397
|
+
note = 'Title required'
|
|
398
|
+
}
|
|
399
|
+
this.draftTitleNoteEl.textContent = note
|
|
400
|
+
}
|
|
401
|
+
updateDraftTitleSuggestion(force = false) {
|
|
402
|
+
this.draftTitleSuggestion = this.suggestDraftTitle()
|
|
403
|
+
if (this.draftTitleInput) {
|
|
404
|
+
if (force || !this.draftTitleEdited) {
|
|
405
|
+
this.draftTitleInput.value = this.draftTitleSuggestion.title
|
|
406
|
+
this.draftTitleEdited = false
|
|
407
|
+
}
|
|
408
|
+
this.draftTitleInput.disabled = !this.reviewMarkdown
|
|
409
|
+
}
|
|
410
|
+
this.updateDraftTitleNote()
|
|
411
|
+
}
|
|
412
|
+
suggestDraftTitle() {
|
|
413
|
+
const payload = this.report || {}
|
|
414
|
+
const appTitle = payload.title || payload.app_id || 'Pinokio app'
|
|
415
|
+
const sections = Array.isArray(payload.sections) ? payload.sections : []
|
|
416
|
+
const preparedSections = []
|
|
417
|
+
let newestModified = 0
|
|
418
|
+
sections.forEach((section, index) => {
|
|
419
|
+
const prepared = this.prepareSection(section, index)
|
|
420
|
+
if (!prepared) return
|
|
421
|
+
const modified = Date.parse(section.modified || '')
|
|
422
|
+
if (Number.isFinite(modified)) {
|
|
423
|
+
newestModified = Math.max(newestModified, modified)
|
|
424
|
+
}
|
|
425
|
+
preparedSections.push({ section, index, prepared, modified: Number.isFinite(modified) ? modified : 0 })
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
const candidates = []
|
|
429
|
+
for (const item of preparedSections) {
|
|
430
|
+
const candidate = this.bestTitleCandidateForSection(item.section, item.index, item.prepared)
|
|
431
|
+
if (!candidate) continue
|
|
432
|
+
let score = candidate.score
|
|
433
|
+
if (newestModified && item.modified && newestModified - item.modified > DRAFT_TITLE_RECENT_WINDOW_MS) {
|
|
434
|
+
score -= 35
|
|
435
|
+
}
|
|
436
|
+
candidates.push({
|
|
437
|
+
...candidate,
|
|
438
|
+
score,
|
|
439
|
+
modified: item.modified,
|
|
440
|
+
file: item.section.file || item.section.script || 'log'
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
candidates.sort((a, b) => {
|
|
445
|
+
const scoreDelta = b.score - a.score
|
|
446
|
+
if (Math.abs(scoreDelta) > 10) return scoreDelta
|
|
447
|
+
if (a.modified && b.modified && Math.abs(a.modified - b.modified) > 60000) {
|
|
448
|
+
return a.modified - b.modified
|
|
449
|
+
}
|
|
450
|
+
return a.lineIndex - b.lineIndex
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const best = candidates[0]
|
|
454
|
+
if (!best || best.score < 35) {
|
|
455
|
+
return { title: '', confidence: 'low', fallback: true }
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
title: this.truncateDraftTitle(`${appTitle}: ${best.text}`, DRAFT_TITLE_MAX_LENGTH),
|
|
459
|
+
confidence: best.score >= 70 ? 'high' : 'medium',
|
|
460
|
+
source: best.file,
|
|
461
|
+
fallback: false
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
bestTitleCandidateForSection(section, index, prepared) {
|
|
465
|
+
const lines = String((prepared && prepared.text) || '').split(/\r?\n/)
|
|
466
|
+
let best = null
|
|
467
|
+
lines.forEach((line, lineIndex) => {
|
|
468
|
+
const text = this.cleanDraftTitleLine(line)
|
|
469
|
+
if (!text) return
|
|
470
|
+
const score = this.scoreDraftTitleLine(text, lineIndex, lines.length)
|
|
471
|
+
if (score < 25) return
|
|
472
|
+
if (!best || score > best.score + 6 || (Math.abs(score - best.score) <= 6 && lineIndex < best.lineIndex)) {
|
|
473
|
+
best = { text, score, lineIndex, sectionIndex: index }
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
return best
|
|
477
|
+
}
|
|
478
|
+
scoreDraftTitleLine(line, lineIndex, lineCount) {
|
|
479
|
+
let score = 0
|
|
480
|
+
if (DRAFT_TITLE_FAILURE_PATTERN.test(line)) score += 35
|
|
481
|
+
if (DRAFT_TITLE_STRONG_PATTERN.test(line)) score += 35
|
|
482
|
+
if (/:\s+\S/.test(line) && DRAFT_TITLE_FAILURE_PATTERN.test(line)) score += 8
|
|
483
|
+
if (lineIndex > Math.floor(lineCount * 0.7)) score += 6
|
|
484
|
+
if (DRAFT_TITLE_STACK_PATTERN.test(line)) score -= 28
|
|
485
|
+
if (DRAFT_TITLE_NOISE_PATTERN.test(line)) score -= 60
|
|
486
|
+
if (line.length > 180) score -= 12
|
|
487
|
+
return score
|
|
488
|
+
}
|
|
489
|
+
cleanDraftTitleLine(value) {
|
|
490
|
+
let text = String(value || '')
|
|
491
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
492
|
+
.replace(/\r/g, ' ')
|
|
493
|
+
.trim()
|
|
494
|
+
if (!text || DRAFT_TITLE_NOISE_PATTERN.test(text)) {
|
|
495
|
+
return ''
|
|
496
|
+
}
|
|
497
|
+
text = text
|
|
498
|
+
.replace(/^[-+*]\s+/, '')
|
|
499
|
+
.replace(/^<<PINOKIO_SHELL>>\s*/, '')
|
|
500
|
+
.replace(/\b([A-Za-z_][A-Za-z0-9_.-]*(?:TOKEN|SECRET|PASSWORD|API[_-]?KEY)[A-Za-z0-9_.-]*\s*=\s*)\S+/gi, '$1[redacted]')
|
|
501
|
+
.replace(/\b(?:sk|hf|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{12,}\b/g, '[secret]')
|
|
502
|
+
.replace(/\/(?:Users|home)\/[^/\s"'`]+\/[^\s"'`]*/g, (match) => this.shortenTitlePath(match, '/'))
|
|
503
|
+
.replace(/[A-Za-z]:\\Users\\[^\\\s"'`]+\\[^\s"'`]*/g, (match) => this.shortenTitlePath(match, '\\'))
|
|
504
|
+
.replace(/\s+/g, ' ')
|
|
505
|
+
.trim()
|
|
506
|
+
if (!text || /^[\s=#-]+$/.test(text)) {
|
|
507
|
+
return ''
|
|
508
|
+
}
|
|
509
|
+
return this.truncateDraftTitle(text, DRAFT_TITLE_DISPLAY_LENGTH)
|
|
510
|
+
}
|
|
511
|
+
shortenTitlePath(value, separator) {
|
|
512
|
+
const text = String(value || '')
|
|
513
|
+
const parts = text.split(separator).filter(Boolean)
|
|
514
|
+
if (parts.length <= 3) {
|
|
515
|
+
return text
|
|
516
|
+
}
|
|
517
|
+
return `…${separator}${parts.slice(-2).join(separator)}`
|
|
518
|
+
}
|
|
519
|
+
renderDraftMarkdown() {
|
|
520
|
+
const payload = this.report || {}
|
|
521
|
+
const sections = Array.isArray(payload.sections) ? payload.sections : []
|
|
522
|
+
const lines = [
|
|
523
|
+
'# Issue Report',
|
|
524
|
+
'',
|
|
525
|
+
`App: ${payload.title || payload.app_id || 'unknown'} (${payload.app_id || 'unknown'})`,
|
|
526
|
+
payload.repo_url ? `Repo: ${payload.repo_url}` : null,
|
|
527
|
+
`Generated: ${payload.generated_at || new Date().toISOString()}`,
|
|
528
|
+
`Pinokio: ${payload.pinokiod || 'unknown'}`,
|
|
529
|
+
`Platform: ${payload.platform || 'unknown'} ${payload.arch || ''}`.trim(),
|
|
530
|
+
`Node: ${payload.node || 'unknown'}`,
|
|
531
|
+
'',
|
|
532
|
+
'## Summary',
|
|
533
|
+
'',
|
|
534
|
+
'',
|
|
535
|
+
'## System',
|
|
536
|
+
'',
|
|
537
|
+
'```json',
|
|
538
|
+
JSON.stringify(payload.system_spec || {}, null, 2),
|
|
539
|
+
'```',
|
|
540
|
+
'',
|
|
541
|
+
'## Logs'
|
|
542
|
+
].filter((line) => line !== null)
|
|
543
|
+
|
|
544
|
+
let included = 0
|
|
545
|
+
sections.forEach((section, index) => {
|
|
546
|
+
const prepared = this.prepareSection(section, index)
|
|
547
|
+
if (!prepared) {
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
included += 1
|
|
551
|
+
const totalLines = Number(section.line_count) || prepared.includedLines || 0
|
|
552
|
+
const availableLines = Math.min(totalLines, Number(section.tail_count) || prepared.includedLines || totalLines)
|
|
553
|
+
const lineSummary = prepared.omittedLines > 0
|
|
554
|
+
? `${totalLines} total, last ${prepared.includedLines} selected by user`
|
|
555
|
+
: `${totalLines} total, last ${availableLines} included${section.truncated ? ' (truncated)' : ''}`
|
|
556
|
+
lines.push(
|
|
557
|
+
'',
|
|
558
|
+
`### ${section.file || section.script || 'log'}`,
|
|
559
|
+
'',
|
|
560
|
+
`Source: ${section.source || 'api'}${section.script ? ` / ${section.script}` : ''}`,
|
|
561
|
+
`Lines: ${lineSummary}`,
|
|
562
|
+
'',
|
|
563
|
+
'```text',
|
|
564
|
+
prepared.text || '',
|
|
565
|
+
'```'
|
|
566
|
+
)
|
|
567
|
+
})
|
|
568
|
+
if (!included) {
|
|
569
|
+
lines.push('', 'No app log files were selected.')
|
|
570
|
+
}
|
|
571
|
+
return lines.join('\n')
|
|
572
|
+
}
|
|
573
|
+
rebuildDraftPreview(resetRedactions) {
|
|
574
|
+
this.reviewMarkdown = this.renderDraftMarkdown()
|
|
575
|
+
if (resetRedactions) {
|
|
576
|
+
this.redactionHasRun = false
|
|
577
|
+
this.redactionItems = []
|
|
578
|
+
this.renderedRedactionItems = []
|
|
579
|
+
this.selectedRedactionId = null
|
|
580
|
+
this.activeRedactionFilter = 'all'
|
|
581
|
+
this.resetRedactionReview(this.reviewMarkdown ? 'Run the privacy filter to review detected items.' : 'No report text to review.')
|
|
582
|
+
}
|
|
583
|
+
this.currentMarkdown = this.reviewMarkdown
|
|
584
|
+
this.updateDraftTitleSuggestion(false)
|
|
585
|
+
this.renderCurrentReport()
|
|
586
|
+
this.updateSectionCount(this.report && this.report.sections)
|
|
587
|
+
this.setRunFilterEnabled(Boolean(this.reviewMarkdown) && !this.filtering)
|
|
588
|
+
}
|
|
589
|
+
renderReportFiles(sections) {
|
|
590
|
+
this.clearReportFiles()
|
|
591
|
+
if (!this.reportFilesEl) return
|
|
592
|
+
;(sections || []).forEach((section, index) => {
|
|
593
|
+
const mode = this.sectionMode(section, index)
|
|
594
|
+
const prepared = this.prepareSection(section, index)
|
|
595
|
+
const item = document.createElement('div')
|
|
596
|
+
item.className = 'logs-section-control'
|
|
597
|
+
item.classList.toggle('is-excluded', mode === 'exclude')
|
|
598
|
+
|
|
599
|
+
const checkbox = document.createElement('input')
|
|
600
|
+
checkbox.type = 'checkbox'
|
|
601
|
+
checkbox.className = 'logs-section-checkbox'
|
|
602
|
+
checkbox.checked = mode !== 'exclude'
|
|
603
|
+
checkbox.setAttribute('aria-label', `Include ${section.file || section.script || 'log section'}`)
|
|
604
|
+
checkbox.addEventListener('change', () => {
|
|
605
|
+
this.setSectionMode(section, index, checkbox.checked ? 'full' : 'exclude')
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
const textWrap = document.createElement('div')
|
|
609
|
+
textWrap.className = 'logs-section-text'
|
|
610
|
+
const name = document.createElement('div')
|
|
611
|
+
name.className = 'logs-section-name'
|
|
612
|
+
name.textContent = section.file || section.script || section.source || 'log'
|
|
613
|
+
name.title = name.textContent
|
|
614
|
+
const meta = document.createElement('div')
|
|
615
|
+
meta.className = 'logs-section-meta'
|
|
616
|
+
const size = Number(section.size) ? humanBytes(section.size) : 'unknown size'
|
|
617
|
+
const totalLines = Number(section.line_count) || 0
|
|
618
|
+
const trimText = prepared && prepared.omittedLines > 0 ? ` · ${prepared.omittedLines.toLocaleString()} older lines omitted` : ''
|
|
619
|
+
meta.textContent = `${totalLines.toLocaleString()} lines · ${size}${trimText}`
|
|
620
|
+
textWrap.appendChild(name)
|
|
621
|
+
textWrap.appendChild(meta)
|
|
622
|
+
|
|
623
|
+
const select = document.createElement('select')
|
|
624
|
+
select.className = 'logs-section-mode'
|
|
625
|
+
select.setAttribute('aria-label', `Draft inclusion for ${name.textContent}`)
|
|
626
|
+
for (const optionConfig of DRAFT_SECTION_MODES) {
|
|
627
|
+
const option = document.createElement('option')
|
|
628
|
+
option.value = optionConfig.value
|
|
629
|
+
option.textContent = optionConfig.label
|
|
630
|
+
select.appendChild(option)
|
|
631
|
+
}
|
|
632
|
+
select.value = mode
|
|
633
|
+
select.addEventListener('change', () => this.setSectionMode(section, index, select.value))
|
|
634
|
+
|
|
635
|
+
item.appendChild(checkbox)
|
|
636
|
+
item.appendChild(textWrap)
|
|
637
|
+
item.appendChild(select)
|
|
638
|
+
this.reportFilesEl.appendChild(item)
|
|
639
|
+
})
|
|
640
|
+
if (!sections || !sections.length) {
|
|
641
|
+
const empty = document.createElement('div')
|
|
642
|
+
empty.className = 'logs-review-empty'
|
|
643
|
+
empty.textContent = 'No latest files found.'
|
|
644
|
+
this.reportFilesEl.appendChild(empty)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
renderSummary(payload) {
|
|
648
|
+
const sections = Array.isArray(payload && payload.sections) ? payload.sections : []
|
|
649
|
+
this.updateSectionCount(sections)
|
|
650
|
+
if (this.reportGeneratedEl) {
|
|
651
|
+
this.reportGeneratedEl.textContent = this.formatGenerated(payload && payload.generated_at)
|
|
652
|
+
}
|
|
653
|
+
this.renderReportFiles(sections)
|
|
654
|
+
return sections
|
|
655
|
+
}
|
|
656
|
+
setCopyEnabled(enabled) {
|
|
657
|
+
if (this.copyButton) {
|
|
658
|
+
this.copyButton.disabled = !enabled
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
setRunFilterEnabled(enabled) {
|
|
662
|
+
if (this.runFilterButton) {
|
|
663
|
+
this.runFilterButton.disabled = !enabled
|
|
664
|
+
if (!this.filtering) {
|
|
665
|
+
this.runFilterButton.classList.remove('is-busy')
|
|
666
|
+
this.runFilterButton.innerHTML = '<i class="fa-solid fa-shield-halved"></i><span>Run privacy filter</span>'
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
setFiltering(isFiltering) {
|
|
671
|
+
this.filtering = Boolean(isFiltering)
|
|
672
|
+
if (this.runFilterButton) {
|
|
673
|
+
this.runFilterButton.disabled = this.filtering || !this.reviewMarkdown
|
|
674
|
+
this.runFilterButton.classList.toggle('is-busy', this.filtering)
|
|
675
|
+
if (this.filtering) {
|
|
676
|
+
this.runFilterButton.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i><span>Filtering…</span>'
|
|
677
|
+
} else {
|
|
678
|
+
this.runFilterButton.innerHTML = '<i class="fa-solid fa-shield-halved"></i><span>Run privacy filter</span>'
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this.updateDraftSizeReview()
|
|
682
|
+
}
|
|
683
|
+
setOutputText(text) {
|
|
684
|
+
if (this.outputEl) {
|
|
685
|
+
this.outputEl.textContent = text || ''
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
normalizeRedactionItems(items, text) {
|
|
689
|
+
const normalized = (Array.isArray(items) ? items : [])
|
|
690
|
+
.map((item, index) => {
|
|
691
|
+
const sourceStart = Number(item.sourceStart != null ? item.sourceStart : item.maskedStart)
|
|
692
|
+
const sourceEnd = Number(item.sourceEnd != null ? item.sourceEnd : item.maskedEnd)
|
|
693
|
+
if (!Number.isFinite(sourceStart) || !Number.isFinite(sourceEnd) || sourceEnd <= sourceStart || sourceStart < 0 || sourceEnd > text.length) {
|
|
694
|
+
return null
|
|
695
|
+
}
|
|
696
|
+
const id = item.id != null ? String(item.id) : String(index)
|
|
697
|
+
const label = String(item.label || 'private')
|
|
698
|
+
return {
|
|
699
|
+
id,
|
|
700
|
+
label,
|
|
701
|
+
sourceStart,
|
|
702
|
+
sourceEnd,
|
|
703
|
+
replacement: item.replacement || `[${label}]`,
|
|
704
|
+
enabled: item.enabled !== false,
|
|
705
|
+
line: this.lineForOffset(text, sourceStart),
|
|
706
|
+
context: this.lineContext(text, sourceStart, sourceEnd),
|
|
707
|
+
source: this.sourceForOffset(text, sourceStart)
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
.filter(Boolean)
|
|
711
|
+
.sort((a, b) => a.sourceStart - b.sourceStart || a.sourceEnd - b.sourceEnd)
|
|
712
|
+
const nonOverlapping = []
|
|
713
|
+
let cursor = 0
|
|
714
|
+
for (const item of normalized) {
|
|
715
|
+
if (item.sourceStart < cursor) {
|
|
716
|
+
continue
|
|
717
|
+
}
|
|
718
|
+
nonOverlapping.push(item)
|
|
719
|
+
cursor = item.sourceEnd
|
|
720
|
+
}
|
|
721
|
+
return nonOverlapping
|
|
722
|
+
}
|
|
723
|
+
lineForOffset(text, offset) {
|
|
724
|
+
return String(text || '').slice(0, Math.max(0, offset)).split('\n').length
|
|
725
|
+
}
|
|
726
|
+
lineContext(text, start, end) {
|
|
727
|
+
const value = String(text || '')
|
|
728
|
+
const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1
|
|
729
|
+
const nextLine = value.indexOf('\n', Math.max(0, end))
|
|
730
|
+
const lineEnd = nextLine >= 0 ? nextLine : value.length
|
|
731
|
+
return value.slice(lineStart, lineEnd).trim()
|
|
732
|
+
}
|
|
733
|
+
sourceForOffset(text, offset) {
|
|
734
|
+
const before = String(text || '').slice(0, Math.max(0, offset))
|
|
735
|
+
let source = 'Issue report'
|
|
736
|
+
const pattern = /^###\s+(.+)$/gm
|
|
737
|
+
let match = pattern.exec(before)
|
|
738
|
+
while (match) {
|
|
739
|
+
source = match[1]
|
|
740
|
+
match = pattern.exec(before)
|
|
741
|
+
}
|
|
742
|
+
return source
|
|
743
|
+
}
|
|
744
|
+
buildCurrentReport() {
|
|
745
|
+
const text = this.reviewMarkdown || ''
|
|
746
|
+
if (!text || !this.redactionItems.length) {
|
|
747
|
+
this.currentMarkdown = text
|
|
748
|
+
this.renderedRedactionItems = []
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
let cursor = 0
|
|
752
|
+
let output = ''
|
|
753
|
+
const renderedItems = []
|
|
754
|
+
for (const item of this.redactionItems) {
|
|
755
|
+
if (item.sourceStart < cursor) {
|
|
756
|
+
continue
|
|
757
|
+
}
|
|
758
|
+
if (item.sourceStart > cursor) {
|
|
759
|
+
output += text.slice(cursor, item.sourceStart)
|
|
760
|
+
}
|
|
761
|
+
const value = item.enabled ? item.replacement : text.slice(item.sourceStart, item.sourceEnd)
|
|
762
|
+
const maskedStart = output.length
|
|
763
|
+
output += value
|
|
764
|
+
const maskedEnd = output.length
|
|
765
|
+
renderedItems.push({
|
|
766
|
+
...item,
|
|
767
|
+
maskedStart,
|
|
768
|
+
maskedEnd,
|
|
769
|
+
line: this.lineForOffset(output, maskedStart),
|
|
770
|
+
context: this.lineContext(output, maskedStart, maskedEnd),
|
|
771
|
+
source: this.sourceForOffset(output, maskedStart)
|
|
772
|
+
})
|
|
773
|
+
cursor = item.sourceEnd
|
|
774
|
+
}
|
|
775
|
+
if (cursor < text.length) {
|
|
776
|
+
output += text.slice(cursor)
|
|
777
|
+
}
|
|
778
|
+
this.currentMarkdown = output
|
|
779
|
+
this.renderedRedactionItems = renderedItems
|
|
780
|
+
}
|
|
781
|
+
enabledRedactionCount() {
|
|
782
|
+
return this.redactionItems.reduce((total, item) => total + (item.enabled ? 1 : 0), 0)
|
|
783
|
+
}
|
|
784
|
+
updateRedactionCount() {
|
|
785
|
+
if (!this.reviewCountEl) return
|
|
786
|
+
if (!this.redactionHasRun) {
|
|
787
|
+
this.reviewCountEl.textContent = 'Not run'
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
const enabled = this.enabledRedactionCount()
|
|
791
|
+
const total = this.redactionItems.length
|
|
792
|
+
this.reviewCountEl.textContent = total ? `${enabled} masked` : '0 masked'
|
|
793
|
+
}
|
|
794
|
+
encodeUtf8Base64(value) {
|
|
795
|
+
const text = String(value || '')
|
|
796
|
+
if (!textEncoder || typeof btoa !== 'function') {
|
|
797
|
+
return btoa(unescape(encodeURIComponent(text)))
|
|
798
|
+
}
|
|
799
|
+
const bytes = textEncoder.encode(text)
|
|
800
|
+
let binary = ''
|
|
801
|
+
const chunkSize = 0x8000
|
|
802
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
803
|
+
const chunk = bytes.subarray(index, index + chunkSize)
|
|
804
|
+
binary += String.fromCharCode.apply(null, chunk)
|
|
805
|
+
}
|
|
806
|
+
return btoa(binary)
|
|
807
|
+
}
|
|
808
|
+
buildDraftMetadata() {
|
|
809
|
+
const payload = this.report || {}
|
|
810
|
+
const appTitle = payload.title || payload.app_id || 'Pinokio app'
|
|
811
|
+
const title = this.draftTitleInput
|
|
812
|
+
? this.draftTitleValue()
|
|
813
|
+
: (this.draftTitleValue() || `Issue report: ${appTitle}`)
|
|
814
|
+
const metadata = {
|
|
815
|
+
title,
|
|
816
|
+
body: this.currentMarkdown || this.reviewMarkdown || '',
|
|
817
|
+
tags: ['bug', 'logs'],
|
|
818
|
+
source: 'pinokio-logs',
|
|
819
|
+
appLocalId: payload.app_id || ''
|
|
820
|
+
}
|
|
821
|
+
if (payload.repo_url) {
|
|
822
|
+
metadata.repoUrl = payload.repo_url
|
|
823
|
+
metadata.appRepoUrl = payload.repo_url
|
|
824
|
+
metadata.parent = { type: 'app', url: payload.repo_url }
|
|
825
|
+
}
|
|
826
|
+
return metadata
|
|
827
|
+
}
|
|
828
|
+
buildDraftImportPayload() {
|
|
829
|
+
const metadata = this.buildDraftMetadata()
|
|
830
|
+
const metadataJson = JSON.stringify(metadata)
|
|
831
|
+
const metadataB64 = this.encodeUtf8Base64(metadataJson)
|
|
832
|
+
return {
|
|
833
|
+
metadata,
|
|
834
|
+
metadataB64,
|
|
835
|
+
bodyBytes: textByteLength(metadata.body || ''),
|
|
836
|
+
payloadBytes: textByteLength(metadataB64)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
setCreateDraftBusy(isBusy) {
|
|
840
|
+
this.importingDraft = Boolean(isBusy)
|
|
841
|
+
if (!this.createDraftButton) return
|
|
842
|
+
if (this.importingDraft) {
|
|
843
|
+
this.createDraftButton.disabled = true
|
|
844
|
+
this.createDraftButton.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i><span>Opening…</span>'
|
|
845
|
+
} else {
|
|
846
|
+
this.createDraftButton.innerHTML = '<i class="fa-solid fa-paper-plane"></i><span>Ask Community</span>'
|
|
847
|
+
this.updateDraftSizeReview()
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
updateDraftSizeReview() {
|
|
851
|
+
const hasText = Boolean(this.currentMarkdown || this.reviewMarkdown)
|
|
852
|
+
const hasTitle = this.hasDraftTitle()
|
|
853
|
+
let draft = null
|
|
854
|
+
if (hasText) {
|
|
855
|
+
draft = this.buildDraftImportPayload()
|
|
856
|
+
this.draftBodyBytes = draft.bodyBytes
|
|
857
|
+
this.draftPayloadBytes = draft.payloadBytes
|
|
858
|
+
this.draftOversized = draft.bodyBytes > DRAFT_BODY_TARGET_BYTES || draft.payloadBytes > DRAFT_IMPORT_FIELD_LIMIT_BYTES
|
|
859
|
+
} else {
|
|
860
|
+
this.draftBodyBytes = 0
|
|
861
|
+
this.draftPayloadBytes = 0
|
|
862
|
+
this.draftOversized = false
|
|
863
|
+
}
|
|
864
|
+
if (this.draftSizeBadgeEl) {
|
|
865
|
+
this.draftSizeBadgeEl.textContent = hasText ? `${humanBytes(this.draftBodyBytes)} / ${humanBytes(DRAFT_BODY_TARGET_BYTES)}` : '--'
|
|
866
|
+
this.draftSizeBadgeEl.classList.toggle('is-error', Boolean(hasText && this.draftOversized))
|
|
867
|
+
}
|
|
868
|
+
if (this.draftMeterFillEl) {
|
|
869
|
+
const pct = hasText ? Math.min(100, Math.round((this.draftBodyBytes / DRAFT_BODY_TARGET_BYTES) * 100)) : 0
|
|
870
|
+
this.draftMeterFillEl.style.width = `${pct}%`
|
|
871
|
+
this.draftMeterFillEl.classList.toggle('is-error', Boolean(hasText && this.draftOversized))
|
|
872
|
+
}
|
|
873
|
+
if (this.draftStatusEl) {
|
|
874
|
+
let message = 'Waiting for latest log snapshot.'
|
|
875
|
+
let isError = false
|
|
876
|
+
if (hasText && this.draftOversized) {
|
|
877
|
+
isError = true
|
|
878
|
+
message = this.draftPayloadBytes > DRAFT_IMPORT_FIELD_LIMIT_BYTES
|
|
879
|
+
? `Too large for registry import after encoding. Exclude a section or keep fewer recent lines.`
|
|
880
|
+
: `Too large for one-click draft import. Exclude a section or keep fewer recent lines.`
|
|
881
|
+
} else if (hasText && !hasTitle) {
|
|
882
|
+
isError = true
|
|
883
|
+
message = 'Add a title before posting to Community.'
|
|
884
|
+
} else if (hasText) {
|
|
885
|
+
message = 'Ready. Community will use this preview exactly.'
|
|
886
|
+
}
|
|
887
|
+
this.draftStatusEl.textContent = message
|
|
888
|
+
this.draftStatusEl.classList.toggle('is-error', isError)
|
|
889
|
+
}
|
|
890
|
+
if (this.draftTitleInput) {
|
|
891
|
+
this.draftTitleInput.disabled = !hasText
|
|
892
|
+
}
|
|
893
|
+
this.updateDraftTitleNote()
|
|
894
|
+
if (this.createDraftButton && !this.importingDraft) {
|
|
895
|
+
const canCreate = hasText && hasTitle && !this.draftOversized && !this.filtering && Boolean(this.draftUrl)
|
|
896
|
+
this.createDraftButton.disabled = !canCreate
|
|
897
|
+
this.createDraftButton.title = canCreate
|
|
898
|
+
? 'Open a draft question on Community'
|
|
899
|
+
: (!hasTitle ? 'Add a title before posting to Community' : (this.draftOversized ? 'Reduce the report size before posting to Community' : 'Community posting is not available for this view'))
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
renderCurrentReport() {
|
|
903
|
+
this.buildCurrentReport()
|
|
904
|
+
const text = this.currentMarkdown || 'No latest app logs were found.'
|
|
905
|
+
this.renderHighlightedOutput(text, this.renderedRedactionItems)
|
|
906
|
+
this.renderRedactionReview()
|
|
907
|
+
this.updateRedactionCount()
|
|
908
|
+
this.updateDraftSizeReview()
|
|
909
|
+
this.setCopyEnabled(Boolean(this.currentMarkdown))
|
|
910
|
+
this.selectRedaction(this.selectedRedactionId, false)
|
|
911
|
+
}
|
|
912
|
+
renderHighlightedOutput(text, items = []) {
|
|
913
|
+
if (!this.outputEl) {
|
|
914
|
+
return
|
|
915
|
+
}
|
|
916
|
+
this.outputEl.textContent = ''
|
|
917
|
+
if (!text) {
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
if (!items.length) {
|
|
921
|
+
this.outputEl.textContent = text
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
let cursor = 0
|
|
925
|
+
for (const item of items) {
|
|
926
|
+
if (item.maskedStart < cursor) {
|
|
927
|
+
continue
|
|
928
|
+
}
|
|
929
|
+
if (item.maskedStart > cursor) {
|
|
930
|
+
this.outputEl.appendChild(document.createTextNode(text.slice(cursor, item.maskedStart)))
|
|
931
|
+
}
|
|
932
|
+
const token = document.createElement('span')
|
|
933
|
+
token.className = item.enabled ? 'logs-mask-token' : 'logs-unmasked-token'
|
|
934
|
+
token.dataset.redactionId = item.id
|
|
935
|
+
token.textContent = text.slice(item.maskedStart, item.maskedEnd)
|
|
936
|
+
token.title = `${item.enabled ? 'Masked' : 'Visible'} ${item.label} · line ${item.line}`
|
|
937
|
+
this.outputEl.appendChild(token)
|
|
938
|
+
cursor = item.maskedEnd
|
|
939
|
+
}
|
|
940
|
+
if (cursor < text.length) {
|
|
941
|
+
this.outputEl.appendChild(document.createTextNode(text.slice(cursor)))
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
renderRedactionReview() {
|
|
945
|
+
const items = this.redactionItems || []
|
|
946
|
+
const renderedById = new Map((this.renderedRedactionItems || []).map((item) => [item.id, item]))
|
|
947
|
+
const labels = new Map()
|
|
948
|
+
for (const item of items) {
|
|
949
|
+
labels.set(item.label, (labels.get(item.label) || 0) + 1)
|
|
950
|
+
}
|
|
951
|
+
this.updateRedactionCount()
|
|
952
|
+
if (this.reviewFiltersEl) {
|
|
953
|
+
this.reviewFiltersEl.textContent = ''
|
|
954
|
+
const filters = [['all', `All ${items.length}`], ...Array.from(labels.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([label, count]) => [label, `${label} ${count}`])]
|
|
955
|
+
for (const [value, label] of filters) {
|
|
956
|
+
const button = document.createElement('button')
|
|
957
|
+
button.type = 'button'
|
|
958
|
+
button.className = 'logs-redaction-filter'
|
|
959
|
+
button.classList.toggle('is-active', this.activeRedactionFilter === value)
|
|
960
|
+
button.dataset.redactionFilter = value
|
|
961
|
+
button.textContent = label
|
|
962
|
+
button.addEventListener('click', () => {
|
|
963
|
+
this.activeRedactionFilter = value
|
|
964
|
+
this.renderRedactionReview()
|
|
965
|
+
})
|
|
966
|
+
this.reviewFiltersEl.appendChild(button)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (!this.reviewListEl) {
|
|
970
|
+
return
|
|
971
|
+
}
|
|
972
|
+
this.reviewListEl.textContent = ''
|
|
973
|
+
if (!items.length) {
|
|
974
|
+
const empty = document.createElement('div')
|
|
975
|
+
empty.className = 'logs-redaction-empty'
|
|
976
|
+
empty.textContent = this.redactionHasRun ? 'No redactions detected.' : 'Run the privacy filter to review detected items.'
|
|
977
|
+
this.reviewListEl.appendChild(empty)
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
const visibleItems = this.activeRedactionFilter === 'all'
|
|
981
|
+
? items
|
|
982
|
+
: items.filter((item) => item.label === this.activeRedactionFilter)
|
|
983
|
+
if (!visibleItems.length) {
|
|
984
|
+
const empty = document.createElement('div')
|
|
985
|
+
empty.className = 'logs-redaction-empty'
|
|
986
|
+
empty.textContent = 'No redactions match this filter.'
|
|
987
|
+
this.reviewListEl.appendChild(empty)
|
|
988
|
+
return
|
|
989
|
+
}
|
|
990
|
+
for (const item of visibleItems) {
|
|
991
|
+
const rendered = renderedById.get(item.id) || item
|
|
992
|
+
const row = document.createElement('div')
|
|
993
|
+
row.className = 'logs-redaction-row'
|
|
994
|
+
row.dataset.redactionId = item.id
|
|
995
|
+
row.classList.toggle('is-selected', this.selectedRedactionId === item.id)
|
|
996
|
+
row.classList.toggle('is-disabled', !item.enabled)
|
|
997
|
+
row.tabIndex = 0
|
|
998
|
+
row.setAttribute('role', 'button')
|
|
999
|
+
row.setAttribute('aria-current', this.selectedRedactionId === item.id ? 'true' : 'false')
|
|
1000
|
+
row.addEventListener('click', () => this.selectRedaction(item.id, true))
|
|
1001
|
+
row.addEventListener('keydown', (event) => {
|
|
1002
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1003
|
+
event.preventDefault()
|
|
1004
|
+
this.selectRedaction(item.id, true)
|
|
1005
|
+
}
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
const top = document.createElement('div')
|
|
1009
|
+
top.className = 'logs-redaction-row-top'
|
|
1010
|
+
const meta = document.createElement('div')
|
|
1011
|
+
meta.className = 'logs-redaction-row-meta'
|
|
1012
|
+
|
|
1013
|
+
const label = document.createElement('span')
|
|
1014
|
+
label.className = 'logs-redaction-label'
|
|
1015
|
+
label.textContent = item.label
|
|
1016
|
+
meta.appendChild(label)
|
|
1017
|
+
|
|
1018
|
+
const source = document.createElement('span')
|
|
1019
|
+
source.className = 'logs-redaction-source'
|
|
1020
|
+
source.textContent = `${item.source} · line ${item.line}`
|
|
1021
|
+
source.title = source.textContent
|
|
1022
|
+
meta.appendChild(source)
|
|
1023
|
+
|
|
1024
|
+
const toggle = document.createElement('label')
|
|
1025
|
+
toggle.className = 'logs-redaction-toggle'
|
|
1026
|
+
toggle.title = item.enabled ? 'Keep this item masked' : 'Show this item in the copied report'
|
|
1027
|
+
toggle.addEventListener('click', (event) => event.stopPropagation())
|
|
1028
|
+
const checkbox = document.createElement('input')
|
|
1029
|
+
checkbox.type = 'checkbox'
|
|
1030
|
+
checkbox.checked = item.enabled
|
|
1031
|
+
checkbox.setAttribute('aria-label', `${item.enabled ? 'Mask' : 'Show'} ${item.label}`)
|
|
1032
|
+
checkbox.addEventListener('click', (event) => event.stopPropagation())
|
|
1033
|
+
checkbox.addEventListener('change', (event) => {
|
|
1034
|
+
event.stopPropagation()
|
|
1035
|
+
item.enabled = checkbox.checked
|
|
1036
|
+
this.selectedRedactionId = item.id
|
|
1037
|
+
this.renderCurrentReport()
|
|
1038
|
+
})
|
|
1039
|
+
const toggleTrack = document.createElement('span')
|
|
1040
|
+
toggleTrack.className = 'logs-redaction-toggle-track'
|
|
1041
|
+
toggle.appendChild(checkbox)
|
|
1042
|
+
toggle.appendChild(toggleTrack)
|
|
1043
|
+
|
|
1044
|
+
const context = document.createElement('div')
|
|
1045
|
+
context.className = 'logs-redaction-context'
|
|
1046
|
+
context.textContent = rendered.context || ''
|
|
1047
|
+
context.title = rendered.context || ''
|
|
1048
|
+
|
|
1049
|
+
top.appendChild(meta)
|
|
1050
|
+
top.appendChild(toggle)
|
|
1051
|
+
row.appendChild(top)
|
|
1052
|
+
row.appendChild(context)
|
|
1053
|
+
this.reviewListEl.appendChild(row)
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
selectRedaction(id, scrollPreview) {
|
|
1057
|
+
this.selectedRedactionId = id == null ? null : String(id)
|
|
1058
|
+
if (this.outputEl) {
|
|
1059
|
+
this.outputEl.querySelectorAll('.logs-mask-token, .logs-unmasked-token').forEach((node) => {
|
|
1060
|
+
node.classList.toggle('is-selected', node.dataset.redactionId === this.selectedRedactionId)
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
if (this.reviewListEl) {
|
|
1064
|
+
this.reviewListEl.querySelectorAll('.logs-redaction-row').forEach((node) => {
|
|
1065
|
+
const selected = node.dataset.redactionId === this.selectedRedactionId
|
|
1066
|
+
node.classList.toggle('is-selected', selected)
|
|
1067
|
+
node.setAttribute('aria-current', selected ? 'true' : 'false')
|
|
1068
|
+
})
|
|
1069
|
+
}
|
|
1070
|
+
if (!scrollPreview || !this.outputEl || !this.selectedRedactionId) {
|
|
1071
|
+
return
|
|
1072
|
+
}
|
|
1073
|
+
const token = Array.from(this.outputEl.querySelectorAll('.logs-mask-token, .logs-unmasked-token')).find((node) => {
|
|
1074
|
+
return node.dataset.redactionId === this.selectedRedactionId
|
|
1075
|
+
})
|
|
1076
|
+
if (!token) {
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
1079
|
+
const reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
1080
|
+
const outputRect = this.outputEl.getBoundingClientRect()
|
|
1081
|
+
const tokenRect = token.getBoundingClientRect()
|
|
1082
|
+
const nextTop = this.outputEl.scrollTop + tokenRect.top - outputRect.top - Math.round(outputRect.height * 0.35)
|
|
1083
|
+
this.outputEl.scrollTo({
|
|
1084
|
+
top: Math.max(0, nextTop),
|
|
1085
|
+
behavior: reducedMotion ? 'auto' : 'smooth'
|
|
1086
|
+
})
|
|
1087
|
+
}
|
|
1088
|
+
resetRedactionReview(message = 'Filtering has not run yet.') {
|
|
1089
|
+
this.redactionItems = []
|
|
1090
|
+
this.renderedRedactionItems = []
|
|
1091
|
+
this.selectedRedactionId = null
|
|
1092
|
+
this.activeRedactionFilter = 'all'
|
|
1093
|
+
this.updateRedactionCount()
|
|
1094
|
+
if (this.reviewFiltersEl) {
|
|
1095
|
+
this.reviewFiltersEl.textContent = ''
|
|
1096
|
+
}
|
|
1097
|
+
if (this.reviewListEl) {
|
|
1098
|
+
this.reviewListEl.textContent = ''
|
|
1099
|
+
const empty = document.createElement('div')
|
|
1100
|
+
empty.className = 'logs-redaction-empty'
|
|
1101
|
+
empty.textContent = message
|
|
1102
|
+
this.reviewListEl.appendChild(empty)
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
render(payload) {
|
|
1106
|
+
const sections = this.renderSummary(payload)
|
|
1107
|
+
this.rawMarkdown = payload && payload.markdown ? String(payload.markdown) : ''
|
|
1108
|
+
this.ensureSectionModes(sections)
|
|
1109
|
+
this.reviewMarkdown = this.renderDraftMarkdown()
|
|
1110
|
+
this.currentMarkdown = this.reviewMarkdown
|
|
1111
|
+
this.redactionItems = []
|
|
1112
|
+
this.renderedRedactionItems = []
|
|
1113
|
+
this.filterChunks = 0
|
|
1114
|
+
this.redactionHasRun = false
|
|
1115
|
+
this.filtering = false
|
|
1116
|
+
this.selectedRedactionId = null
|
|
1117
|
+
this.activeRedactionFilter = 'all'
|
|
1118
|
+
this.draftTitleEdited = false
|
|
1119
|
+
this.updateDraftTitleSuggestion(true)
|
|
1120
|
+
this.renderCurrentReport()
|
|
1121
|
+
this.resetRedactionReview(this.reviewMarkdown ? 'Run the privacy filter to review detected items.' : 'No report text to review.')
|
|
1122
|
+
this.updateRedactionCount()
|
|
1123
|
+
this.setRunFilterEnabled(Boolean(this.reviewMarkdown))
|
|
1124
|
+
if (!this.reviewMarkdown) {
|
|
1125
|
+
this.setStatus(`Latest snapshot found ${sections.length} log section${sections.length === 1 ? '' : 's'}.`)
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
this.setStatus(`Latest snapshot built from ${sections.length} log section${sections.length === 1 ? '' : 's'}. Run the privacy filter only if you want local redaction review.`)
|
|
1129
|
+
}
|
|
1130
|
+
renderError(message) {
|
|
1131
|
+
if (this.reportSectionsEl) this.reportSectionsEl.textContent = '--'
|
|
1132
|
+
if (this.reportGeneratedEl) this.reportGeneratedEl.textContent = '--'
|
|
1133
|
+
this.clearReportFiles()
|
|
1134
|
+
this.rawMarkdown = ''
|
|
1135
|
+
this.reviewMarkdown = ''
|
|
1136
|
+
this.currentMarkdown = ''
|
|
1137
|
+
this.redactionHasRun = false
|
|
1138
|
+
this.filtering = false
|
|
1139
|
+
this.clearDraftTitle()
|
|
1140
|
+
this.setOutputText(message || 'Unable to build latest log snapshot.')
|
|
1141
|
+
this.resetRedactionReview('No redaction review is available.')
|
|
1142
|
+
this.setCopyEnabled(false)
|
|
1143
|
+
this.setRunFilterEnabled(false)
|
|
1144
|
+
this.updateDraftSizeReview()
|
|
1145
|
+
this.setStatus(message || 'Unable to build latest log snapshot.', true)
|
|
1146
|
+
}
|
|
1147
|
+
renderFilterProgress(progress) {
|
|
1148
|
+
if (!progress || typeof progress !== 'object') {
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1151
|
+
if (progress.type === 'runtime') {
|
|
1152
|
+
this.setStatus(`Loading OpenAI Privacy Filter locally (${progress.device}/${progress.dtype}). First run downloads and caches the model.`)
|
|
1153
|
+
} else if (progress.type === 'fallback') {
|
|
1154
|
+
this.setStatus(progress.message || `Retrying privacy filtering locally with ${progress.device}/${progress.dtype}.`)
|
|
1155
|
+
} else if (progress.type === 'download') {
|
|
1156
|
+
const fileLabel = progress.file ? ` ${progress.file}` : ''
|
|
1157
|
+
if (progress.total) {
|
|
1158
|
+
this.setStatus(`Downloading privacy filter${fileLabel}: ${humanBytes(progress.loaded)} / ${humanBytes(progress.total)}. Cached for future reports.`)
|
|
1159
|
+
} else {
|
|
1160
|
+
this.setStatus(`Downloading privacy filter${fileLabel}. Cached for future reports.`)
|
|
1161
|
+
}
|
|
1162
|
+
} else if (progress.type === 'chunk') {
|
|
1163
|
+
const total = Number(progress.total) || 0
|
|
1164
|
+
const done = Number(progress.done) || 0
|
|
1165
|
+
this.setStatus(total > 0 ? `Filtering locally… ${Math.min(done + 1, total)} / ${total} chunks.` : 'Filtering locally…')
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
renderFilterError(error) {
|
|
1169
|
+
this.redactionItems = []
|
|
1170
|
+
this.renderedRedactionItems = []
|
|
1171
|
+
this.redactionHasRun = false
|
|
1172
|
+
this.currentMarkdown = this.reviewMarkdown
|
|
1173
|
+
this.selectedRedactionId = null
|
|
1174
|
+
this.setCopyEnabled(Boolean(this.currentMarkdown))
|
|
1175
|
+
this.setRunFilterEnabled(Boolean(this.reviewMarkdown))
|
|
1176
|
+
this.setOutputText(this.reviewMarkdown || 'Privacy filter failed. No report text is available.')
|
|
1177
|
+
this.updateDraftSizeReview()
|
|
1178
|
+
this.resetRedactionReview('Privacy filtering failed before any redactions could be reviewed.')
|
|
1179
|
+
this.setStatus(error && error.message ? error.message : 'Privacy filter failed.', true)
|
|
1180
|
+
}
|
|
1181
|
+
async filterReport() {
|
|
1182
|
+
const sourceMarkdown = this.reviewMarkdown || ''
|
|
1183
|
+
if (!sourceMarkdown || this.filtering) {
|
|
1184
|
+
return
|
|
1185
|
+
}
|
|
1186
|
+
const token = this.filterToken + 1
|
|
1187
|
+
this.filterToken = token
|
|
1188
|
+
this.setFiltering(true)
|
|
1189
|
+
this.setCopyEnabled(false)
|
|
1190
|
+
try {
|
|
1191
|
+
const result = await this.privacyFilter.filter(sourceMarkdown, (progress) => {
|
|
1192
|
+
if (token === this.filterToken) {
|
|
1193
|
+
this.renderFilterProgress(progress)
|
|
1194
|
+
}
|
|
1195
|
+
})
|
|
1196
|
+
if (token !== this.filterToken) {
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
this.redactionItems = this.normalizeRedactionItems(result && result.items, sourceMarkdown)
|
|
1200
|
+
this.filterChunks = Number(result && result.chunks) || 1
|
|
1201
|
+
this.redactionHasRun = true
|
|
1202
|
+
this.selectedRedactionId = this.redactionItems.length ? this.redactionItems[0].id : null
|
|
1203
|
+
this.renderCurrentReport()
|
|
1204
|
+
this.selectRedaction(this.selectedRedactionId, false)
|
|
1205
|
+
const maskedCount = this.enabledRedactionCount()
|
|
1206
|
+
this.setStatus(`Privacy filter finished locally. ${maskedCount} item${maskedCount === 1 ? '' : 's'} masked across ${this.filterChunks} chunk${this.filterChunks === 1 ? '' : 's'}.`)
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
if (token === this.filterToken) {
|
|
1209
|
+
this.renderFilterError(error)
|
|
1210
|
+
}
|
|
1211
|
+
} finally {
|
|
1212
|
+
if (token === this.filterToken) {
|
|
1213
|
+
this.setFiltering(false)
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
async load(force = false) {
|
|
1218
|
+
if (!this.reportUrl) {
|
|
1219
|
+
this.renderError('Latest log snapshot is available for app workspaces.')
|
|
1220
|
+
return
|
|
1221
|
+
}
|
|
1222
|
+
if (this.report && !force) {
|
|
1223
|
+
this.renderSummary(this.report)
|
|
1224
|
+
this.rebuildDraftPreview(false)
|
|
1225
|
+
this.setRunFilterEnabled(Boolean(this.reviewMarkdown) && !this.filtering)
|
|
1226
|
+
if (!this.redactionHasRun && this.reviewMarkdown) {
|
|
1227
|
+
this.setStatus('Unfiltered report is ready. Run the privacy filter only if you want local redaction review.')
|
|
1228
|
+
}
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
if (this.loading) return
|
|
1232
|
+
this.loading = true
|
|
1233
|
+
this.setBusy(true)
|
|
1234
|
+
this.setStatus('Building latest log snapshot…')
|
|
1235
|
+
this.filterToken += 1
|
|
1236
|
+
this.rawMarkdown = ''
|
|
1237
|
+
this.reviewMarkdown = ''
|
|
1238
|
+
this.currentMarkdown = ''
|
|
1239
|
+
this.redactionItems = []
|
|
1240
|
+
this.renderedRedactionItems = []
|
|
1241
|
+
this.redactionHasRun = false
|
|
1242
|
+
this.selectedRedactionId = null
|
|
1243
|
+
this.activeRedactionFilter = 'all'
|
|
1244
|
+
this.clearDraftTitle()
|
|
1245
|
+
this.resetRedactionReview('Waiting for latest log snapshot.')
|
|
1246
|
+
this.setCopyEnabled(false)
|
|
1247
|
+
this.setFiltering(false)
|
|
1248
|
+
this.updateDraftSizeReview()
|
|
1249
|
+
try {
|
|
1250
|
+
const response = await fetch(this.rawReportUrl || this.reportUrl, {
|
|
1251
|
+
headers: { 'Accept': 'application/json' },
|
|
1252
|
+
cache: 'no-store'
|
|
1253
|
+
})
|
|
1254
|
+
const payload = await response.json().catch(() => null)
|
|
1255
|
+
if (!response.ok) {
|
|
1256
|
+
throw new Error(payload && payload.error ? payload.error : `HTTP ${response.status}`)
|
|
1257
|
+
}
|
|
1258
|
+
this.report = payload
|
|
1259
|
+
this.render(payload)
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
this.renderError(error && error.message ? error.message : String(error || 'Unknown error'))
|
|
1262
|
+
} finally {
|
|
1263
|
+
this.loading = false
|
|
1264
|
+
this.setBusy(false)
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
registryOrigin(value) {
|
|
1268
|
+
try {
|
|
1269
|
+
return new URL(value || this.registryBase, window.location.origin).origin
|
|
1270
|
+
} catch (_) {
|
|
1271
|
+
return 'https://pinokio.co'
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
registryMessageOrigins() {
|
|
1275
|
+
const origins = new Set([this.registryOrigin(this.registryBase)])
|
|
1276
|
+
if (origins.has('https://pinokio.co')) {
|
|
1277
|
+
origins.add('https://www.pinokio.co')
|
|
1278
|
+
}
|
|
1279
|
+
return origins
|
|
1280
|
+
}
|
|
1281
|
+
async submitDraftImport(token, registry, popup, popupOrigin) {
|
|
1282
|
+
const draft = this.buildDraftImportPayload()
|
|
1283
|
+
if (!draft.metadata.title) {
|
|
1284
|
+
throw new Error('Add a title before posting to Community.')
|
|
1285
|
+
}
|
|
1286
|
+
if (!draft.metadata.body || this.draftOversized || draft.payloadBytes > DRAFT_IMPORT_FIELD_LIMIT_BYTES) {
|
|
1287
|
+
throw new Error('Draft is too large for registry import.')
|
|
1288
|
+
}
|
|
1289
|
+
const form = new FormData()
|
|
1290
|
+
form.append('token', token)
|
|
1291
|
+
form.append('registry', registry || this.registryBase)
|
|
1292
|
+
form.append('metadata_b64', draft.metadataB64)
|
|
1293
|
+
const response = await fetch(this.draftUrl, {
|
|
1294
|
+
method: 'POST',
|
|
1295
|
+
body: form,
|
|
1296
|
+
cache: 'no-store'
|
|
1297
|
+
})
|
|
1298
|
+
const payload = await response.json().catch(() => ({}))
|
|
1299
|
+
if (!response.ok) {
|
|
1300
|
+
throw new Error(payload && payload.error ? payload.error : `Draft import failed (${response.status})`)
|
|
1301
|
+
}
|
|
1302
|
+
const editUrl = payload && payload.editUrl ? String(payload.editUrl) : ''
|
|
1303
|
+
if (popup && !popup.closed) {
|
|
1304
|
+
popup.postMessage({
|
|
1305
|
+
type: 'pinokio:draft-import-result',
|
|
1306
|
+
ok: true,
|
|
1307
|
+
editUrl
|
|
1308
|
+
}, popupOrigin || this.registryOrigin(this.registryBase))
|
|
1309
|
+
}
|
|
1310
|
+
this.setStatus(editUrl ? 'Community draft created. Opening editor.' : 'Community draft created.')
|
|
1311
|
+
return payload
|
|
1312
|
+
}
|
|
1313
|
+
async createDraft() {
|
|
1314
|
+
if (this.importingDraft || this.draftOversized || !this.hasDraftTitle() || !this.currentMarkdown || !this.draftUrl) {
|
|
1315
|
+
this.updateDraftSizeReview()
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
const authorizeUrl = new URL('/draft-import/authorize', this.registryBase)
|
|
1319
|
+
authorizeUrl.searchParams.set('handoff', 'post_message')
|
|
1320
|
+
authorizeUrl.searchParams.set('origin', window.location.origin)
|
|
1321
|
+
authorizeUrl.searchParams.set('wait', '1')
|
|
1322
|
+
this.setCreateDraftBusy(true)
|
|
1323
|
+
this.setStatus('Opening Community authorization…')
|
|
1324
|
+
const popup = window.open(authorizeUrl.toString(), 'pinokio-draft-import', 'width=720,height=760')
|
|
1325
|
+
if (!popup) {
|
|
1326
|
+
this.setCreateDraftBusy(false)
|
|
1327
|
+
this.setStatus('Community authorization window was blocked.', true)
|
|
1328
|
+
return
|
|
1329
|
+
}
|
|
1330
|
+
const registryOrigin = this.registryOrigin(this.registryBase)
|
|
1331
|
+
const registryMessageOrigins = this.registryMessageOrigins()
|
|
1332
|
+
let popupOrigin = registryOrigin
|
|
1333
|
+
let settled = false
|
|
1334
|
+
const finish = (message, isError) => {
|
|
1335
|
+
settled = true
|
|
1336
|
+
window.removeEventListener('message', onMessage)
|
|
1337
|
+
window.clearInterval(closeTimer)
|
|
1338
|
+
this.setCreateDraftBusy(false)
|
|
1339
|
+
if (message) {
|
|
1340
|
+
this.setStatus(message, isError)
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const failPopup = (message) => {
|
|
1344
|
+
if (popup && !popup.closed) {
|
|
1345
|
+
popup.postMessage({
|
|
1346
|
+
type: 'pinokio:draft-import-result',
|
|
1347
|
+
ok: false,
|
|
1348
|
+
error: message
|
|
1349
|
+
}, popupOrigin)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const onMessage = async (event) => {
|
|
1353
|
+
if (settled || event.source !== popup || !registryMessageOrigins.has(event.origin)) {
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
const data = event.data || {}
|
|
1357
|
+
if (!data || data.type !== 'pinokio:draft-import-token') {
|
|
1358
|
+
return
|
|
1359
|
+
}
|
|
1360
|
+
popupOrigin = event.origin
|
|
1361
|
+
const token = typeof data.token === 'string' ? data.token : ''
|
|
1362
|
+
const registry = typeof data.registry === 'string' ? data.registry : this.registryBase
|
|
1363
|
+
if (!token) {
|
|
1364
|
+
failPopup('Community did not return an import token.')
|
|
1365
|
+
finish('Community did not return an import token.', true)
|
|
1366
|
+
return
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
await this.submitDraftImport(token, registry, popup, popupOrigin)
|
|
1370
|
+
finish(null, false)
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
const message = error && error.message ? error.message : 'Draft import failed.'
|
|
1373
|
+
failPopup(message)
|
|
1374
|
+
finish(message, true)
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const closeTimer = window.setInterval(() => {
|
|
1378
|
+
if (!settled && popup.closed) {
|
|
1379
|
+
finish('Community authorization was closed before import.', true)
|
|
1380
|
+
}
|
|
1381
|
+
}, 1000)
|
|
1382
|
+
window.addEventListener('message', onMessage)
|
|
1383
|
+
popup.focus()
|
|
1384
|
+
}
|
|
1385
|
+
async copy() {
|
|
1386
|
+
if (!this.currentMarkdown) return
|
|
1387
|
+
try {
|
|
1388
|
+
await navigator.clipboard.writeText(this.currentMarkdown)
|
|
1389
|
+
this.setStatus(this.redactionHasRun ? 'Reviewed report copied.' : 'Unfiltered report copied.')
|
|
1390
|
+
} catch (_) {
|
|
1391
|
+
if (this.outputEl) {
|
|
1392
|
+
this.outputEl.focus()
|
|
1393
|
+
const selection = window.getSelection()
|
|
1394
|
+
const range = document.createRange()
|
|
1395
|
+
range.selectNodeContents(this.outputEl)
|
|
1396
|
+
selection.removeAllRanges()
|
|
1397
|
+
selection.addRange(range)
|
|
1398
|
+
}
|
|
1399
|
+
this.setStatus('Select the snapshot text to copy.')
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
71
1404
|
class LogsViewer {
|
|
72
1405
|
constructor(options) {
|
|
73
1406
|
this.outputEl = options.outputEl
|
|
@@ -385,6 +1718,8 @@
|
|
|
385
1718
|
this.rootDisplay = config.rootDisplay || ''
|
|
386
1719
|
this.workspace = typeof config.workspace === 'string' ? config.workspace.trim() : ''
|
|
387
1720
|
this.workspaceTitle = config.workspaceTitle || ''
|
|
1721
|
+
this.reportUrl = config.reportUrl || (this.workspace ? `/apps/logs/${encodeURIComponent(this.workspace)}/report` : '')
|
|
1722
|
+
this.initialView = config.initialView === 'latest' && this.reportUrl ? 'latest' : 'raw'
|
|
388
1723
|
this.boundApplyHeight = null
|
|
389
1724
|
this.boundBeforeUnload = null
|
|
390
1725
|
this.headerObserver = null
|
|
@@ -399,6 +1734,7 @@
|
|
|
399
1734
|
this.resizeState = null
|
|
400
1735
|
this.sidebarPreferenceKey = this.workspace ? `${LOGS_SIDEBAR_STORAGE_KEY}:${this.workspace}` : LOGS_SIDEBAR_STORAGE_KEY
|
|
401
1736
|
this.sidebarWidthKey = this.workspace ? `${LOGS_SIDEBAR_WIDTH_KEY}:${this.workspace}` : LOGS_SIDEBAR_WIDTH_KEY
|
|
1737
|
+
this.closeButton = document.getElementById('logs-close-view')
|
|
402
1738
|
const downloadHref = config.downloadUrl || (this.workspace ? `/pinokio/logs.zip?workspace=${encodeURIComponent(this.workspace)}` : '/pinokio/logs.zip')
|
|
403
1739
|
const zipEndpoint = this.workspace ? `/pinokio/log?workspace=${encodeURIComponent(this.workspace)}` : '/pinokio/log'
|
|
404
1740
|
const zipControls = new LogsZipControls({
|
|
@@ -409,6 +1745,28 @@
|
|
|
409
1745
|
defaultDownloadHref: downloadHref
|
|
410
1746
|
})
|
|
411
1747
|
this.zipControls = zipControls
|
|
1748
|
+
this.latestReport = new LogsLatestReport({
|
|
1749
|
+
reportUrl: this.reportUrl,
|
|
1750
|
+
draftUrl: config.draftUrl || '',
|
|
1751
|
+
registryBase: config.registryBase || 'https://pinokio.co',
|
|
1752
|
+
statusEl: document.getElementById('logs-report-status'),
|
|
1753
|
+
outputEl: document.getElementById('logs-report-output'),
|
|
1754
|
+
copyButton: document.getElementById('logs-copy-report'),
|
|
1755
|
+
createDraftButton: document.getElementById('logs-create-draft'),
|
|
1756
|
+
draftTitleInput: document.getElementById('logs-draft-title'),
|
|
1757
|
+
draftTitleNoteEl: document.getElementById('logs-draft-title-note'),
|
|
1758
|
+
runFilterButton: document.getElementById('logs-run-filter'),
|
|
1759
|
+
refreshButton: document.getElementById('logs-refresh-report'),
|
|
1760
|
+
reportFilesEl: document.getElementById('logs-report-files'),
|
|
1761
|
+
reportGeneratedEl: document.getElementById('logs-report-generated'),
|
|
1762
|
+
reportSectionsEl: document.getElementById('logs-report-sections'),
|
|
1763
|
+
draftSizeBadgeEl: document.getElementById('logs-draft-size-badge'),
|
|
1764
|
+
draftMeterFillEl: document.getElementById('logs-draft-meter-fill'),
|
|
1765
|
+
draftStatusEl: document.getElementById('logs-draft-status'),
|
|
1766
|
+
reviewListEl: document.getElementById('logs-redaction-list'),
|
|
1767
|
+
reviewFiltersEl: document.getElementById('logs-redaction-filters'),
|
|
1768
|
+
reviewCountEl: document.getElementById('logs-redaction-count')
|
|
1769
|
+
})
|
|
412
1770
|
this.viewer = new LogsViewer({
|
|
413
1771
|
outputEl: document.getElementById('logs-viewer-output'),
|
|
414
1772
|
statusEl: document.getElementById('logs-viewer-status'),
|
|
@@ -440,12 +1798,82 @@
|
|
|
440
1798
|
}
|
|
441
1799
|
})
|
|
442
1800
|
}
|
|
1801
|
+
this.initCloseButton()
|
|
1802
|
+
this.initViewSwitch()
|
|
443
1803
|
this.initSidebarWidth()
|
|
444
1804
|
this.initSidebarToggle()
|
|
445
1805
|
this.initSidebarResizer()
|
|
446
1806
|
this.setupPaneHeightManagement()
|
|
447
1807
|
}
|
|
448
1808
|
|
|
1809
|
+
initCloseButton() {
|
|
1810
|
+
if (!this.closeButton) {
|
|
1811
|
+
return
|
|
1812
|
+
}
|
|
1813
|
+
this.closeButton.addEventListener('click', (event) => {
|
|
1814
|
+
event.preventDefault()
|
|
1815
|
+
event.stopPropagation()
|
|
1816
|
+
try {
|
|
1817
|
+
window.parent.postMessage({ type: 'pinokio:close-logs', e: 'pinokio:close-logs' }, window.location.origin)
|
|
1818
|
+
} catch (_) {
|
|
1819
|
+
window.parent.postMessage({ type: 'pinokio:close-logs', e: 'pinokio:close-logs' }, '*')
|
|
1820
|
+
}
|
|
1821
|
+
})
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
initViewSwitch() {
|
|
1825
|
+
if (!this.rootElement) {
|
|
1826
|
+
return
|
|
1827
|
+
}
|
|
1828
|
+
this.viewButtons = Array.from(this.rootElement.querySelectorAll('[data-logs-view]'))
|
|
1829
|
+
this.viewButtons.forEach((button) => {
|
|
1830
|
+
button.addEventListener('click', () => {
|
|
1831
|
+
const view = button.dataset.logsView === 'latest' && this.reportUrl ? 'latest' : 'raw'
|
|
1832
|
+
this.setView(view, true)
|
|
1833
|
+
})
|
|
1834
|
+
})
|
|
1835
|
+
this.setView(this.initialView, false)
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
setView(view, updateUrl) {
|
|
1839
|
+
const nextView = view === 'latest' && this.reportUrl ? 'latest' : 'raw'
|
|
1840
|
+
if (this.rootElement) {
|
|
1841
|
+
this.rootElement.dataset.view = nextView
|
|
1842
|
+
}
|
|
1843
|
+
if (Array.isArray(this.viewButtons)) {
|
|
1844
|
+
this.viewButtons.forEach((button) => {
|
|
1845
|
+
const active = button.dataset.logsView === nextView
|
|
1846
|
+
button.classList.toggle('is-active', active)
|
|
1847
|
+
button.setAttribute('aria-selected', active ? 'true' : 'false')
|
|
1848
|
+
button.tabIndex = active ? 0 : -1
|
|
1849
|
+
})
|
|
1850
|
+
}
|
|
1851
|
+
const latestPanel = document.getElementById('logs-latest-panel')
|
|
1852
|
+
const rawPanel = document.getElementById('logs-raw-panel')
|
|
1853
|
+
if (latestPanel) {
|
|
1854
|
+
latestPanel.hidden = nextView !== 'latest'
|
|
1855
|
+
}
|
|
1856
|
+
if (rawPanel) {
|
|
1857
|
+
rawPanel.hidden = nextView !== 'raw'
|
|
1858
|
+
}
|
|
1859
|
+
if (nextView === 'latest') {
|
|
1860
|
+
if (this.viewer) {
|
|
1861
|
+
this.viewer.stop()
|
|
1862
|
+
}
|
|
1863
|
+
if (this.latestReport) {
|
|
1864
|
+
this.latestReport.load(false)
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (updateUrl) {
|
|
1868
|
+
try {
|
|
1869
|
+
const url = new URL(window.location.href)
|
|
1870
|
+
url.searchParams.set('view', nextView === 'latest' ? 'latest' : 'raw')
|
|
1871
|
+
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`)
|
|
1872
|
+
} catch (_) {}
|
|
1873
|
+
}
|
|
1874
|
+
this.applyPaneHeight()
|
|
1875
|
+
}
|
|
1876
|
+
|
|
449
1877
|
initSidebarWidth() {
|
|
450
1878
|
if (!this.rootElement) {
|
|
451
1879
|
return
|