forked from retoor/devplacepy
feat: add container manager API, islop router, and container runtime files with vim/bot/d stealth clients
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user