portmap-dev 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- portmap/__init__.py +2 -0
- portmap/__main__.py +5 -0
- portmap/cli.py +40 -0
- portmap/main.py +201 -0
- portmap/scraper.py +131 -0
- portmap/static/index.html +806 -0
- portmap_dev-1.1.0.dist-info/METADATA +194 -0
- portmap_dev-1.1.0.dist-info/RECORD +11 -0
- portmap_dev-1.1.0.dist-info/WHEEL +4 -0
- portmap_dev-1.1.0.dist-info/entry_points.txt +2 -0
- portmap_dev-1.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Portmap</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
9
|
+
<style>
|
|
10
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #080c10;
|
|
14
|
+
--surface: #0e1419;
|
|
15
|
+
--surface2: #141b22;
|
|
16
|
+
--border: #1e2a35;
|
|
17
|
+
--border2: #253040;
|
|
18
|
+
--text: #d4dde6;
|
|
19
|
+
--muted: #4d6070;
|
|
20
|
+
--accent: #00d4ff;
|
|
21
|
+
--accent2: #0099bb;
|
|
22
|
+
--green: #00e676;
|
|
23
|
+
--green-dim: #00c85310;
|
|
24
|
+
--red: #ff4d4d;
|
|
25
|
+
--red-dim: #ff4d4d12;
|
|
26
|
+
--yellow: #ffc400;
|
|
27
|
+
--font-mono: 'IBM Plex Mono', monospace;
|
|
28
|
+
--font-ui: 'Inter', sans-serif;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
body {
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
font-family: var(--font-ui);
|
|
35
|
+
min-height: 100vh;
|
|
36
|
+
padding: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Subtle grid background */
|
|
40
|
+
body::before {
|
|
41
|
+
content: '';
|
|
42
|
+
position: fixed;
|
|
43
|
+
inset: 0;
|
|
44
|
+
background-image:
|
|
45
|
+
linear-gradient(var(--border) 1px, transparent 1px),
|
|
46
|
+
linear-gradient(90deg, var(--border) 1px, transparent 1px);
|
|
47
|
+
background-size: 40px 40px;
|
|
48
|
+
opacity: 0.3;
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
z-index: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.layout {
|
|
54
|
+
position: relative;
|
|
55
|
+
z-index: 1;
|
|
56
|
+
max-width: 960px;
|
|
57
|
+
margin: 0 auto;
|
|
58
|
+
padding: 32px 24px 64px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ββ Header ββ */
|
|
62
|
+
header {
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
align-items: flex-start;
|
|
66
|
+
margin-bottom: 40px;
|
|
67
|
+
padding-bottom: 28px;
|
|
68
|
+
border-bottom: 1px solid var(--border);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.logo-group { display: flex; flex-direction: column; gap: 6px; }
|
|
72
|
+
|
|
73
|
+
.logo {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 10px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.logo-icon {
|
|
80
|
+
width: 28px; height: 28px;
|
|
81
|
+
border: 1.5px solid var(--accent);
|
|
82
|
+
border-radius: 6px;
|
|
83
|
+
display: grid;
|
|
84
|
+
place-items: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.logo-icon svg { width: 14px; height: 14px; }
|
|
88
|
+
|
|
89
|
+
h1 {
|
|
90
|
+
font-family: var(--font-mono);
|
|
91
|
+
font-size: 18px;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
letter-spacing: 0.02em;
|
|
94
|
+
color: #fff;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.tagline {
|
|
98
|
+
font-size: 12px;
|
|
99
|
+
color: var(--muted);
|
|
100
|
+
letter-spacing: 0.01em;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.header-right {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
align-items: flex-end;
|
|
107
|
+
gap: 8px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.live-badge {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 7px;
|
|
114
|
+
padding: 5px 12px;
|
|
115
|
+
background: var(--green-dim);
|
|
116
|
+
border: 1px solid #00e67620;
|
|
117
|
+
border-radius: 20px;
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
font-family: var(--font-mono);
|
|
120
|
+
color: var(--green);
|
|
121
|
+
letter-spacing: 0.04em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.dot {
|
|
125
|
+
width: 6px; height: 6px;
|
|
126
|
+
border-radius: 50%;
|
|
127
|
+
background: var(--green);
|
|
128
|
+
position: relative;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.dot::after {
|
|
132
|
+
content: '';
|
|
133
|
+
position: absolute;
|
|
134
|
+
inset: -3px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
background: var(--green);
|
|
137
|
+
opacity: 0.3;
|
|
138
|
+
animation: pulse 2s ease-in-out infinite;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes pulse {
|
|
142
|
+
0%, 100% { transform: scale(1); opacity: 0.3; }
|
|
143
|
+
50% { transform: scale(1.8); opacity: 0; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.stats-row {
|
|
147
|
+
display: flex;
|
|
148
|
+
gap: 16px;
|
|
149
|
+
align-items: center;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.stat {
|
|
153
|
+
text-align: right;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.stat-value {
|
|
157
|
+
font-family: var(--font-mono);
|
|
158
|
+
font-size: 20px;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
color: #fff;
|
|
161
|
+
line-height: 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.stat-label {
|
|
165
|
+
font-size: 10px;
|
|
166
|
+
color: var(--muted);
|
|
167
|
+
text-transform: uppercase;
|
|
168
|
+
letter-spacing: 0.08em;
|
|
169
|
+
margin-top: 3px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.stat-divider {
|
|
173
|
+
width: 1px;
|
|
174
|
+
height: 28px;
|
|
175
|
+
background: var(--border2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ββ Filter Bar ββ */
|
|
179
|
+
.filter-bar {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
margin-bottom: 20px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.filter-btn {
|
|
187
|
+
padding: 5px 14px;
|
|
188
|
+
border-radius: 20px;
|
|
189
|
+
border: 1px solid var(--border2);
|
|
190
|
+
background: transparent;
|
|
191
|
+
color: var(--muted);
|
|
192
|
+
font-size: 12px;
|
|
193
|
+
font-family: var(--font-ui);
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
transition: all 0.15s;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.filter-btn:hover { border-color: var(--border2); color: var(--text); }
|
|
199
|
+
|
|
200
|
+
.filter-btn.active {
|
|
201
|
+
background: var(--surface2);
|
|
202
|
+
border-color: var(--accent);
|
|
203
|
+
color: var(--accent);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.filter-spacer { flex: 1; }
|
|
207
|
+
|
|
208
|
+
.search-wrap {
|
|
209
|
+
position: relative;
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.search-wrap svg {
|
|
215
|
+
position: absolute;
|
|
216
|
+
left: 10px;
|
|
217
|
+
color: var(--muted);
|
|
218
|
+
width: 13px; height: 13px;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#search {
|
|
223
|
+
background: var(--surface);
|
|
224
|
+
border: 1px solid var(--border2);
|
|
225
|
+
border-radius: 8px;
|
|
226
|
+
color: var(--text);
|
|
227
|
+
font-family: var(--font-mono);
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
padding: 6px 12px 6px 30px;
|
|
230
|
+
width: 200px;
|
|
231
|
+
outline: none;
|
|
232
|
+
transition: border-color 0.15s;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#search:focus { border-color: var(--accent2); }
|
|
236
|
+
#search::placeholder { color: var(--muted); }
|
|
237
|
+
|
|
238
|
+
/* ββ Grid ββ */
|
|
239
|
+
#grid {
|
|
240
|
+
display: grid;
|
|
241
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
242
|
+
gap: 12px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ββ Service Card ββ */
|
|
246
|
+
.card {
|
|
247
|
+
background: var(--surface);
|
|
248
|
+
border: 1px solid var(--border);
|
|
249
|
+
border-radius: 12px;
|
|
250
|
+
padding: 18px 20px;
|
|
251
|
+
display: flex;
|
|
252
|
+
flex-direction: column;
|
|
253
|
+
gap: 14px;
|
|
254
|
+
transition: border-color 0.2s, transform 0.2s;
|
|
255
|
+
animation: cardIn 0.25s ease forwards;
|
|
256
|
+
cursor: default;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@keyframes cardIn {
|
|
260
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
261
|
+
to { opacity: 1; transform: translateY(0); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.card:hover {
|
|
265
|
+
border-color: var(--border2);
|
|
266
|
+
transform: translateY(-1px);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.card.dying {
|
|
270
|
+
opacity: 0.4;
|
|
271
|
+
filter: grayscale(1);
|
|
272
|
+
pointer-events: none;
|
|
273
|
+
transition: opacity 0.4s, filter 0.4s;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* Card top row */
|
|
277
|
+
.card-top {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
justify-content: space-between;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.service-id {
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
gap: 10px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.service-icon {
|
|
290
|
+
width: 34px; height: 34px;
|
|
291
|
+
border-radius: 8px;
|
|
292
|
+
display: grid;
|
|
293
|
+
place-items: center;
|
|
294
|
+
font-size: 16px;
|
|
295
|
+
flex-shrink: 0;
|
|
296
|
+
border: 1px solid transparent;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.service-name {
|
|
300
|
+
font-size: 14px;
|
|
301
|
+
font-weight: 600;
|
|
302
|
+
color: #fff;
|
|
303
|
+
line-height: 1.2;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.service-proc {
|
|
307
|
+
font-family: var(--font-mono);
|
|
308
|
+
font-size: 10px;
|
|
309
|
+
color: var(--muted);
|
|
310
|
+
margin-top: 1px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Status badge */
|
|
314
|
+
.status-badge {
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
gap: 5px;
|
|
318
|
+
padding: 3px 8px;
|
|
319
|
+
border-radius: 20px;
|
|
320
|
+
font-size: 10px;
|
|
321
|
+
font-family: var(--font-mono);
|
|
322
|
+
font-weight: 500;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.status-badge.up {
|
|
326
|
+
background: var(--green-dim);
|
|
327
|
+
border: 1px solid #00e67618;
|
|
328
|
+
color: var(--green);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.status-badge.protected {
|
|
332
|
+
background: #ffffff08;
|
|
333
|
+
border: 1px solid var(--border2);
|
|
334
|
+
color: var(--muted);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.status-dot {
|
|
338
|
+
width: 5px; height: 5px;
|
|
339
|
+
border-radius: 50%;
|
|
340
|
+
background: currentColor;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Card meta row */
|
|
344
|
+
.card-meta {
|
|
345
|
+
display: flex;
|
|
346
|
+
gap: 20px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.meta-item { display: flex; flex-direction: column; gap: 2px; }
|
|
350
|
+
|
|
351
|
+
.meta-label {
|
|
352
|
+
font-size: 9px;
|
|
353
|
+
text-transform: uppercase;
|
|
354
|
+
letter-spacing: 0.08em;
|
|
355
|
+
color: var(--muted);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.meta-value {
|
|
359
|
+
font-family: var(--font-mono);
|
|
360
|
+
font-size: 13px;
|
|
361
|
+
font-weight: 500;
|
|
362
|
+
color: var(--accent);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.meta-value.muted { color: var(--muted); }
|
|
366
|
+
|
|
367
|
+
/* Card bottom row */
|
|
368
|
+
.card-bottom {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
justify-content: space-between;
|
|
372
|
+
padding-top: 10px;
|
|
373
|
+
border-top: 1px solid var(--border);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.open-link {
|
|
377
|
+
display: flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
gap: 5px;
|
|
380
|
+
font-family: var(--font-mono);
|
|
381
|
+
font-size: 11px;
|
|
382
|
+
color: var(--muted);
|
|
383
|
+
text-decoration: none;
|
|
384
|
+
transition: color 0.15s;
|
|
385
|
+
padding: 4px 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.open-link svg { width: 11px; height: 11px; }
|
|
389
|
+
|
|
390
|
+
.open-link:hover { color: var(--accent); }
|
|
391
|
+
|
|
392
|
+
.card-actions { display: flex; gap: 6px; }
|
|
393
|
+
|
|
394
|
+
.btn-kill {
|
|
395
|
+
padding: 5px 14px;
|
|
396
|
+
border-radius: 7px;
|
|
397
|
+
border: 1px solid #ff4d4d30;
|
|
398
|
+
background: var(--red-dim);
|
|
399
|
+
color: var(--red);
|
|
400
|
+
font-size: 11px;
|
|
401
|
+
font-family: var(--font-mono);
|
|
402
|
+
font-weight: 500;
|
|
403
|
+
cursor: pointer;
|
|
404
|
+
transition: all 0.15s;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.btn-kill:hover:not(:disabled) {
|
|
408
|
+
background: #ff4d4d22;
|
|
409
|
+
border-color: #ff4d4d60;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.btn-kill:disabled {
|
|
413
|
+
opacity: 0.5;
|
|
414
|
+
cursor: not-allowed;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Empty state */
|
|
418
|
+
#empty {
|
|
419
|
+
display: none;
|
|
420
|
+
grid-column: 1 / -1;
|
|
421
|
+
text-align: center;
|
|
422
|
+
padding: 64px 0;
|
|
423
|
+
color: var(--muted);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#empty svg { width: 40px; height: 40px; margin-bottom: 12px; opacity: 0.3; }
|
|
427
|
+
#empty p { font-size: 13px; }
|
|
428
|
+
|
|
429
|
+
/* Protected card style */
|
|
430
|
+
.card.protected-card {
|
|
431
|
+
border-style: dashed;
|
|
432
|
+
opacity: 0.6;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.card.protected-card:hover { opacity: 0.7; transform: none; }
|
|
436
|
+
|
|
437
|
+
/* Separator between user/system */
|
|
438
|
+
.section-label {
|
|
439
|
+
grid-column: 1 / -1;
|
|
440
|
+
font-size: 10px;
|
|
441
|
+
text-transform: uppercase;
|
|
442
|
+
letter-spacing: 0.1em;
|
|
443
|
+
color: var(--muted);
|
|
444
|
+
padding: 4px 0;
|
|
445
|
+
border-bottom: 1px solid var(--border);
|
|
446
|
+
margin-bottom: 4px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* Toast */
|
|
450
|
+
#toast {
|
|
451
|
+
position: fixed;
|
|
452
|
+
bottom: 24px;
|
|
453
|
+
right: 24px;
|
|
454
|
+
padding: 10px 18px;
|
|
455
|
+
background: var(--surface2);
|
|
456
|
+
border: 1px solid var(--border2);
|
|
457
|
+
border-radius: 10px;
|
|
458
|
+
font-size: 12px;
|
|
459
|
+
font-family: var(--font-mono);
|
|
460
|
+
color: var(--text);
|
|
461
|
+
z-index: 100;
|
|
462
|
+
transform: translateY(20px);
|
|
463
|
+
opacity: 0;
|
|
464
|
+
transition: all 0.2s;
|
|
465
|
+
pointer-events: none;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.status-badge.unknown {
|
|
469
|
+
background: #ffffff06;
|
|
470
|
+
border: 1px solid var(--border2);
|
|
471
|
+
color: var(--muted);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#toast.show { transform: translateY(0); opacity: 1; }
|
|
475
|
+
#toast.error { border-color: #ff4d4d50; color: var(--red); }
|
|
476
|
+
#toast.success { border-color: #00e67640; color: var(--green); }
|
|
477
|
+
</style>
|
|
478
|
+
</head>
|
|
479
|
+
<body>
|
|
480
|
+
|
|
481
|
+
<div class="layout">
|
|
482
|
+
<header>
|
|
483
|
+
<div class="logo-group">
|
|
484
|
+
<div class="logo">
|
|
485
|
+
<div class="logo-icon">
|
|
486
|
+
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
487
|
+
<circle cx="7" cy="7" r="2.5" stroke="#00d4ff" stroke-width="1.2"/>
|
|
488
|
+
<path d="M7 1v2M7 11v2M1 7h2M11 7h2" stroke="#00d4ff" stroke-width="1.2" stroke-linecap="round"/>
|
|
489
|
+
</svg>
|
|
490
|
+
</div>
|
|
491
|
+
<h1>portmap</h1>
|
|
492
|
+
</div>
|
|
493
|
+
<span class="tagline">Local service monitor Β· Real-time</span>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div class="header-right">
|
|
497
|
+
<div class="live-badge">
|
|
498
|
+
<span class="dot"></span>
|
|
499
|
+
LIVE
|
|
500
|
+
</div>
|
|
501
|
+
<div class="stats-row">
|
|
502
|
+
<div class="stat">
|
|
503
|
+
<div class="stat-value" id="count-active">β</div>
|
|
504
|
+
<div class="stat-label">Active</div>
|
|
505
|
+
</div>
|
|
506
|
+
<div class="stat-divider"></div>
|
|
507
|
+
<div class="stat">
|
|
508
|
+
<div class="stat-value" id="count-protected">β</div>
|
|
509
|
+
<div class="stat-label">Protected</div>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="stat-divider"></div>
|
|
512
|
+
<div class="stat">
|
|
513
|
+
<div class="stat-value" id="count-total">β</div>
|
|
514
|
+
<div class="stat-label">Total</div>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
</header>
|
|
519
|
+
|
|
520
|
+
<div class="filter-bar">
|
|
521
|
+
<button class="filter-btn active" data-filter="all">All</button>
|
|
522
|
+
<button class="filter-btn" data-filter="user">User</button>
|
|
523
|
+
<button class="filter-btn" data-filter="system">System</button>
|
|
524
|
+
<div class="filter-spacer"></div>
|
|
525
|
+
<div class="search-wrap">
|
|
526
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
527
|
+
<circle cx="6.5" cy="6.5" r="4.5"/><path d="M11 11l3 3" stroke-linecap="round"/>
|
|
528
|
+
</svg>
|
|
529
|
+
<input id="search" type="text" placeholder="filter ports, names..." />
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<div id="grid">
|
|
534
|
+
<div id="empty">
|
|
535
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
536
|
+
<path d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-linecap="round"/>
|
|
537
|
+
</svg>
|
|
538
|
+
<p>No services match your filter.</p>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div id="toast"></div>
|
|
544
|
+
|
|
545
|
+
<script>
|
|
546
|
+
// Escape text for safe embedding in HTML attributes
|
|
547
|
+
function escapeAttr(s) {
|
|
548
|
+
return String(s).replace(/&/g, '&').replace(/"/g, '"')
|
|
549
|
+
.replace(/</g, '<').replace(/>/g, '>');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ββ Service metadata (icons + colors) ββ
|
|
553
|
+
const FRAMEWORK_META = {
|
|
554
|
+
'Django': { emoji: 'π’', bg: '#0c4b3310', border: '#0c4b3340', accent: '#44b78b' },
|
|
555
|
+
'Python': { emoji: 'π', bg: '#3572a510', border: '#3572a530', accent: '#3572a5' },
|
|
556
|
+
'FastAPI': { emoji: 'β‘', bg: '#00b09610', border: '#00b09630', accent: '#00b096' },
|
|
557
|
+
'Node.js': { emoji: 'π¦', bg: '#41692110', border: '#41692130', accent: '#8bc34a' },
|
|
558
|
+
'Go': { emoji: 'πΉ', bg: '#00add810', border: '#00add830', accent: '#00add8' },
|
|
559
|
+
'Rust': { emoji: 'π¦', bg: '#de3a1710', border: '#de3a1730', accent: '#de3a17' },
|
|
560
|
+
'Ruby': { emoji: 'π', bg: '#cc342d10', border: '#cc342d30', accent: '#cc342d' },
|
|
561
|
+
'Java': { emoji: 'β', bg: '#b0721310', border: '#b0721330', accent: '#b07213' },
|
|
562
|
+
'Redis': { emoji: 'π΄', bg: '#dc143c10', border: '#dc143c30', accent: '#dc143c' },
|
|
563
|
+
'PostgreSQL': { emoji: 'π', bg: '#33669910', border: '#33669930', accent: '#336699' },
|
|
564
|
+
'MySQL': { emoji: 'π¬', bg: '#0076a310', border: '#0076a330', accent: '#0076a3' },
|
|
565
|
+
'MongoDB': { emoji: 'π', bg: '#00ed6410', border: '#00ed6430', accent: '#00ed64' },
|
|
566
|
+
'Nginx': { emoji: 'π', bg: '#26962510', border: '#26962530', accent: '#269625' },
|
|
567
|
+
'Docker': { emoji: 'π³', bg: '#0db7ed10', border: '#0db7ed30', accent: '#0db7ed' },
|
|
568
|
+
'PHP': { emoji: 'π', bg: '#77719910', border: '#77719930', accent: '#777199' },
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const UNKNOWN_META = { emoji: 'β', bg: '#ffffff06', border: '#ffffff12', accent: '#4d6070' };
|
|
572
|
+
|
|
573
|
+
function getMeta(framework) {
|
|
574
|
+
if (!framework) return UNKNOWN_META;
|
|
575
|
+
return FRAMEWORK_META[framework] || UNKNOWN_META;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ββ State ββ
|
|
579
|
+
let allConnections = [];
|
|
580
|
+
let activeFilter = 'all';
|
|
581
|
+
let searchQuery = '';
|
|
582
|
+
|
|
583
|
+
// ββ WebSocket ββ
|
|
584
|
+
let ws;
|
|
585
|
+
function connect() {
|
|
586
|
+
ws = new WebSocket(`ws://${window.location.host}/ws/scan`);
|
|
587
|
+
ws.onmessage = (e) => {
|
|
588
|
+
allConnections = JSON.parse(e.data);
|
|
589
|
+
render();
|
|
590
|
+
};
|
|
591
|
+
ws.onclose = () => setTimeout(connect, 2000);
|
|
592
|
+
}
|
|
593
|
+
connect();
|
|
594
|
+
|
|
595
|
+
// ββ Filters ββ
|
|
596
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
597
|
+
btn.addEventListener('click', () => {
|
|
598
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
599
|
+
btn.classList.add('active');
|
|
600
|
+
activeFilter = btn.dataset.filter;
|
|
601
|
+
render();
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
document.getElementById('search').addEventListener('input', (e) => {
|
|
606
|
+
searchQuery = e.target.value.toLowerCase();
|
|
607
|
+
render();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ββ Render ββ
|
|
611
|
+
// ββ Render ββ
|
|
612
|
+
function render() {
|
|
613
|
+
const grid = document.getElementById('grid');
|
|
614
|
+
|
|
615
|
+
let filtered = allConnections.filter(conn => {
|
|
616
|
+
if (activeFilter === 'user' && conn.kind === 'system') return false;
|
|
617
|
+
if (activeFilter === 'system' && conn.kind !== 'system') return false;
|
|
618
|
+
if (searchQuery) {
|
|
619
|
+
const q = searchQuery;
|
|
620
|
+
return String(conn.port).includes(q) ||
|
|
621
|
+
conn.name.toLowerCase().includes(q);
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Stats
|
|
627
|
+
const projectCount = allConnections.filter(c => c.kind === 'project' || c.kind === 'infrastructure').length;
|
|
628
|
+
const sysCount = allConnections.filter(c => c.kind === 'system').length;
|
|
629
|
+
|
|
630
|
+
document.getElementById('count-active').textContent = projectCount;
|
|
631
|
+
document.getElementById('count-protected').textContent = sysCount;
|
|
632
|
+
document.getElementById('count-total').textContent = allConnections.length;
|
|
633
|
+
|
|
634
|
+
// Empty state
|
|
635
|
+
const empty = document.getElementById('empty');
|
|
636
|
+
empty.style.display = filtered.length === 0 ? 'block' : 'none';
|
|
637
|
+
|
|
638
|
+
// 2 Sections Layout: User Services (Projects + Unconfirmed) & System
|
|
639
|
+
const userConns = filtered.filter(c => c.kind !== 'system');
|
|
640
|
+
const sysConns = filtered.filter(c => c.kind === 'system');
|
|
641
|
+
|
|
642
|
+
let html = '';
|
|
643
|
+
|
|
644
|
+
if (userConns.length > 0) {
|
|
645
|
+
html += `<div class="section-label">User Services Β· ${userConns.length}</div>`;
|
|
646
|
+
userConns.forEach(conn => { html += buildUserCard(conn); });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (sysConns.length > 0) {
|
|
650
|
+
html += `<div class="section-label">System Β· ${sysConns.length}</div>`;
|
|
651
|
+
sysConns.forEach(conn => { html += buildSystemCard(conn); });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const cards = grid.querySelectorAll('.card, .section-label');
|
|
655
|
+
cards.forEach(el => el.remove());
|
|
656
|
+
grid.insertAdjacentHTML('beforeend', html);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function buildUserCard(conn) {
|
|
660
|
+
const meta = getMeta(conn.framework);
|
|
661
|
+
const localUrl = `http://localhost:${conn.port}`;
|
|
662
|
+
const isProject = conn.kind === 'project';
|
|
663
|
+
|
|
664
|
+
const displayName = conn.name || conn.framework;
|
|
665
|
+
|
|
666
|
+
const subtitle = isProject
|
|
667
|
+
? (conn.framework || `pid ${conn.pid}`)
|
|
668
|
+
: `pid ${conn.pid}`;
|
|
669
|
+
|
|
670
|
+
const cardBorder = conn.framework
|
|
671
|
+
? `border-color: ${meta.border}`
|
|
672
|
+
: 'border-color: var(--border)';
|
|
673
|
+
|
|
674
|
+
return `
|
|
675
|
+
<div class="card" id="card-${conn.pid}"
|
|
676
|
+
data-pid="${conn.pid}"
|
|
677
|
+
data-port="${conn.port}"
|
|
678
|
+
style="${cardBorder}">
|
|
679
|
+
|
|
680
|
+
<div class="card-top">
|
|
681
|
+
<div class="service-id">
|
|
682
|
+
<div class="service-icon"
|
|
683
|
+
style="background:${meta.bg}; border-color:${meta.border}; font-size:15px">
|
|
684
|
+
${meta.emoji}
|
|
685
|
+
</div>
|
|
686
|
+
<div>
|
|
687
|
+
<div class="service-name">${displayName}</div>
|
|
688
|
+
<div class="service-proc">${subtitle}</div>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div class="status-badge ${conn.framework ? 'up' : 'unknown'}">
|
|
693
|
+
<span class="status-dot"></span>
|
|
694
|
+
${conn.framework ? conn.kind.toUpperCase() : 'UNCONFIRMED'}
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
<div class="card-meta">
|
|
699
|
+
<div class="meta-item">
|
|
700
|
+
<span class="meta-label">Port</span>
|
|
701
|
+
<span class="meta-value">:${conn.port}</span>
|
|
702
|
+
</div>
|
|
703
|
+
${conn.cwd ? `
|
|
704
|
+
<div class="meta-item">
|
|
705
|
+
<span class="meta-label">Project</span>
|
|
706
|
+
<span class="meta-value" style="color:var(--muted);font-size:11px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
|
|
707
|
+
title="${conn.cwd}">${conn.cwd}</span>
|
|
708
|
+
</div>` : ''}
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div class="card-bottom">
|
|
712
|
+
<a class="open-link" href="${localUrl}" target="_blank" rel="noopener">
|
|
713
|
+
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor"
|
|
714
|
+
stroke-width="1.5" stroke-linecap="round">
|
|
715
|
+
<path d="M2 10L10 2M5 2h5v5"/>
|
|
716
|
+
</svg>
|
|
717
|
+
localhost:${conn.port}
|
|
718
|
+
</a>
|
|
719
|
+
<div class="card-actions">
|
|
720
|
+
<button class="btn-kill"
|
|
721
|
+
id="kill-${conn.pid}"
|
|
722
|
+
data-name="${escapeAttr(displayName)}"
|
|
723
|
+
onclick="killProcess(${conn.pid}, ${conn.port})">
|
|
724
|
+
Kill
|
|
725
|
+
</button>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
</div>`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function buildSystemCard(conn) {
|
|
732
|
+
return `
|
|
733
|
+
<div class="card protected-card" id="card-sys-${conn.port}">
|
|
734
|
+
<div class="card-top">
|
|
735
|
+
<div class="service-id">
|
|
736
|
+
<div class="service-icon" style="background:#ffffff06;border-color:#ffffff10">π</div>
|
|
737
|
+
<div>
|
|
738
|
+
<div class="service-name" style="color:#4d6070">System Process</div>
|
|
739
|
+
<div class="service-proc">pid protected</div>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
<div class="status-badge protected">
|
|
743
|
+
<span class="status-dot"></span>
|
|
744
|
+
SYS
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
<div class="card-meta">
|
|
748
|
+
<div class="meta-item">
|
|
749
|
+
<span class="meta-label">Port</span>
|
|
750
|
+
<span class="meta-value muted">:${conn.port}</span>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
<div class="card-bottom">
|
|
754
|
+
<span class="open-link" style="cursor:default">
|
|
755
|
+
System protected
|
|
756
|
+
</span>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ββ Kill ββ
|
|
763
|
+
async function killProcess(pid, port) {
|
|
764
|
+
const btn = document.getElementById(`kill-${pid}`);
|
|
765
|
+
const card = document.getElementById(`card-${pid}`);
|
|
766
|
+
|
|
767
|
+
// Require explicit confirmation before sending any kill request.
|
|
768
|
+
const name = (btn && btn.dataset.name) || 'this process';
|
|
769
|
+
if (!confirm(`Kill ${name} (PID ${pid})?`)) {
|
|
770
|
+
return; // Cancelled β no request is sent.
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (btn) { btn.textContent = 'Killingβ¦'; btn.disabled = true; }
|
|
774
|
+
if (card) { card.classList.add('dying'); }
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
const res = await fetch(`/api/kill/${pid}`, { method: 'POST' });
|
|
778
|
+
const data = await res.json();
|
|
779
|
+
if (res.ok) {
|
|
780
|
+
// Display method used for termination
|
|
781
|
+
const method = data.method === 'SIGKILL' ? ' (force killed)' : '';
|
|
782
|
+
showToast(`Port :${port} process terminated${method}`, 'success');
|
|
783
|
+
} else {
|
|
784
|
+
showToast(data.detail || 'Kill failed', 'error');
|
|
785
|
+
if (btn) { btn.textContent = 'Kill'; btn.disabled = false; }
|
|
786
|
+
if (card) { card.classList.remove('dying'); }
|
|
787
|
+
}
|
|
788
|
+
} catch {
|
|
789
|
+
showToast('Network error', 'error');
|
|
790
|
+
if (btn) { btn.textContent = 'Kill'; btn.disabled = false; }
|
|
791
|
+
if (card) { card.classList.remove('dying'); }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ββ Toast ββ
|
|
796
|
+
let toastTimer;
|
|
797
|
+
function showToast(msg, type = '') {
|
|
798
|
+
const t = document.getElementById('toast');
|
|
799
|
+
t.textContent = msg;
|
|
800
|
+
t.className = `show ${type}`;
|
|
801
|
+
clearTimeout(toastTimer);
|
|
802
|
+
toastTimer = setTimeout(() => { t.className = ''; }, 3000);
|
|
803
|
+
}
|
|
804
|
+
</script>
|
|
805
|
+
</body>
|
|
806
|
+
</html>
|