1 line
56 KiB
JSON
1 line
56 KiB
JSON
{"repo": "antonychd/asimon", "files": {"asimon-agent/asimon/src/asimon/server/static/js/api.js": {"335e56da63c76e706aa8accd45aa42f702893f8d": {"t": 1785746699.7264965, "n": 18391, "h": "ed465b5d7d9be7de", "c": "// Asimon API helpers\nconst AsimonApi = {\n\n async api(method, path, body) {\n const opts = { method, headers: { 'Content-Type': 'application/json' } };\n const token = this.localPrefs.control_token?.trim();\n if (token) opts.headers.Authorization = `Bearer ${token}`;\n if (body) opts.body = JSON.stringify(body);\n const r = await fetch(path, opts);\n if (r.status === 204) return null;\n const data = await r.json();\n if (!r.ok) {\n let msg = `HTTP ${r.status}`;\n if (typeof data.detail === 'string') msg = data.detail;\n else if (Array.isArray(data.detail)) msg = data.detail.map(e => e.msg).join('; ');\n throw new Error(msg);\n }\n return data;\n },\n\n async fetchText(path) {\n const headers = {};\n const token = this.localPrefs.control_token?.trim();\n if (token) headers.Authorization = `Bearer ${token}`;\n const r = await fetch(path, { headers });\n const text = await r.text();\n if (!r.ok) {\n let detail = text;\n try {\n const data = JSON.parse(text);\n detail = data.detail || text;\n } catch {}\n throw new Error(detail || `HTTP ${r.status}`);\n }\n return text;\n },\n\n showToast(msg, type = 'info') {\n this.toast = { show: true, message: msg, type };\n setTimeout(() => this.toast.show = false, 3000);\n },\n\n setLanguage(language) {\n if (!this.translations[language]) return;\n this.language = language;\n localStorage.setItem('asimon-language', language);\n },\n\n async loadProjects() {\n try { this.projects = await this.api('GET', '/projects'); } catch(e) { console.error(e); }\n },\n\n async loadProject(id) {\n try {\n this.project = await this.api('GET', `/projects/${id}`);\n await this.loadChallenges(id, { silent: true });\n } catch(e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async loadSettings() {\n try {\n const s = await this.api('GET', '/settings');\n this.settingsForm.intent_timeout = s.intent_timeout;\n this.settingsForm.reason_timeout = s.reason_timeout;\n } catch(e) { console.error(e); }\n },\n\n async saveServerSettings() {\n try {\n await this.api('PUT', '/settings', this.settingsForm);\n this.showSettings = false;\n this.showToast('Server settings saved');\n } catch(e) { this.showToast(e.message, 'error'); }\n },\n\n async createProject() {\n let projectId = null;\n try {\n const actor = this.actorName();\n const hintContents = this.newProject.hints.filter(h => h.content?.trim());\n const profile = this.buildCtfProjectProfile();\n const body = {\n title: this.newProject.title,\n origin: this.newProject.origin.trim() || profile.origin,\n goal: this.newProject.goal.trim() || profile.goal,\n };\n const hints = hintContents.map(h => ({ content: h.content.trim(), creator: actor }));\n if (hints.length > 0) body.hints = hints;\n const data = await this.api('POST', '/projects', body);\n projectId = data.project.id;\n const challenge = await this.api('POST', `/projects/${projectId}/challenges`, {\n target_url: this.newProject.target_url,\n challenge_type: (this.newProject.challenge_type || 'Web CTF').trim(),\n goal: profile.challengeGoal,\n allowed_hosts: this.parseAllowedHosts(this.newProject.allowed_hosts),\n budget: {\n max_rounds: Number(this.newProject.budget.max_rounds) || 8,\n max_tool_calls: Number(this.newProject.budget.max_tool_calls) || 40,\n max_seconds: Number(this.newProject.budget.max_seconds) || 120,\n },\n });\n this.showNewProject = false;\n this.resetNewProjectForm();\n await this.loadProjects();\n await this.openProject(data.project.id, { tab: 'ctf' });\n await this.selectChallenge(challenge.id);\n await this.loadChallengeWriteup(challenge.id);\n this.showToast('Project created');\n } catch(e) {\n if (projectId) {\n try { await this.api('DELETE', `/projects/${projectId}`); } catch (_) {}\n }\n this.showToast(e.message, 'error');\n }\n },\n\n buildCtfProjectProfile() {\n const target = this.newProject.target_url.trim();\n const challengeType = this.newProject.challenge_type.trim() || 'Web CTF';\n const allowed = this.parseAllowedHosts(this.newProject.allowed_hosts);\n const allowedText = allowed.length ? allowed.join(', ') : this.hostFromUrl(target);\n const origin = [\n `Authorized CTF target is ${target}.`,\n `Target profile: ${challengeType}.`,\n `Allowed hosts: ${allowedText}.`,\n ].join(' ');\n const challengeGoal = `Solve ${challengeType} at ${target}, identify a valid flag if present, and preserve reproducible evidence.`;\n const goal = `Complete the authorized CTF assessment through the project worker, identify a valid flag if present, and record a reproducible evidence path.`;\n return { origin, goal, challengeGoal };\n },\n\n refreshGeneratedProjectFacts() {\n const profile = this.buildCtfProjectProfile();\n this.newProject.origin = profile.origin;\n this.newProject.goal = profile.goal;\n },\n\n resetNewProjectForm() {\n this.newProject = {\n title: '',\n target_url: '',\n challenge_type: '',\n allowed_hosts: [{ value: '' }],\n origin: '',\n goal: '',\n budget: { max_rounds: 8, max_tool_calls: 40, max_seconds: 120 },\n hints: [{ content: '' }],\n base_url: 'https://api.deepseek.com/v1',\n api_key: '',\n };\n },\n\n async toggleProjectStop() {\n if (!this.selectedProjectId || !this.project || this.project.project.status === 'completed') return;\n const nextStatus = this.project.project.status === 'active' ? 'stopped' : 'active';\n try {\n await this.setProjectStatus(this.selectedProjectId, nextStatus);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async toggleProjectStopById(projectId, currentStatus) {\n if (!projectId || currentStatus === 'completed') return;\n const nextStatus = currentStatus === 'active' ? 'stopped' : 'active';\n try {\n await this.setProjectStatus(projectId, nextStatus);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async setProjectStatus(projectId, status, options = {}) {\n const { reload = true, toast = true } = options;\n const updated = await this.api('PUT', `/projects/${projectId}/status`, { status });\n const projectSummary = this.projects.find(item => item.id === projectId);\n if (projectSummary) {\n projectSummary.status = updated.status;\n projectSummary.reason = updated.reason;\n }\n if (this.selectedProjectId === projectId && this.project) {\n this.project.project.status = updated.status;\n this.project.project.reason = updated.reason;\n if (!this.projectIsActive()) {\n this.showIntentModal = false;\n this.showConcludeModal = false;\n this.showCompleteModal = false;\n }\n if (!this.projectCanWriteHints()) {\n this.showHintModal = false;\n }\n }\n if (reload) await this.loadProjects();\n if (toast) this.showToast(status === 'stopped' ? 'Project stopped' : 'Project resumed');\n return updated;\n },\n\n async stopAllActiveProjects() {\n const activeProjects = this.projects.filter(project => project.status === 'active');\n if (activeProjects.length === 0 || this.isStoppingAllProjects) return;\n this.isStoppingAllProjects = true;\n try {\n const results = await Promise.allSettled(\n activeProjects.map(project => this.setProjectStatus(project.id, 'stopped', { reload: false, toast: false }))\n );\n await this.loadProjects();\n const successCount = results.filter(result => result.status === 'fulfilled').length;\n const failureCount = results.length - successCount;\n if (failureCount === 0) {\n this.showToast(`Stopped ${successCount} active project${successCount === 1 ? '' : 's'}`);\n } else {\n this.showToast(`Stopped ${successCount} projects, ${failureCount} failed`, 'error');\n }\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.isStoppingAllProjects = false;\n }\n },\n\n async renameProject() {\n const projectId = this.renameForm.projectId || this.selectedProjectId;\n if (!projectId) return;\n try {\n const updated = await this.api('PUT', `/projects/${projectId}/title`, {\n title: this.renameForm.title,\n });\n const projectSummary = this.projects.find(item => item.id === projectId);\n if (projectSummary) {\n projectSummary.title = updated.title;\n }\n if (this.selectedProjectId === projectId && this.project) {\n this.project.project.title = updated.title;\n }\n this.showRenameModal = false;\n this.renameForm = { projectId: '', originalTitle: '', title: '' };\n await this.loadProjects();\n this.showToast('Project renamed');\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async reopenProject() {\n const projectId = this.reopenForm.projectId || this.selectedProjectId;\n if (!projectId) return;\n try {\n const actor = this.actorName();\n await this.api('POST', `/projects/${projectId}/reopen`, {\n description: this.reopenForm.description,\n creator: actor,\n });\n this.showReopenModal = false;\n this.reopenForm = { projectId: '', projectTitle: '', description: '' };\n await this.loadProjects();\n if (this.selectedProjectId === projectId && this.view === 'graph') {\n await this.loadProject(projectId);\n this.updateGraph();\n }\n this.showToast('Project reopened');\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n deleteProject() {\n if (!this.selectedProjectId) return;\n this.requestDeleteProject(this.selectedProjectId, this.project?.project?.title || '', this.project?.project?.status || '');\n },\n\n deleteProjectById(projectId, projectTitle = '', projectStatus = '') {\n this.requestDeleteProject(projectId, projectTitle, projectStatus);\n },\n\n async confirmDeleteProject() {\n const projectId = this.deleteConfirm.id;\n if (!projectId || this.isDeletingProject) return;\n try {\n this.isDeletingProject = true;\n await this.api('DELETE', `/projects/${projectId}`);\n this.closeDeleteModal(true);\n if (this.selectedProjectId === projectId && this.view === 'graph') {\n this.backToList();\n } else {\n await this.loadProjects();\n }\n this.showToast(`Deleted ${projectId}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.isDeletingProject = false;\n if (!this.showDeleteModal) this.deleteConfirm = { id: '', title: '', status: '' };\n }\n },\n\n requestDeleteProject(projectId, projectTitle = '', projectStatus = '') {\n if (!projectId) return;\n this.deleteConfirm = { id: projectId, title: projectTitle, status: projectStatus };\n this.showDeleteModal = true;\n },\n\n closeDeleteModal(force = false) {\n if (this.isDeletingProject && !force) return;\n this.showDeleteModal = false;\n this.deleteConfirm = { id: '', title: '', status: '' };\n },\n\n async loadChallenges(projectId = this.selectedProjectId, options = {}) {\n if (!projectId) return;\n try {\n this.challenges = await this.api('GET', `/projects/${projectId}/challenges`);\n const stillSelected = this.challenges.some(item => item.id === this.selectedChallengeId);\n if (!stillSelected && this.challenges.length > 0) {\n this.selectedChallengeId = this.challenges[this.challenges.length - 1].id;\n await this.loadChallengeDetail(this.selectedChallengeId, { silent: true });\n if (this.sideTab === 'ctf' && !this.challengeWriteup) await this.loadChallengeWriteup(this.selectedChallengeId);\n } else if (!stillSelected) {\n this.selectedChallengeId = '';\n this.selectedChallengeDetail = null;\n this.challengeWriteup = '';\n } else if (this.sideTab === 'ctf' || this.sideTab === 'tools') {\n await this.loadChallengeDetail(this.selectedChallengeId, { silent: true });\n }\n } catch (e) {\n if (!options.silent) this.showToast(e.message, 'error');\n }\n },\n\n async selectChallenge(challengeId) {\n this.selectedChallengeId = challengeId;\n this.challengeWriteup = '';\n await this.loadChallengeDetail(challengeId);\n if (this.sideTab === 'ctf') await this.loadChallengeWriteup(challengeId);\n },\n\n async loadChallengeDetail(challengeId = this.selectedChallengeId, options = {}) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n this.selectedChallengeDetail = await this.api('GET', `/projects/${this.selectedProjectId}/challenges/${challengeId}`);\n } catch (e) {\n if (!options.silent) this.showToast(e.message, 'error');\n }\n },\n\n async loadChallengeWriteup(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n this.challengeWriteup = await this.fetchText(`/projects/${this.selectedProjectId}/challenges/${challengeId}/writeup`);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async downloadChallengeWriteup(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n const text = this.challengeWriteup || await this.fetchText(`/projects/${this.selectedProjectId}/challenges/${challengeId}/writeup`);\n this.challengeWriteup = text;\n const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `${this.selectedProjectId}-${challengeId}-writeup.md`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async runChallenge(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId || this.challengeLoading) return;\n this.challengeLoading = true;\n try {\n this.selectedChallengeDetail = await this.api('POST', `/projects/${this.selectedProjectId}/challenges/${challengeId}/run`, {\n strategy: this.ctfRunStrategy,\n });\n await this.loadChallenges(this.selectedProjectId, { silent: true });\n await this.loadChallengeWriteup(challengeId);\n this.showToast(`Ran ${challengeId} with ${this.ctfRunStrategy}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.challengeLoading = false;\n }\n },\n\n async createChallengeRun() {\n if (!this.selectedProjectId || this.challengeLoading) return;\n this.challengeLoading = true;\n try {\n const body = {\n target_url: this.ctfForm.target_url,\n goal: this.ctfForm.goal,\n allowed_hosts: this.parseAllowedHosts(this.ctfForm.allowed_hosts),\n budget: {\n max_rounds: Number(this.ctfForm.max_rounds) || 8,\n max_tool_calls: Number(this.ctfForm.max_tool_calls) || 40,\n max_seconds: Number(this.ctfForm.max_seconds) || 120,\n },\n };\n const created = await this.api('POST', `/projects/${this.selectedProjectId}/challenges`, body);\n await this.loadChallenges(this.selectedProjectId);\n await this.selectChallenge(created.id);\n this.showToast(`Created ${created.id}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.challengeLoading = false;\n }\n },\n\n openReopenProject(projectId, projectTitle = '') {\n if (!projectId) return;\n this.reopenForm = { projectId, projectTitle, description: '' };\n this.showReopenModal = true;\n },\n\n openRenameProject(projectId, projectTitle = '') {\n if (!projectId) return;\n this.renameForm = { projectId, originalTitle: projectTitle, title: projectTitle };\n this.showRenameModal = true;\n this.$nextTick(() => {\n this.$refs.renameTitleInput?.focus();\n this.$refs.renameTitleInput?.select?.();\n });\n },\n\n backToList(fromRoute) {\n if (this.replay.active) {\n this.stopReplayTimer();\n this.replay.active = false;\n this.replay.playing = false;\n this.replay.frames = [];\n this.replay.visibleEvents = [];\n this.replay.frameIndex = -1;\n this.replay.sourceProject = null;\n this._timelineEventsCacheProject = null;\n this._timelineEventsCache = [];\n this.polling = true;\n }\n this.view = 'list';\n this.shouldRestoreProjectListScroll = true;\n this.project = null;\n this.selectedProjectId = '';\n this.challenges = [];\n this.selectedChallengeId = '';\n this.selectedChallengeDetail = null;\n this.challengeWriteup = '';\n this.selectedNode = null;\n this.selectedFacts = [];\n this.selectedTimelineEntryId = null;\n this.teardownAutoFit();\n if (this.cy) { this.cy.destroy(); this.cy = null; }\n if (!fromRoute) location.hash = '/';\n this.loadProjects().then(() => this.restoreProjectListScroll());\n },\n\n async openProject(id, options = {}) {\n if (this.replay.active) {\n this.stopReplayTimer();\n this.replay.active = false;\n this.replay.playing = false;\n this.replay.frames = [];\n this.replay.visibleEvents = [];\n this.replay.frameIndex = -1;\n this.replay.sourceProject = null;\n this._timelineEventsCacheProject = null;\n this._timelineEventsCache = [];\n this.polling = true;\n }\n this.rememberProjectListScroll();\n this.selectedProjectId = id;\n this.selectedNode = null;\n this.selectedFacts = [];\n this.selectedTimelineEntryId = null;\n if (options.tab && ['detail', 'timeline', 'hints', 'ctf', 'tools'].includes(options.tab)) this.sideTab = options.tab;\n await this.loadProject(id);\n this.view = 'graph';\n const desiredHash = options.tab && options.tab !== 'detail' ? `#/projects/${id}/${options.tab}` : `#/projects/${id}`;\n if (!options.preserveHash && location.hash !== desiredHash) location.hash = desiredHash.replace(/^#/, '');\n this.$nextTick(() => {\n const clampedWidth = this.clampPanelWidth(this.sidePanelWidth);\n if (clampedWidth !== this.sidePanelWidth) {\n this.sidePanelWidth = clampedWidth;\n this.saveSidePanelWidth();\n }\n this.teardownAutoFit();\n if (this.cy) { this.cy.destroy(); this.cy = null; }\n this.initGraph();\n });\n },\n\n};\n"}, "6d8a4279598c1b5b5bb96497b07e6fea4a0c60b8": {"t": 1785766988.2528212, "n": 18391, "h": "ed465b5d7d9be7de", "c": "// Asimon API helpers\nconst AsimonApi = {\n\n async api(method, path, body) {\n const opts = { method, headers: { 'Content-Type': 'application/json' } };\n const token = this.localPrefs.control_token?.trim();\n if (token) opts.headers.Authorization = `Bearer ${token}`;\n if (body) opts.body = JSON.stringify(body);\n const r = await fetch(path, opts);\n if (r.status === 204) return null;\n const data = await r.json();\n if (!r.ok) {\n let msg = `HTTP ${r.status}`;\n if (typeof data.detail === 'string') msg = data.detail;\n else if (Array.isArray(data.detail)) msg = data.detail.map(e => e.msg).join('; ');\n throw new Error(msg);\n }\n return data;\n },\n\n async fetchText(path) {\n const headers = {};\n const token = this.localPrefs.control_token?.trim();\n if (token) headers.Authorization = `Bearer ${token}`;\n const r = await fetch(path, { headers });\n const text = await r.text();\n if (!r.ok) {\n let detail = text;\n try {\n const data = JSON.parse(text);\n detail = data.detail || text;\n } catch {}\n throw new Error(detail || `HTTP ${r.status}`);\n }\n return text;\n },\n\n showToast(msg, type = 'info') {\n this.toast = { show: true, message: msg, type };\n setTimeout(() => this.toast.show = false, 3000);\n },\n\n setLanguage(language) {\n if (!this.translations[language]) return;\n this.language = language;\n localStorage.setItem('asimon-language', language);\n },\n\n async loadProjects() {\n try { this.projects = await this.api('GET', '/projects'); } catch(e) { console.error(e); }\n },\n\n async loadProject(id) {\n try {\n this.project = await this.api('GET', `/projects/${id}`);\n await this.loadChallenges(id, { silent: true });\n } catch(e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async loadSettings() {\n try {\n const s = await this.api('GET', '/settings');\n this.settingsForm.intent_timeout = s.intent_timeout;\n this.settingsForm.reason_timeout = s.reason_timeout;\n } catch(e) { console.error(e); }\n },\n\n async saveServerSettings() {\n try {\n await this.api('PUT', '/settings', this.settingsForm);\n this.showSettings = false;\n this.showToast('Server settings saved');\n } catch(e) { this.showToast(e.message, 'error'); }\n },\n\n async createProject() {\n let projectId = null;\n try {\n const actor = this.actorName();\n const hintContents = this.newProject.hints.filter(h => h.content?.trim());\n const profile = this.buildCtfProjectProfile();\n const body = {\n title: this.newProject.title,\n origin: this.newProject.origin.trim() || profile.origin,\n goal: this.newProject.goal.trim() || profile.goal,\n };\n const hints = hintContents.map(h => ({ content: h.content.trim(), creator: actor }));\n if (hints.length > 0) body.hints = hints;\n const data = await this.api('POST', '/projects', body);\n projectId = data.project.id;\n const challenge = await this.api('POST', `/projects/${projectId}/challenges`, {\n target_url: this.newProject.target_url,\n challenge_type: (this.newProject.challenge_type || 'Web CTF').trim(),\n goal: profile.challengeGoal,\n allowed_hosts: this.parseAllowedHosts(this.newProject.allowed_hosts),\n budget: {\n max_rounds: Number(this.newProject.budget.max_rounds) || 8,\n max_tool_calls: Number(this.newProject.budget.max_tool_calls) || 40,\n max_seconds: Number(this.newProject.budget.max_seconds) || 120,\n },\n });\n this.showNewProject = false;\n this.resetNewProjectForm();\n await this.loadProjects();\n await this.openProject(data.project.id, { tab: 'ctf' });\n await this.selectChallenge(challenge.id);\n await this.loadChallengeWriteup(challenge.id);\n this.showToast('Project created');\n } catch(e) {\n if (projectId) {\n try { await this.api('DELETE', `/projects/${projectId}`); } catch (_) {}\n }\n this.showToast(e.message, 'error');\n }\n },\n\n buildCtfProjectProfile() {\n const target = this.newProject.target_url.trim();\n const challengeType = this.newProject.challenge_type.trim() || 'Web CTF';\n const allowed = this.parseAllowedHosts(this.newProject.allowed_hosts);\n const allowedText = allowed.length ? allowed.join(', ') : this.hostFromUrl(target);\n const origin = [\n `Authorized CTF target is ${target}.`,\n `Target profile: ${challengeType}.`,\n `Allowed hosts: ${allowedText}.`,\n ].join(' ');\n const challengeGoal = `Solve ${challengeType} at ${target}, identify a valid flag if present, and preserve reproducible evidence.`;\n const goal = `Complete the authorized CTF assessment through the project worker, identify a valid flag if present, and record a reproducible evidence path.`;\n return { origin, goal, challengeGoal };\n },\n\n refreshGeneratedProjectFacts() {\n const profile = this.buildCtfProjectProfile();\n this.newProject.origin = profile.origin;\n this.newProject.goal = profile.goal;\n },\n\n resetNewProjectForm() {\n this.newProject = {\n title: '',\n target_url: '',\n challenge_type: '',\n allowed_hosts: [{ value: '' }],\n origin: '',\n goal: '',\n budget: { max_rounds: 8, max_tool_calls: 40, max_seconds: 120 },\n hints: [{ content: '' }],\n base_url: 'https://api.deepseek.com/v1',\n api_key: '',\n };\n },\n\n async toggleProjectStop() {\n if (!this.selectedProjectId || !this.project || this.project.project.status === 'completed') return;\n const nextStatus = this.project.project.status === 'active' ? 'stopped' : 'active';\n try {\n await this.setProjectStatus(this.selectedProjectId, nextStatus);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async toggleProjectStopById(projectId, currentStatus) {\n if (!projectId || currentStatus === 'completed') return;\n const nextStatus = currentStatus === 'active' ? 'stopped' : 'active';\n try {\n await this.setProjectStatus(projectId, nextStatus);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async setProjectStatus(projectId, status, options = {}) {\n const { reload = true, toast = true } = options;\n const updated = await this.api('PUT', `/projects/${projectId}/status`, { status });\n const projectSummary = this.projects.find(item => item.id === projectId);\n if (projectSummary) {\n projectSummary.status = updated.status;\n projectSummary.reason = updated.reason;\n }\n if (this.selectedProjectId === projectId && this.project) {\n this.project.project.status = updated.status;\n this.project.project.reason = updated.reason;\n if (!this.projectIsActive()) {\n this.showIntentModal = false;\n this.showConcludeModal = false;\n this.showCompleteModal = false;\n }\n if (!this.projectCanWriteHints()) {\n this.showHintModal = false;\n }\n }\n if (reload) await this.loadProjects();\n if (toast) this.showToast(status === 'stopped' ? 'Project stopped' : 'Project resumed');\n return updated;\n },\n\n async stopAllActiveProjects() {\n const activeProjects = this.projects.filter(project => project.status === 'active');\n if (activeProjects.length === 0 || this.isStoppingAllProjects) return;\n this.isStoppingAllProjects = true;\n try {\n const results = await Promise.allSettled(\n activeProjects.map(project => this.setProjectStatus(project.id, 'stopped', { reload: false, toast: false }))\n );\n await this.loadProjects();\n const successCount = results.filter(result => result.status === 'fulfilled').length;\n const failureCount = results.length - successCount;\n if (failureCount === 0) {\n this.showToast(`Stopped ${successCount} active project${successCount === 1 ? '' : 's'}`);\n } else {\n this.showToast(`Stopped ${successCount} projects, ${failureCount} failed`, 'error');\n }\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.isStoppingAllProjects = false;\n }\n },\n\n async renameProject() {\n const projectId = this.renameForm.projectId || this.selectedProjectId;\n if (!projectId) return;\n try {\n const updated = await this.api('PUT', `/projects/${projectId}/title`, {\n title: this.renameForm.title,\n });\n const projectSummary = this.projects.find(item => item.id === projectId);\n if (projectSummary) {\n projectSummary.title = updated.title;\n }\n if (this.selectedProjectId === projectId && this.project) {\n this.project.project.title = updated.title;\n }\n this.showRenameModal = false;\n this.renameForm = { projectId: '', originalTitle: '', title: '' };\n await this.loadProjects();\n this.showToast('Project renamed');\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async reopenProject() {\n const projectId = this.reopenForm.projectId || this.selectedProjectId;\n if (!projectId) return;\n try {\n const actor = this.actorName();\n await this.api('POST', `/projects/${projectId}/reopen`, {\n description: this.reopenForm.description,\n creator: actor,\n });\n this.showReopenModal = false;\n this.reopenForm = { projectId: '', projectTitle: '', description: '' };\n await this.loadProjects();\n if (this.selectedProjectId === projectId && this.view === 'graph') {\n await this.loadProject(projectId);\n this.updateGraph();\n }\n this.showToast('Project reopened');\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n deleteProject() {\n if (!this.selectedProjectId) return;\n this.requestDeleteProject(this.selectedProjectId, this.project?.project?.title || '', this.project?.project?.status || '');\n },\n\n deleteProjectById(projectId, projectTitle = '', projectStatus = '') {\n this.requestDeleteProject(projectId, projectTitle, projectStatus);\n },\n\n async confirmDeleteProject() {\n const projectId = this.deleteConfirm.id;\n if (!projectId || this.isDeletingProject) return;\n try {\n this.isDeletingProject = true;\n await this.api('DELETE', `/projects/${projectId}`);\n this.closeDeleteModal(true);\n if (this.selectedProjectId === projectId && this.view === 'graph') {\n this.backToList();\n } else {\n await this.loadProjects();\n }\n this.showToast(`Deleted ${projectId}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.isDeletingProject = false;\n if (!this.showDeleteModal) this.deleteConfirm = { id: '', title: '', status: '' };\n }\n },\n\n requestDeleteProject(projectId, projectTitle = '', projectStatus = '') {\n if (!projectId) return;\n this.deleteConfirm = { id: projectId, title: projectTitle, status: projectStatus };\n this.showDeleteModal = true;\n },\n\n closeDeleteModal(force = false) {\n if (this.isDeletingProject && !force) return;\n this.showDeleteModal = false;\n this.deleteConfirm = { id: '', title: '', status: '' };\n },\n\n async loadChallenges(projectId = this.selectedProjectId, options = {}) {\n if (!projectId) return;\n try {\n this.challenges = await this.api('GET', `/projects/${projectId}/challenges`);\n const stillSelected = this.challenges.some(item => item.id === this.selectedChallengeId);\n if (!stillSelected && this.challenges.length > 0) {\n this.selectedChallengeId = this.challenges[this.challenges.length - 1].id;\n await this.loadChallengeDetail(this.selectedChallengeId, { silent: true });\n if (this.sideTab === 'ctf' && !this.challengeWriteup) await this.loadChallengeWriteup(this.selectedChallengeId);\n } else if (!stillSelected) {\n this.selectedChallengeId = '';\n this.selectedChallengeDetail = null;\n this.challengeWriteup = '';\n } else if (this.sideTab === 'ctf' || this.sideTab === 'tools') {\n await this.loadChallengeDetail(this.selectedChallengeId, { silent: true });\n }\n } catch (e) {\n if (!options.silent) this.showToast(e.message, 'error');\n }\n },\n\n async selectChallenge(challengeId) {\n this.selectedChallengeId = challengeId;\n this.challengeWriteup = '';\n await this.loadChallengeDetail(challengeId);\n if (this.sideTab === 'ctf') await this.loadChallengeWriteup(challengeId);\n },\n\n async loadChallengeDetail(challengeId = this.selectedChallengeId, options = {}) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n this.selectedChallengeDetail = await this.api('GET', `/projects/${this.selectedProjectId}/challenges/${challengeId}`);\n } catch (e) {\n if (!options.silent) this.showToast(e.message, 'error');\n }\n },\n\n async loadChallengeWriteup(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n this.challengeWriteup = await this.fetchText(`/projects/${this.selectedProjectId}/challenges/${challengeId}/writeup`);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async downloadChallengeWriteup(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n const text = this.challengeWriteup || await this.fetchText(`/projects/${this.selectedProjectId}/challenges/${challengeId}/writeup`);\n this.challengeWriteup = text;\n const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `${this.selectedProjectId}-${challengeId}-writeup.md`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async runChallenge(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId || this.challengeLoading) return;\n this.challengeLoading = true;\n try {\n this.selectedChallengeDetail = await this.api('POST', `/projects/${this.selectedProjectId}/challenges/${challengeId}/run`, {\n strategy: this.ctfRunStrategy,\n });\n await this.loadChallenges(this.selectedProjectId, { silent: true });\n await this.loadChallengeWriteup(challengeId);\n this.showToast(`Ran ${challengeId} with ${this.ctfRunStrategy}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.challengeLoading = false;\n }\n },\n\n async createChallengeRun() {\n if (!this.selectedProjectId || this.challengeLoading) return;\n this.challengeLoading = true;\n try {\n const body = {\n target_url: this.ctfForm.target_url,\n goal: this.ctfForm.goal,\n allowed_hosts: this.parseAllowedHosts(this.ctfForm.allowed_hosts),\n budget: {\n max_rounds: Number(this.ctfForm.max_rounds) || 8,\n max_tool_calls: Number(this.ctfForm.max_tool_calls) || 40,\n max_seconds: Number(this.ctfForm.max_seconds) || 120,\n },\n };\n const created = await this.api('POST', `/projects/${this.selectedProjectId}/challenges`, body);\n await this.loadChallenges(this.selectedProjectId);\n await this.selectChallenge(created.id);\n this.showToast(`Created ${created.id}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.challengeLoading = false;\n }\n },\n\n openReopenProject(projectId, projectTitle = '') {\n if (!projectId) return;\n this.reopenForm = { projectId, projectTitle, description: '' };\n this.showReopenModal = true;\n },\n\n openRenameProject(projectId, projectTitle = '') {\n if (!projectId) return;\n this.renameForm = { projectId, originalTitle: projectTitle, title: projectTitle };\n this.showRenameModal = true;\n this.$nextTick(() => {\n this.$refs.renameTitleInput?.focus();\n this.$refs.renameTitleInput?.select?.();\n });\n },\n\n backToList(fromRoute) {\n if (this.replay.active) {\n this.stopReplayTimer();\n this.replay.active = false;\n this.replay.playing = false;\n this.replay.frames = [];\n this.replay.visibleEvents = [];\n this.replay.frameIndex = -1;\n this.replay.sourceProject = null;\n this._timelineEventsCacheProject = null;\n this._timelineEventsCache = [];\n this.polling = true;\n }\n this.view = 'list';\n this.shouldRestoreProjectListScroll = true;\n this.project = null;\n this.selectedProjectId = '';\n this.challenges = [];\n this.selectedChallengeId = '';\n this.selectedChallengeDetail = null;\n this.challengeWriteup = '';\n this.selectedNode = null;\n this.selectedFacts = [];\n this.selectedTimelineEntryId = null;\n this.teardownAutoFit();\n if (this.cy) { this.cy.destroy(); this.cy = null; }\n if (!fromRoute) location.hash = '/';\n this.loadProjects().then(() => this.restoreProjectListScroll());\n },\n\n async openProject(id, options = {}) {\n if (this.replay.active) {\n this.stopReplayTimer();\n this.replay.active = false;\n this.replay.playing = false;\n this.replay.frames = [];\n this.replay.visibleEvents = [];\n this.replay.frameIndex = -1;\n this.replay.sourceProject = null;\n this._timelineEventsCacheProject = null;\n this._timelineEventsCache = [];\n this.polling = true;\n }\n this.rememberProjectListScroll();\n this.selectedProjectId = id;\n this.selectedNode = null;\n this.selectedFacts = [];\n this.selectedTimelineEntryId = null;\n if (options.tab && ['detail', 'timeline', 'hints', 'ctf', 'tools'].includes(options.tab)) this.sideTab = options.tab;\n await this.loadProject(id);\n this.view = 'graph';\n const desiredHash = options.tab && options.tab !== 'detail' ? `#/projects/${id}/${options.tab}` : `#/projects/${id}`;\n if (!options.preserveHash && location.hash !== desiredHash) location.hash = desiredHash.replace(/^#/, '');\n this.$nextTick(() => {\n const clampedWidth = this.clampPanelWidth(this.sidePanelWidth);\n if (clampedWidth !== this.sidePanelWidth) {\n this.sidePanelWidth = clampedWidth;\n this.saveSidePanelWidth();\n }\n this.teardownAutoFit();\n if (this.cy) { this.cy.destroy(); this.cy = null; }\n this.initGraph();\n });\n },\n\n};\n"}, "22029d3077d764f79ad04b9bd35466f0ec49d6ff": {"t": 1785766989.8332996, "n": 18433, "h": "d387478956a71d25", "c": "// Asimon API helpers\nconst AsimonApi = {\n\n async api(method, path, body) {\n const opts = { method, headers: { 'Content-Type': 'application/json' } };\n const token = this.localPrefs.control_token?.trim();\n if (token) opts.headers.Authorization = `Bearer ${token}`;\n if (body) opts.body = JSON.stringify(body);\n const r = await fetch(path, opts);\n if (r.status === 204) return null;\n const data = await r.json();\n if (!r.ok) {\n let msg = `HTTP ${r.status}`;\n if (typeof data.detail === 'string') msg = data.detail;\n else if (Array.isArray(data.detail)) msg = data.detail.map(e => e.msg).join('; ');\n throw new Error(msg);\n }\n return data;\n },\n\n async fetchText(path) {\n const headers = {};\n const token = this.localPrefs.control_token?.trim();\n if (token) headers.Authorization = `Bearer ${token}`;\n const r = await fetch(path, { headers });\n const text = await r.text();\n if (!r.ok) {\n let detail = text;\n try {\n const data = JSON.parse(text);\n detail = data.detail || text;\n } catch {}\n throw new Error(detail || `HTTP ${r.status}`);\n }\n return text;\n },\n\n showToast(msg, type = 'info') {\n this.toast = { show: true, message: msg, type };\n setTimeout(() => this.toast.show = false, 3000);\n },\n\n setLanguage(language) {\n if (!this.translations[language]) return;\n this.language = language;\n localStorage.setItem('asimon-language', language);\n },\n\n async loadProjects() {\n try { this.projects = await this.api('GET', '/projects'); } catch(e) { console.error(e); }\n },\n\n async loadProject(id) {\n try {\n this.project = await this.api('GET', `/projects/${id}`);\n await this.loadChallenges(id, { silent: true });\n } catch(e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async loadSettings() {\n try {\n const s = await this.api('GET', '/settings');\n this.settingsForm.intent_timeout = s.intent_timeout;\n this.settingsForm.reason_timeout = s.reason_timeout;\n } catch(e) { console.error(e); }\n },\n\n async saveServerSettings() {\n try {\n await this.api('PUT', '/settings', this.settingsForm);\n this.showSettings = false;\n this.showToast('Server settings saved');\n } catch(e) { this.showToast(e.message, 'error'); }\n },\n\n async createProject() {\n let projectId = null;\n try {\n const actor = AsimonUtils.actorName();\n const hintContents = this.newProject.hints.filter(h => h.content?.trim());\n const profile = this.buildCtfProjectProfile();\n const body = {\n title: this.newProject.title,\n origin: this.newProject.origin.trim() || profile.origin,\n goal: this.newProject.goal.trim() || profile.goal,\n };\n const hints = hintContents.map(h => ({ content: h.content.trim(), creator: actor }));\n if (hints.length > 0) body.hints = hints;\n const data = await this.api('POST', '/projects', body);\n projectId = data.project.id;\n const challenge = await this.api('POST', `/projects/${projectId}/challenges`, {\n target_url: this.newProject.target_url,\n challenge_type: (this.newProject.challenge_type || 'Web CTF').trim(),\n goal: profile.challengeGoal,\n allowed_hosts: AsimonUtils.parseAllowedHosts(this.newProject.allowed_hosts),\n budget: {\n max_rounds: Number(this.newProject.budget.max_rounds) || 8,\n max_tool_calls: Number(this.newProject.budget.max_tool_calls) || 40,\n max_seconds: Number(this.newProject.budget.max_seconds) || 120,\n },\n });\n this.showNewProject = false;\n this.resetNewProjectForm();\n await this.loadProjects();\n await this.openProject(data.project.id, { tab: 'ctf' });\n await this.selectChallenge(challenge.id);\n await this.loadChallengeWriteup(challenge.id);\n this.showToast('Project created');\n } catch(e) {\n if (projectId) {\n try { await this.api('DELETE', `/projects/${projectId}`); } catch (_) {}\n }\n this.showToast(e.message, 'error');\n }\n },\n\n buildCtfProjectProfile() {\n const target = this.newProject.target_url.trim();\n const challengeType = this.newProject.challenge_type.trim() || 'Web CTF';\n const allowed = AsimonUtils.parseAllowedHosts(this.newProject.allowed_hosts);\n const allowedText = allowed.length ? allowed.join(', ') : AsimonUtils.hostFromUrl(target);\n const origin = [\n `Authorized CTF target is ${target}.`,\n `Target profile: ${challengeType}.`,\n `Allowed hosts: ${allowedText}.`,\n ].join(' ');\n const challengeGoal = `Solve ${challengeType} at ${target}, identify a valid flag if present, and preserve reproducible evidence.`;\n const goal = `Complete the authorized CTF assessment through the project worker, identify a valid flag if present, and record a reproducible evidence path.`;\n return { origin, goal, challengeGoal };\n },\n\n refreshGeneratedProjectFacts() {\n const profile = this.buildCtfProjectProfile();\n this.newProject.origin = profile.origin;\n this.newProject.goal = profile.goal;\n },\n\n resetNewProjectForm() {\n this.newProject = {\n title: '',\n target_url: '',\n challenge_type: '',\n allowed_hosts: [{ value: '' }],\n origin: '',\n goal: '',\n budget: { max_rounds: 8, max_tool_calls: 40, max_seconds: 120 },\n hints: [{ content: '' }],\n base_url: 'https://api.deepseek.com/v1',\n api_key: '',\n };\n },\n\n async toggleProjectStop() {\n if (!this.selectedProjectId || !this.project || this.project.project.status === 'completed') return;\n const nextStatus = this.project.project.status === 'active' ? 'stopped' : 'active';\n try {\n await this.setProjectStatus(this.selectedProjectId, nextStatus);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async toggleProjectStopById(projectId, currentStatus) {\n if (!projectId || currentStatus === 'completed') return;\n const nextStatus = currentStatus === 'active' ? 'stopped' : 'active';\n try {\n await this.setProjectStatus(projectId, nextStatus);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async setProjectStatus(projectId, status, options = {}) {\n const { reload = true, toast = true } = options;\n const updated = await this.api('PUT', `/projects/${projectId}/status`, { status });\n const projectSummary = this.projects.find(item => item.id === projectId);\n if (projectSummary) {\n projectSummary.status = updated.status;\n projectSummary.reason = updated.reason;\n }\n if (this.selectedProjectId === projectId && this.project) {\n this.project.project.status = updated.status;\n this.project.project.reason = updated.reason;\n if (!this.projectIsActive()) {\n this.showIntentModal = false;\n this.showConcludeModal = false;\n this.showCompleteModal = false;\n }\n if (!this.projectCanWriteHints()) {\n this.showHintModal = false;\n }\n }\n if (reload) await this.loadProjects();\n if (toast) this.showToast(status === 'stopped' ? 'Project stopped' : 'Project resumed');\n return updated;\n },\n\n async stopAllActiveProjects() {\n const activeProjects = this.projects.filter(project => project.status === 'active');\n if (activeProjects.length === 0 || this.isStoppingAllProjects) return;\n this.isStoppingAllProjects = true;\n try {\n const results = await Promise.allSettled(\n activeProjects.map(project => this.setProjectStatus(project.id, 'stopped', { reload: false, toast: false }))\n );\n await this.loadProjects();\n const successCount = results.filter(result => result.status === 'fulfilled').length;\n const failureCount = results.length - successCount;\n if (failureCount === 0) {\n this.showToast(`Stopped ${successCount} active project${successCount === 1 ? '' : 's'}`);\n } else {\n this.showToast(`Stopped ${successCount} projects, ${failureCount} failed`, 'error');\n }\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.isStoppingAllProjects = false;\n }\n },\n\n async renameProject() {\n const projectId = this.renameForm.projectId || this.selectedProjectId;\n if (!projectId) return;\n try {\n const updated = await this.api('PUT', `/projects/${projectId}/title`, {\n title: this.renameForm.title,\n });\n const projectSummary = this.projects.find(item => item.id === projectId);\n if (projectSummary) {\n projectSummary.title = updated.title;\n }\n if (this.selectedProjectId === projectId && this.project) {\n this.project.project.title = updated.title;\n }\n this.showRenameModal = false;\n this.renameForm = { projectId: '', originalTitle: '', title: '' };\n await this.loadProjects();\n this.showToast('Project renamed');\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async reopenProject() {\n const projectId = this.reopenForm.projectId || this.selectedProjectId;\n if (!projectId) return;\n try {\n const actor = AsimonUtils.actorName();\n await this.api('POST', `/projects/${projectId}/reopen`, {\n description: this.reopenForm.description,\n creator: actor,\n });\n this.showReopenModal = false;\n this.reopenForm = { projectId: '', projectTitle: '', description: '' };\n await this.loadProjects();\n if (this.selectedProjectId === projectId && this.view === 'graph') {\n await this.loadProject(projectId);\n this.updateGraph();\n }\n this.showToast('Project reopened');\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n deleteProject() {\n if (!this.selectedProjectId) return;\n this.requestDeleteProject(this.selectedProjectId, this.project?.project?.title || '', this.project?.project?.status || '');\n },\n\n deleteProjectById(projectId, projectTitle = '', projectStatus = '') {\n this.requestDeleteProject(projectId, projectTitle, projectStatus);\n },\n\n async confirmDeleteProject() {\n const projectId = this.deleteConfirm.id;\n if (!projectId || this.isDeletingProject) return;\n try {\n this.isDeletingProject = true;\n await this.api('DELETE', `/projects/${projectId}`);\n this.closeDeleteModal(true);\n if (this.selectedProjectId === projectId && this.view === 'graph') {\n this.backToList();\n } else {\n await this.loadProjects();\n }\n this.showToast(`Deleted ${projectId}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.isDeletingProject = false;\n if (!this.showDeleteModal) this.deleteConfirm = { id: '', title: '', status: '' };\n }\n },\n\n requestDeleteProject(projectId, projectTitle = '', projectStatus = '') {\n if (!projectId) return;\n this.deleteConfirm = { id: projectId, title: projectTitle, status: projectStatus };\n this.showDeleteModal = true;\n },\n\n closeDeleteModal(force = false) {\n if (this.isDeletingProject && !force) return;\n this.showDeleteModal = false;\n this.deleteConfirm = { id: '', title: '', status: '' };\n },\n\n async loadChallenges(projectId = this.selectedProjectId, options = {}) {\n if (!projectId) return;\n try {\n this.challenges = await this.api('GET', `/projects/${projectId}/challenges`);\n const stillSelected = this.challenges.some(item => item.id === this.selectedChallengeId);\n if (!stillSelected && this.challenges.length > 0) {\n this.selectedChallengeId = this.challenges[this.challenges.length - 1].id;\n await this.loadChallengeDetail(this.selectedChallengeId, { silent: true });\n if (this.sideTab === 'ctf' && !this.challengeWriteup) await this.loadChallengeWriteup(this.selectedChallengeId);\n } else if (!stillSelected) {\n this.selectedChallengeId = '';\n this.selectedChallengeDetail = null;\n this.challengeWriteup = '';\n } else if (this.sideTab === 'ctf' || this.sideTab === 'tools') {\n await this.loadChallengeDetail(this.selectedChallengeId, { silent: true });\n }\n } catch (e) {\n if (!options.silent) this.showToast(e.message, 'error');\n }\n },\n\n async selectChallenge(challengeId) {\n this.selectedChallengeId = challengeId;\n this.challengeWriteup = '';\n await this.loadChallengeDetail(challengeId);\n if (this.sideTab === 'ctf') await this.loadChallengeWriteup(challengeId);\n },\n\n async loadChallengeDetail(challengeId = this.selectedChallengeId, options = {}) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n this.selectedChallengeDetail = await this.api('GET', `/projects/${this.selectedProjectId}/challenges/${challengeId}`);\n } catch (e) {\n if (!options.silent) this.showToast(e.message, 'error');\n }\n },\n\n async loadChallengeWriteup(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n this.challengeWriteup = await this.fetchText(`/projects/${this.selectedProjectId}/challenges/${challengeId}/writeup`);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async downloadChallengeWriteup(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId) return;\n try {\n const text = this.challengeWriteup || await this.fetchText(`/projects/${this.selectedProjectId}/challenges/${challengeId}/writeup`);\n this.challengeWriteup = text;\n const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `${this.selectedProjectId}-${challengeId}-writeup.md`;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n } catch (e) {\n this.showToast(e.message, 'error');\n }\n },\n\n async runChallenge(challengeId = this.selectedChallengeId) {\n if (!this.selectedProjectId || !challengeId || this.challengeLoading) return;\n this.challengeLoading = true;\n try {\n this.selectedChallengeDetail = await this.api('POST', `/projects/${this.selectedProjectId}/challenges/${challengeId}/run`, {\n strategy: this.ctfRunStrategy,\n });\n await this.loadChallenges(this.selectedProjectId, { silent: true });\n await this.loadChallengeWriteup(challengeId);\n this.showToast(`Ran ${challengeId} with ${this.ctfRunStrategy}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.challengeLoading = false;\n }\n },\n\n async createChallengeRun() {\n if (!this.selectedProjectId || this.challengeLoading) return;\n this.challengeLoading = true;\n try {\n const body = {\n target_url: this.ctfForm.target_url,\n goal: this.ctfForm.goal,\n allowed_hosts: AsimonUtils.parseAllowedHosts(this.ctfForm.allowed_hosts),\n budget: {\n max_rounds: Number(this.ctfForm.max_rounds) || 8,\n max_tool_calls: Number(this.ctfForm.max_tool_calls) || 40,\n max_seconds: Number(this.ctfForm.max_seconds) || 120,\n },\n };\n const created = await this.api('POST', `/projects/${this.selectedProjectId}/challenges`, body);\n await this.loadChallenges(this.selectedProjectId);\n await this.selectChallenge(created.id);\n this.showToast(`Created ${created.id}`);\n } catch (e) {\n this.showToast(e.message, 'error');\n } finally {\n this.challengeLoading = false;\n }\n },\n\n openReopenProject(projectId, projectTitle = '') {\n if (!projectId) return;\n this.reopenForm = { projectId, projectTitle, description: '' };\n this.showReopenModal = true;\n },\n\n openRenameProject(projectId, projectTitle = '') {\n if (!projectId) return;\n this.renameForm = { projectId, originalTitle: projectTitle, title: projectTitle };\n this.showRenameModal = true;\n this.$nextTick(() => {\n this.$refs.renameTitleInput?.focus();\n this.$refs.renameTitleInput?.select?.();\n });\n },\n\n backToList(fromRoute) {\n if (this.replay.active) {\n this.stopReplayTimer();\n this.replay.active = false;\n this.replay.playing = false;\n this.replay.frames = [];\n this.replay.visibleEvents = [];\n this.replay.frameIndex = -1;\n this.replay.sourceProject = null;\n this._timelineEventsCacheProject = null;\n this._timelineEventsCache = [];\n this.polling = true;\n }\n this.view = 'list';\n this.shouldRestoreProjectListScroll = true;\n this.project = null;\n this.selectedProjectId = '';\n this.challenges = [];\n this.selectedChallengeId = '';\n this.selectedChallengeDetail = null;\n this.challengeWriteup = '';\n this.selectedNode = null;\n this.selectedFacts = [];\n this.selectedTimelineEntryId = null;\n this.teardownAutoFit();\n if (this.cy) { this.cy.destroy(); this.cy = null; }\n if (!fromRoute) location.hash = '/';\n this.loadProjects().then(() => this.restoreProjectListScroll());\n },\n\n async openProject(id, options = {}) {\n if (this.replay.active) {\n this.stopReplayTimer();\n this.replay.active = false;\n this.replay.playing = false;\n this.replay.frames = [];\n this.replay.visibleEvents = [];\n this.replay.frameIndex = -1;\n this.replay.sourceProject = null;\n this._timelineEventsCacheProject = null;\n this._timelineEventsCache = [];\n this.polling = true;\n }\n this.rememberProjectListScroll();\n this.selectedProjectId = id;\n this.selectedNode = null;\n this.selectedFacts = [];\n this.selectedTimelineEntryId = null;\n if (options.tab && ['detail', 'timeline', 'hints', 'ctf', 'tools'].includes(options.tab)) this.sideTab = options.tab;\n await this.loadProject(id);\n this.view = 'graph';\n const desiredHash = options.tab && options.tab !== 'detail' ? `#/projects/${id}/${options.tab}` : `#/projects/${id}`;\n if (!options.preserveHash && location.hash !== desiredHash) location.hash = desiredHash.replace(/^#/, '');\n this.$nextTick(() => {\n const clampedWidth = this.clampPanelWidth(this.sidePanelWidth);\n if (clampedWidth !== this.sidePanelWidth) {\n this.sidePanelWidth = clampedWidth;\n this.saveSidePanelWidth();\n }\n this.teardownAutoFit();\n if (this.cy) { this.cy.destroy(); this.cy = null; }\n this.initGraph();\n });\n },\n\n};\n"}}}} |