feat: add container manager API, islop router, and container runtime files with vim/bot/d stealth clients

This commit is contained in:
2026-07-06 03:58:46 +00:00
parent 9a8046ab2a
commit 499f91e16a
88 changed files with 21337 additions and 0 deletions
+809
View File
@@ -0,0 +1,809 @@
/* retoor <retoor@molodetz.nl> */
.isslop-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 24px;
align-items: start;
}
.isslop-main {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
.isslop-header h1 {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 8px;
font-size: 1.6rem;
color: var(--text-primary);
}
.isslop-header p {
margin: 0;
color: var(--text-secondary);
max-width: 70ch;
}
.isslop-report-target {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-top: 10px;
}
.isslop-report-target code {
padding: 6px 12px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-secondary);
font-size: 0.88rem;
word-break: break-all;
}
.isslop-report-page section[id] {
scroll-margin-top: 80px;
}
.isslop-side-list {
margin: 0;
padding-left: 18px;
color: var(--text-secondary);
font-size: 0.88rem;
display: flex;
flex-direction: column;
gap: 6px;
}
.isslop-definitions,
.isslop-form-card,
.isslop-history,
.isslop-live-card,
.isslop-score-card,
.isslop-report-body,
.isslop-images,
.isslop-dom-pages,
.isslop-files,
.isslop-badge-panel {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
}
.isslop-definitions-lead {
margin: 0 0 14px;
color: var(--text-primary);
font-size: 1.02rem;
}
.isslop-definition-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.isslop-definition-card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-secondary);
}
.isslop-definition-card p {
margin: 0;
color: var(--text-secondary);
font-size: 0.9rem;
}
.isslop-form-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.isslop-input {
flex: 1;
min-width: 240px;
padding: 10px 14px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-input);
color: var(--text-primary);
}
.isslop-input:focus {
outline: none;
border-color: var(--accent);
}
.isslop-form-error {
margin: 10px 0 0;
color: var(--danger);
font-size: 0.9rem;
}
.isslop-history-title {
margin: 0 0 12px;
font-size: 1.1rem;
color: var(--text-primary);
}
.isslop-history-empty {
margin: 0;
color: var(--text-muted);
}
.isslop-history-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.isslop-history-link {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-secondary);
text-decoration: none;
color: var(--text-primary);
transition: background 0.15s ease;
}
.isslop-history-link:hover {
background: var(--bg-card-hover);
}
.isslop-history-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.isslop-history-source {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.isslop-history-detail {
color: var(--text-secondary);
font-size: 0.85rem;
}
.isslop-grade-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 40px;
border-radius: var(--radius);
font-weight: 700;
font-size: 1.1rem;
flex-shrink: 0;
color: #ffffff;
background: var(--text-muted);
}
.isslop-grade-a { background: #34a853; }
.isslop-grade-b { background: #7cb342; }
.isslop-grade-c { background: #fbbc04; color: #1a1030; }
.isslop-grade-d { background: #fb8c00; }
.isslop-grade-f { background: #ea4335; }
.isslop-live-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.isslop-live-status {
color: var(--text-primary);
font-weight: 600;
}
.isslop-progress {
height: 8px;
border-radius: 4px;
background: var(--bg-secondary);
overflow: hidden;
margin-bottom: 12px;
}
.isslop-progress-bar {
height: 100%;
width: 0;
background: var(--accent-gradient);
transition: width 0.3s ease;
}
.isslop-progress-failed {
background: var(--danger);
}
.isslop-feed {
list-style: none;
margin: 0;
padding: 0;
max-height: 420px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.88rem;
}
.isslop-feed-item {
display: flex;
gap: 8px;
padding: 4px 6px;
border-radius: 6px;
color: var(--text-secondary);
}
.isslop-feed-stage,
.isslop-feed-score {
color: var(--text-primary);
font-weight: 600;
background: var(--bg-secondary);
}
.isslop-feed-signal { color: var(--warning); }
.isslop-feed-error { color: var(--danger); }
.isslop-feed-done { color: var(--success); }
.isslop-feed-loader {
align-items: center;
color: var(--text-muted);
font-family: monospace;
}
.isslop-loader-cursor {
display: inline-block;
width: 8px;
height: 14px;
background: var(--accent);
animation: isslop-blink 1s steps(2, start) infinite;
}
.isslop-loader-text::after {
content: "";
animation: isslop-dots 1.8s steps(4, end) infinite;
}
@keyframes isslop-blink {
to {
visibility: hidden;
}
}
@keyframes isslop-dots {
0% { content: ""; }
25% { content: "."; }
50% { content: ".."; }
75% { content: "..."; }
}
.isslop-grade-pending {
animation: isslop-pulse 1.6s ease-in-out infinite;
}
@keyframes isslop-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
.isslop-feed-icon {
flex-shrink: 0;
}
.isslop-feed-text {
word-break: break-word;
}
.isslop-score-card {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.isslop-gauge {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 108px;
height: 108px;
border-radius: 50%;
background: var(--bg-secondary);
border: 4px solid var(--text-muted);
flex-shrink: 0;
}
.isslop-gauge.isslop-grade-a { border-color: #34a853; background: var(--bg-secondary); }
.isslop-gauge.isslop-grade-b { border-color: #7cb342; background: var(--bg-secondary); }
.isslop-gauge.isslop-grade-c { border-color: #fbbc04; background: var(--bg-secondary); color: var(--text-primary); }
.isslop-gauge.isslop-grade-d { border-color: #fb8c00; background: var(--bg-secondary); }
.isslop-gauge.isslop-grade-f { border-color: #ea4335; background: var(--bg-secondary); }
.isslop-gauge-grade {
font-size: 2rem;
font-weight: 800;
color: var(--text-primary);
}
.isslop-gauge-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.isslop-score-meta {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.isslop-score-meta h2 {
margin: 0;
font-size: 1.15rem;
word-break: break-all;
color: var(--text-primary);
}
.isslop-split {
display: flex;
height: 10px;
border-radius: 5px;
overflow: hidden;
max-width: 420px;
background: var(--bg-secondary);
}
.isslop-split-human {
background: var(--success);
}
.isslop-split-ai {
background: var(--danger);
}
.isslop-facts {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.isslop-chip {
padding: 3px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg-secondary);
color: var(--text-secondary);
font-size: 0.8rem;
}
.isslop-chip-builder {
border-color: rgba(234, 67, 53, 0.6);
background: rgba(234, 67, 53, 0.85);
color: var(--text-primary);
font-weight: 600;
}
.isslop-section-title {
margin: 0 0 12px;
font-size: 1.1rem;
color: var(--text-primary);
}
.isslop-report-body .rendered-content {
color: var(--text-secondary);
}
.isslop-table-wrap {
overflow-x: auto;
}
.isslop-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.isslop-table th,
.isslop-table td {
text-align: left;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
color: var(--text-secondary);
white-space: nowrap;
}
.isslop-table td:first-child {
white-space: normal;
word-break: break-all;
color: var(--text-primary);
}
.isslop-signal-chip {
display: inline-block;
margin: 1px 3px 1px 0;
padding: 1px 8px;
border-radius: 999px;
background: var(--bg-secondary);
border: 1px solid var(--border);
font-size: 0.75rem;
color: var(--text-secondary);
white-space: nowrap;
}
.isslop-signal-strong {
border-color: rgba(234, 67, 53, 0.55);
color: #f28b82;
background: rgba(234, 67, 53, 0.12);
}
.isslop-signal-medium {
border-color: rgba(251, 140, 0, 0.5);
color: #ffb74d;
background: rgba(251, 140, 0, 0.1);
}
.isslop-signal-weak {
border-color: var(--border);
color: var(--text-muted);
}
.isslop-signal-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
margin-left: 5px;
padding: 0 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: inherit;
font-size: 0.68rem;
font-weight: 700;
vertical-align: 1px;
}
.isslop-metric {
display: inline-block;
min-width: 44px;
padding: 2px 9px;
border-radius: 999px;
text-align: center;
font-weight: 700;
font-size: 0.8rem;
}
.isslop-metric-ok {
background: rgba(52, 168, 83, 0.16);
color: #81c995;
}
.isslop-metric-warn {
background: rgba(251, 188, 4, 0.16);
color: #fdd663;
}
.isslop-metric-high {
background: rgba(251, 140, 0, 0.18);
color: #ffb74d;
}
.isslop-metric-critical {
background: rgba(234, 67, 53, 0.18);
color: #f28b82;
}
.isslop-cat {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
border: 1px solid var(--border);
background: var(--bg-secondary);
color: var(--text-secondary);
}
.isslop-cat-human-clean {
border-color: rgba(52, 168, 83, 0.5);
background: rgba(52, 168, 83, 0.14);
color: #81c995;
}
.isslop-cat-human-messy {
border-color: rgba(251, 188, 4, 0.5);
background: rgba(251, 188, 4, 0.12);
color: #fdd663;
}
.isslop-cat-sophisticated-ai {
border-color: rgba(251, 140, 0, 0.5);
background: rgba(251, 140, 0, 0.14);
color: #ffb74d;
}
.isslop-cat-ai-slop {
border-color: rgba(234, 67, 53, 0.5);
background: rgba(234, 67, 53, 0.16);
color: #f28b82;
}
.isslop-image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
}
.isslop-image-card {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-secondary);
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.isslop-image-thumb-wrap {
display: flex;
align-items: center;
justify-content: center;
height: 150px;
margin: -12px -12px 6px;
padding: 10px;
background:
linear-gradient(45deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.04) 75%),
linear-gradient(45deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.04) 75%),
var(--bg-primary);
background-size: 16px 16px, 16px 16px;
background-position: 0 0, 8px 8px;
border-bottom: 1px solid var(--border);
border-radius: var(--radius) var(--radius) 0 0;
overflow: hidden;
}
.isslop-image-thumb {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
border-radius: 4px;
cursor: zoom-in;
}
.isslop-feed-thumb {
height: 28px;
max-width: 72px;
width: auto;
object-fit: contain;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg-primary);
flex-shrink: 0;
align-self: center;
}
.isslop-image-head {
display: flex;
align-items: center;
gap: 8px;
}
.isslop-image-kind {
color: var(--text-primary);
font-weight: 600;
font-size: 0.9rem;
}
.isslop-image-path {
color: var(--text-muted);
font-size: 0.78rem;
word-break: break-all;
}
.isslop-page-url {
margin: 0;
color: var(--text-primary);
font-size: 0.9rem;
font-weight: 600;
word-break: break-all;
}
.isslop-image-desc {
color: var(--text-secondary);
font-size: 0.85rem;
}
.isslop-badge-preview {
margin-bottom: 12px;
}
.isslop-snippet {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.isslop-snippet code {
flex: 1;
padding: 8px 10px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 0.78rem;
overflow-x: auto;
white-space: nowrap;
color: var(--text-secondary);
}
.isslop-report-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.isslop-empty {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 24px;
color: var(--text-secondary);
}
@media (max-width: 900px) {
.isslop-layout {
grid-template-columns: 1fr;
}
}
.isslop-source-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 8px;
}
.isslop-source-scroll {
overflow-x: auto;
}
.isslop-source {
width: 100%;
border-collapse: collapse;
font-family: monospace;
font-size: 0.82rem;
line-height: 1.55;
}
.isslop-source-num {
width: 1%;
min-width: 44px;
padding: 0 12px 0 8px;
text-align: right;
color: var(--text-muted);
user-select: none;
vertical-align: top;
}
.isslop-source-num a {
color: inherit;
text-decoration: none;
}
.isslop-source-num a:hover {
color: var(--accent);
}
.isslop-source-code {
padding: 0 12px;
color: var(--text-secondary);
white-space: pre;
}
.isslop-source-hit .isslop-source-code {
background: rgba(251, 140, 0, 0.08);
border-left: 3px solid var(--warning);
color: var(--text-primary);
}
.isslop-source-focus .isslop-source-code {
background: rgba(255, 107, 53, 0.14);
border-left: 3px solid var(--accent);
}
.isslop-source-line {
scroll-margin-top: 90px;
}
.isslop-source-annotation {
padding: 6px 12px 2px;
}
.isslop-source-annotation .isslop-signal-chip {
white-space: normal;
}
.isslop-source-link {
color: inherit;
text-decoration: underline;
text-decoration-color: var(--border-light);
text-underline-offset: 3px;
}
.isslop-source-link:hover {
color: var(--accent);
}
a.isslop-signal-chip {
text-decoration: none;
cursor: pointer;
}
a.isslop-signal-chip:hover {
border-color: var(--accent);
}
.isslop-source-nav {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
}
.isslop-side-list {
list-style: none;
padding-left: 4px;
}
.isslop-side-list li::before {
content: "🔹 ";
font-size: 0.7rem;
}
.isslop-report-body .rendered-content ul {
list-style: none;
padding-left: 8px;
}
.isslop-report-body .rendered-content ul > li::before {
content: "🔸 ";
font-size: 0.75rem;
}
.isslop-report-body .rendered-content ol > li::marker {
color: var(--accent);
font-weight: 600;
}
@@ -0,0 +1,129 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
import { Http } from "../Http.js";
import { Poller } from "../Poller.js";
const IDLE_INTERVAL_MS = 15000;
const ACTIVE_INTERVAL_MS = 4000;
const ACTIVE_STATES = ["pending", "running"];
export class AppIsslop extends Component {
connectedCallback() {
this._render();
this._form = this.querySelector("[data-isslop-form]");
this._input = this.querySelector("[data-isslop-url]");
this._button = this.querySelector("[data-isslop-run]");
this._error = this.querySelector("[data-isslop-error]");
this._list = this.querySelector("[data-isslop-list]");
this._empty = this.querySelector("[data-isslop-empty]");
this._form.addEventListener("submit", (event) => this._submit(event));
this._poller = new Poller(() => this._refresh(), IDLE_INTERVAL_MS, { pauseHidden: true });
}
disconnectedCallback() {
if (this._poller) this._poller.stop();
}
_render() {
this.innerHTML = `
<section class="isslop-form-card">
<form class="isslop-form" data-isslop-form autocomplete="off" aria-label="Analyze a source">
<div class="isslop-form-row">
<input type="url" name="url" class="isslop-input" data-isslop-url
placeholder="https://github.com/owner/repository or https://example.com"
required aria-required="true" maxlength="2048"
aria-label="Repository or website URL to classify">
<button type="submit" class="btn btn-primary isslop-run-btn" data-isslop-run>Classify</button>
</div>
<p class="isslop-form-error" data-isslop-error hidden role="alert"></p>
</form>
</section>
<section class="isslop-history" aria-label="My analyses">
<h2 class="isslop-history-title">My analyses</h2>
<p class="isslop-history-empty" data-isslop-empty>No analyses yet. Submit a repository or website above.</p>
<ul class="isslop-history-list" data-isslop-list></ul>
</section>
`;
}
async _submit(event) {
event.preventDefault();
const url = this._input.value.trim();
if (!url) return;
this._button.disabled = true;
this._error.hidden = true;
try {
const result = await Http.sendForm("/tools/isslop/run", { url });
window.location.href = result.report_url;
} catch (error) {
this._error.textContent = error.message || "Could not start the analysis.";
this._error.hidden = false;
this._button.disabled = false;
}
}
async _refresh() {
let payload;
try {
payload = await Http.getJson("/tools/isslop/list");
} catch (error) {
return;
}
const analyses = payload.analyses || [];
this._empty.hidden = analyses.length > 0;
this._list.replaceChildren(...analyses.map((row) => this._row(row)));
const busy = analyses.some((row) => ACTIVE_STATES.includes(row.status));
const interval = busy ? ACTIVE_INTERVAL_MS : IDLE_INTERVAL_MS;
if (interval !== this._interval) {
this._interval = interval;
this._poller.stop();
this._poller = new Poller(() => this._refresh(), interval, { pauseHidden: true, immediate: false });
}
}
_row(analysis) {
const item = document.createElement("li");
item.className = "isslop-history-item";
const link = document.createElement("a");
link.className = "isslop-history-link";
link.href = analysis.report_url;
const grade = document.createElement("span");
const gradeValue = analysis.grade || (ACTIVE_STATES.includes(analysis.status) ? "…" : "?");
grade.className = `isslop-grade-badge isslop-grade-${(analysis.grade || "pending").toLowerCase()}`;
grade.textContent = gradeValue;
const meta = document.createElement("span");
meta.className = "isslop-history-meta";
const source = document.createElement("span");
source.className = "isslop-history-source";
source.textContent = analysis.source_url;
const detail = document.createElement("span");
detail.className = "isslop-history-detail";
detail.textContent = this._detailText(analysis);
if (analysis.created_at) {
const when = document.createElement("time");
when.dateTime = analysis.created_at;
when.setAttribute("data-dt", "");
when.setAttribute("data-dt-mode", "ago");
detail.append(" · ", when);
}
meta.append(source, detail);
link.append(grade, meta);
item.appendChild(link);
return item;
}
_detailText(analysis) {
if (analysis.status === "failed") return `failed: ${analysis.error || "unknown error"}`;
if (ACTIVE_STATES.includes(analysis.status)) return analysis.status;
const parts = [];
if (analysis.category) parts.push(analysis.category);
if (analysis.human_percent !== null && analysis.human_percent !== undefined) {
parts.push(`${analysis.human_percent}% human`);
}
if (analysis.files_analyzed) parts.push(`${analysis.files_analyzed} files`);
return parts.join(" · ");
}
}
customElements.define("dp-isslop", AppIsslop);
@@ -0,0 +1,146 @@
// retoor <retoor@molodetz.nl>
import { Component } from "./Component.js";
import { Http } from "../Http.js";
import { Poller } from "../Poller.js";
const POLL_INTERVAL_MS = 3000;
const FEED_CAP = 600;
const RELOAD_DELAY_MS = 1500;
const TERMINAL_KINDS = ["done", "error"];
const KIND_ICONS = {
stage: "🧭",
log: "📋",
file: "📄",
signal: "🚩",
ai: "🤖",
image: "🖼️",
progress: "⏳",
score: "🏁",
report: "📝",
error: "❌",
done: "✅",
status: "📡",
};
const KIND_ICON_FALLBACK = "🔹";
export class AppIsslopRun extends Component {
connectedCallback() {
this._uid = this.attr("uid");
this._topic = this.attr("topic");
this._eventsUrl = this.attr("events-url", `/tools/isslop/${this._uid}/events`);
this._lastSeq = 0;
this._finished = false;
this._render();
this._status = this.querySelector("[data-isslop-status]");
this._bar = this.querySelector("[data-isslop-bar]");
this._feed = this.querySelector("[data-isslop-feed]");
this._subscribed = false;
this._subscribe();
this._poller = new Poller(() => this._poll(), POLL_INTERVAL_MS);
}
disconnectedCallback() {
if (this._poller) this._poller.stop();
if (this._subscribed && window.app && window.app.pubsub) {
window.app.pubsub.unsubscribe(this._topic, this._onFrame);
}
}
_render() {
this.innerHTML = `
<div class="isslop-live-head">
<span class="isslop-live-status" data-isslop-status role="status" aria-live="polite">Waiting for the analyzer…</span>
</div>
<div class="isslop-progress"><div class="isslop-progress-bar" data-isslop-bar></div></div>
<ul class="isslop-feed" data-isslop-feed role="log" aria-live="polite"></ul>
`;
this._loader = document.createElement("li");
this._loader.className = "isslop-feed-item isslop-feed-loader";
this._loader.setAttribute("aria-hidden", "true");
this._loader.innerHTML = '<span class="isslop-loader-cursor"></span><span class="isslop-loader-text">working</span>';
this.querySelector("[data-isslop-feed]").appendChild(this._loader);
}
_subscribe() {
const app = window.app;
if (!this._topic || !app || !app.pubsub || typeof app.pubsub.subscribe !== "function") return;
this._onFrame = (frame) => this._apply(frame);
app.pubsub.subscribe(this._topic, this._onFrame);
this._subscribed = true;
}
async _poll() {
if (this._finished) return;
let payload;
try {
payload = await Http.getJson(`${this._eventsUrl}?after=${this._lastSeq}`);
} catch (error) {
return;
}
(payload.events || []).forEach((event) => this._apply(event));
if (!this._finished && payload.status === "failed" && this._lastSeq === 0) {
this._finish(false, "Analysis failed before producing events.");
}
}
_apply(event) {
if (!event || typeof event.seq !== "number") return;
if (event.seq > 0 && event.seq <= this._lastSeq) return;
if (event.seq > 0) this._lastSeq = event.seq;
if (event.kind === "stage") this._status.textContent = event.message;
if (event.kind === "progress" && event.data && event.data.total) {
const percent = Math.min(100, Math.round((event.data.current / event.data.total) * 100));
this._bar.style.width = `${percent}%`;
}
this._append(event);
if (TERMINAL_KINDS.includes(event.kind)) {
this._finish(event.kind === "done", event.message);
}
}
_append(event) {
const item = document.createElement("li");
item.className = `isslop-feed-item isslop-feed-${event.kind}`;
const icon = document.createElement("span");
icon.className = "isslop-feed-icon";
icon.textContent = KIND_ICONS[event.kind] || KIND_ICON_FALLBACK;
const text = document.createElement("span");
text.className = "isslop-feed-text";
text.textContent = event.message;
item.append(icon, text);
const thumb = event.data ? event.data.thumb : null;
if (thumb && /^[a-f0-9]{16}\.webp$/.test(thumb)) {
const preview = document.createElement("img");
preview.className = "isslop-feed-thumb";
preview.src = `/tools/isslop/${this._uid}/media/${thumb}`;
preview.alt = "";
preview.loading = "lazy";
item.appendChild(preview);
}
this._feed.appendChild(item);
this._feed.appendChild(this._loader);
while (this._feed.children.length > FEED_CAP) {
this._feed.removeChild(this._feed.firstChild);
}
this._feed.scrollTop = this._feed.scrollHeight;
}
_finish(success, message) {
if (this._finished) return;
this._finished = true;
if (this._loader) this._loader.remove();
if (this._poller) this._poller.stop();
this._bar.style.width = "100%";
this._bar.classList.toggle("isslop-progress-failed", !success);
this._status.textContent = success
? "Analysis complete, loading the report…"
: message || "Analysis failed.";
if (success) {
window.setTimeout(() => window.location.reload(), RELOAD_DELAY_MS);
}
}
}
customElements.define("dp-isslop-run", AppIsslopRun);