@imenam/simple-scraper 1.0.24 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +444 -398
- package/dist/browser.js +40 -11
- package/dist/cli/setup-mcp.js +2 -2
- package/dist/cookie-sync.js +67 -0
- package/dist/cookies.js +28 -40
- package/dist/data-dir.js +24 -0
- package/dist/gui.js +83 -85
- package/dist/index.js +36 -38
- package/dist/keep-alive.js +51 -0
- package/dist/login.js +157 -0
- package/dist/relay.js +11 -7
- package/dist/remote-input.js +67 -0
- package/dist/rpc.js +184 -0
- package/dist/sessions.js +23 -11
- package/dist/signal-picker.js +144 -0
- package/dist/site-check.js +88 -0
- package/dist/site-store.js +210 -0
- package/package.json +2 -1
- package/public/dist/assets/index-BAs4dkz0.css +1 -0
- package/public/dist/assets/index-D44p8v3x.js +11 -0
- package/public/dist/index.html +13 -13
- package/public/dist/assets/index-DAjr-PsH.js +0 -12
- package/public/dist/assets/index-IkSxq6_f.css +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getDataDir } from './data-dir.js';
|
|
4
|
+
const MAX_EVENTS = 1000;
|
|
5
|
+
/** « 1 cookie enregistré », « 18 cookies enregistrés ». */
|
|
6
|
+
export function cookieCountLabel(count, participle) {
|
|
7
|
+
return count > 1 ? `${count} cookies ${participle}s` : `${count} cookie ${participle}`;
|
|
8
|
+
}
|
|
9
|
+
const SITE_ID_RE = /^site_[a-z0-9]{6}$/;
|
|
10
|
+
export function isSiteId(value) {
|
|
11
|
+
return SITE_ID_RE.test(value);
|
|
12
|
+
}
|
|
13
|
+
/** L'hôte appartient au domaine, ou à l'un de ses sous-domaines. */
|
|
14
|
+
export function hostMatches(host, domain) {
|
|
15
|
+
return host === domain || host.endsWith('.' + domain);
|
|
16
|
+
}
|
|
17
|
+
/** Le cookie s'applique à l'un des domaines, ou l'un des domaines l'englobe. */
|
|
18
|
+
export function cookieInScope(cookieDomain, scope) {
|
|
19
|
+
const d = cookieDomain.replace(/^\./, '').toLowerCase();
|
|
20
|
+
return scope.some(s => s === d || s.endsWith('.' + d) || d.endsWith('.' + s));
|
|
21
|
+
}
|
|
22
|
+
/** Cookie réduit aux champs acceptés par `setCookie`, sans date pour un cookie de session. */
|
|
23
|
+
export function toCookieData(cookie) {
|
|
24
|
+
const data = {
|
|
25
|
+
name: cookie.name,
|
|
26
|
+
value: cookie.value,
|
|
27
|
+
domain: cookie.domain,
|
|
28
|
+
path: cookie.path ?? '/',
|
|
29
|
+
secure: cookie.secure ?? false,
|
|
30
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
31
|
+
};
|
|
32
|
+
if (cookie.sameSite)
|
|
33
|
+
data.sameSite = cookie.sameSite;
|
|
34
|
+
if (typeof cookie.expires === 'number' && cookie.expires > 0)
|
|
35
|
+
data.expires = cookie.expires;
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
function isExpired(cookie, nowSec) {
|
|
39
|
+
return typeof cookie.expires === 'number' && cookie.expires > 0 && cookie.expires < nowSec;
|
|
40
|
+
}
|
|
41
|
+
function cookieKey(c) {
|
|
42
|
+
return `${c.domain}\t${c.path ?? '/'}\t${c.name}`;
|
|
43
|
+
}
|
|
44
|
+
function writeJson(file, value) {
|
|
45
|
+
const tmp = `${file}.tmp`;
|
|
46
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf-8');
|
|
47
|
+
fs.renameSync(tmp, file);
|
|
48
|
+
}
|
|
49
|
+
function readJson(file, empty) {
|
|
50
|
+
if (!fs.existsSync(file))
|
|
51
|
+
return empty;
|
|
52
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
53
|
+
}
|
|
54
|
+
function generateSiteId() {
|
|
55
|
+
return 'site_' + Math.random().toString(36).slice(2, 8).padEnd(6, '0');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sites d'un dossier de données : `sites.json` (réglages et état), `events.json`
|
|
59
|
+
* (historique borné) et `cookies/<id>.json` (cookies de chaque site). Le process
|
|
60
|
+
* principal en est le seul lecteur et le seul écrivain ; tout est gardé en mémoire
|
|
61
|
+
* et réécrit à chaque modification.
|
|
62
|
+
*/
|
|
63
|
+
export class SiteStore {
|
|
64
|
+
dir;
|
|
65
|
+
sitesFile;
|
|
66
|
+
eventsFile;
|
|
67
|
+
cookiesDir;
|
|
68
|
+
sites;
|
|
69
|
+
events;
|
|
70
|
+
jars = new Map();
|
|
71
|
+
jarUpdatedAt = new Map();
|
|
72
|
+
constructor(dir) {
|
|
73
|
+
this.dir = dir;
|
|
74
|
+
this.sitesFile = path.join(dir, 'sites.json');
|
|
75
|
+
this.eventsFile = path.join(dir, 'events.json');
|
|
76
|
+
this.cookiesDir = path.join(dir, 'cookies');
|
|
77
|
+
fs.mkdirSync(this.cookiesDir, { recursive: true });
|
|
78
|
+
this.sites = readJson(this.sitesFile, { sites: [] }).sites;
|
|
79
|
+
this.events = readJson(this.eventsFile, []);
|
|
80
|
+
for (const site of this.sites) {
|
|
81
|
+
const file = this.jarFile(site.id);
|
|
82
|
+
this.jars.set(site.id, readJson(file, []));
|
|
83
|
+
if (fs.existsSync(file))
|
|
84
|
+
this.jarUpdatedAt.set(site.id, fs.statSync(file).mtimeMs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
jarFile(id) {
|
|
88
|
+
return path.join(this.cookiesDir, `${id}.json`);
|
|
89
|
+
}
|
|
90
|
+
saveSites() {
|
|
91
|
+
writeJson(this.sitesFile, { sites: this.sites });
|
|
92
|
+
}
|
|
93
|
+
list() {
|
|
94
|
+
return this.sites;
|
|
95
|
+
}
|
|
96
|
+
get(id) {
|
|
97
|
+
const site = this.sites.find(s => s.id === id);
|
|
98
|
+
if (!site)
|
|
99
|
+
throw new Error(`Site "${id}" introuvable.`);
|
|
100
|
+
return site;
|
|
101
|
+
}
|
|
102
|
+
/** Premier site dont l'un des domaines couvre l'hôte de l'URL. */
|
|
103
|
+
siteForUrl(url) {
|
|
104
|
+
let host;
|
|
105
|
+
try {
|
|
106
|
+
host = new URL(url).hostname.toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return this.sites.find(s => s.domains.some(d => hostMatches(host, d))) ?? null;
|
|
112
|
+
}
|
|
113
|
+
create(config) {
|
|
114
|
+
let id = generateSiteId();
|
|
115
|
+
while (this.sites.some(s => s.id === id))
|
|
116
|
+
id = generateSiteId();
|
|
117
|
+
const site = {
|
|
118
|
+
...config,
|
|
119
|
+
id,
|
|
120
|
+
state: { status: 'unknown', sessionOpenedAt: null, lostAt: null, lastCheckAt: null, lastCheckError: null },
|
|
121
|
+
};
|
|
122
|
+
this.sites.push(site);
|
|
123
|
+
this.jars.set(id, []);
|
|
124
|
+
this.saveSites();
|
|
125
|
+
return site;
|
|
126
|
+
}
|
|
127
|
+
update(id, config) {
|
|
128
|
+
const site = this.get(id);
|
|
129
|
+
Object.assign(site, config);
|
|
130
|
+
this.saveSites();
|
|
131
|
+
return site;
|
|
132
|
+
}
|
|
133
|
+
remove(id) {
|
|
134
|
+
this.get(id);
|
|
135
|
+
this.sites = this.sites.filter(s => s.id !== id);
|
|
136
|
+
this.jars.delete(id);
|
|
137
|
+
this.jarUpdatedAt.delete(id);
|
|
138
|
+
fs.rmSync(this.jarFile(id), { force: true });
|
|
139
|
+
this.events = this.events.filter(e => e.siteId !== id);
|
|
140
|
+
this.saveSites();
|
|
141
|
+
writeJson(this.eventsFile, this.events);
|
|
142
|
+
}
|
|
143
|
+
setState(id, patch) {
|
|
144
|
+
const site = this.get(id);
|
|
145
|
+
site.state = { ...site.state, ...patch };
|
|
146
|
+
this.saveSites();
|
|
147
|
+
return site;
|
|
148
|
+
}
|
|
149
|
+
addEvent(siteId, kind, detail) {
|
|
150
|
+
this.events.push({ siteId, at: Date.now(), kind, detail });
|
|
151
|
+
if (this.events.length > MAX_EVENTS)
|
|
152
|
+
this.events.splice(0, this.events.length - MAX_EVENTS);
|
|
153
|
+
writeJson(this.eventsFile, this.events);
|
|
154
|
+
}
|
|
155
|
+
/** Événements du plus récent au plus ancien, éventuellement limités à un site. */
|
|
156
|
+
listEvents(opts = {}) {
|
|
157
|
+
const filtered = opts.siteId ? this.events.filter(e => e.siteId === opts.siteId) : this.events;
|
|
158
|
+
return filtered.slice().reverse().slice(0, opts.limit ?? 200);
|
|
159
|
+
}
|
|
160
|
+
/** Cookies encore valides du site. */
|
|
161
|
+
getJar(id) {
|
|
162
|
+
this.get(id);
|
|
163
|
+
const now = Date.now() / 1000;
|
|
164
|
+
return (this.jars.get(id) ?? []).filter(c => !isExpired(c, now));
|
|
165
|
+
}
|
|
166
|
+
jarUpdated(id) {
|
|
167
|
+
return this.jarUpdatedAt.get(id) ?? null;
|
|
168
|
+
}
|
|
169
|
+
/** Cookies encore valides de tous les sites. */
|
|
170
|
+
allCookies() {
|
|
171
|
+
return this.sites.flatMap(s => this.getJar(s.id));
|
|
172
|
+
}
|
|
173
|
+
/** Domaines dont le site garde les cookies : les siens, plus ceux déjà présents dans ses cookies. */
|
|
174
|
+
cookieScope(id) {
|
|
175
|
+
const site = this.get(id);
|
|
176
|
+
const jarDomains = (this.jars.get(id) ?? []).map(c => c.domain.replace(/^\./, '').toLowerCase());
|
|
177
|
+
return Array.from(new Set([...site.domains, ...jarDomains]));
|
|
178
|
+
}
|
|
179
|
+
/** Remplace les cookies du site. Retourne false quand ils sont identiques à ceux déjà enregistrés. */
|
|
180
|
+
writeJar(id, cookies) {
|
|
181
|
+
this.get(id);
|
|
182
|
+
const next = cookies.map(toCookieData).sort((a, b) => cookieKey(a).localeCompare(cookieKey(b)));
|
|
183
|
+
const current = this.jars.get(id) ?? [];
|
|
184
|
+
if (JSON.stringify(current) === JSON.stringify(next))
|
|
185
|
+
return false;
|
|
186
|
+
this.jars.set(id, next);
|
|
187
|
+
writeJson(this.jarFile(id), next);
|
|
188
|
+
this.jarUpdatedAt.set(id, Date.now());
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let store = null;
|
|
193
|
+
/** Store du dossier de données courant, ou null si aucun dossier n'est configuré. */
|
|
194
|
+
export function getStore() {
|
|
195
|
+
const dir = getDataDir();
|
|
196
|
+
if (!dir) {
|
|
197
|
+
store = null;
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!store || store.dir !== dir)
|
|
201
|
+
store = new SiteStore(dir);
|
|
202
|
+
return store;
|
|
203
|
+
}
|
|
204
|
+
/** Store du dossier de données courant ; échoue si aucun dossier n'est configuré. */
|
|
205
|
+
export function requireStore() {
|
|
206
|
+
const s = getStore();
|
|
207
|
+
if (!s)
|
|
208
|
+
throw new Error('SCRAPER_DATA_DIR (ou COOKIES_DIR) n\'est pas configuré : les sites sont désactivés.');
|
|
209
|
+
return s;
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/simple-scraper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "MCP server for web scraping and JavaScript execution using Puppeteer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@types/react-dom": "^19.2.3",
|
|
53
53
|
"@vitejs/plugin-react": "^6.0.1",
|
|
54
54
|
"concurrently": "^9.2.1",
|
|
55
|
+
"lucide-react": "^0.468.0",
|
|
55
56
|
"react": "^19.2.4",
|
|
56
57
|
"react-dom": "^19.2.4",
|
|
57
58
|
"tsx": "^4.21.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#0d0f14;--sidebar:#11131a;--surface:#151821;--inset:#11131a;--raised:#1b1f2e;--hover:#1a1e2b;--border:#232735;--border-strong:#2c3142;--divider:#1f2330;--text:#e7e9f0;--text-2:#c3c8d6;--muted:#7c8296;--faint:#5d6377;--accent:#6d7cff;--accent-hover:#7f8cff;--accent-text:#8b95ff;--ok:#34d399;--warn:#f5b14c;--danger:#f87171;--radius:12px;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}*,:before,:after{box-sizing:border-box;margin:0;padding:0}body{background:var(--bg);color:var(--text);min-height:100vh;font-size:13px;line-height:1.45}a{color:inherit;text-decoration:none}button{font:inherit;color:inherit;cursor:pointer;background:0 0;border:none}button:disabled{cursor:not-allowed;opacity:.5}input,select{font:inherit;color:inherit}.muted{color:var(--muted)}.faint{color:var(--faint)}.accent{color:var(--accent-text);align-items:center;gap:4px;display:inline-flex}.ok{color:var(--ok)}.warn{color:var(--warn)}.danger{color:var(--danger)}.small{font-size:12px}.mono{font-family:JetBrains Mono,Consolas,monospace;font-size:13px;font-weight:600}.stack{flex-direction:column;min-width:0;display:flex}.ellipsis{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.push-right{margin-left:auto}.spin{animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.layout{min-height:100vh;display:flex}.main{flex:1;min-width:0}.page{flex-direction:column;gap:20px;max-width:1208px;padding:36px 40px 48px;display:flex}.page-head{justify-content:space-between;align-items:flex-start;gap:24px;display:flex}.page-head h1,.site-head h1{letter-spacing:-.01em;font-size:22px;font-weight:600}.page-sub{color:var(--muted);margin-top:6px}.sidebar{background:var(--sidebar);border-right:1px solid var(--divider);flex-direction:column;flex-shrink:0;width:232px;height:100vh;padding:22px 12px 20px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:12px;margin-bottom:32px;padding:0 8px;display:flex}.brand-logo{width:34px;height:34px;color:var(--accent-text);background:#1d2135;border-radius:9px;place-items:center;display:grid}.brand-name{font-size:14px;font-weight:600;display:block}.brand-sub{color:var(--muted);font-size:12px;display:block}.nav{flex-direction:column;gap:4px;display:flex}.nav-item{color:#a3a9ba;border-radius:8px;align-items:center;gap:10px;height:36px;padding:0 12px;font-weight:500;transition:background .1s,color .1s;display:flex}.nav-item svg{color:var(--muted)}.nav-item:hover{background:var(--hover);color:var(--text)}.nav-item.active{background:var(--raised);color:var(--text)}.nav-item.active svg{color:var(--accent-text)}.nav-label{flex:1}.nav-count{text-align:center;min-width:24px;color:var(--muted);font-size:11px}.nav-count-alert{color:var(--danger);background:#f8717129;border-radius:9px;padding:1px 6px;font-weight:600}.keepalive-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;margin-top:auto;padding:12px 14px}.keepalive-title{align-items:center;gap:8px;font-size:12px;font-weight:600;display:flex}.keepalive-detail{color:var(--muted);margin-top:6px;font-size:12px}.dot{background:currentColor;border-radius:50%;flex-shrink:0;width:8px;height:8px;display:inline-block}.dot-ok{background:var(--ok)}.dot-muted{background:var(--faint)}.btn{white-space:nowrap;border-radius:8px;justify-content:center;align-items:center;gap:8px;height:36px;padding:0 16px;font-weight:600;transition:background .1s,border-color .1s;display:inline-flex}.btn-lg{height:38px}.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--surface);border:1px solid var(--border-strong);color:var(--text-2);font-weight:500}.btn-ghost:hover:not(:disabled){background:var(--hover)}.btn-warn{background:var(--warn);color:#1a1405}.btn-danger-ghost{color:var(--danger);background:#f871710f;border:1px solid #f871714d;font-weight:500}.btn-danger-ghost:hover:not(:disabled){background:#f871711f}.icon-btn{width:34px;height:34px;color:var(--muted);border-radius:8px;place-items:center;display:inline-grid}.icon-btn:hover{background:var(--hover);color:var(--text)}.icon-btn.bordered{border:1px solid var(--border-strong);color:var(--text-2)}.link-btn{color:var(--accent-text);align-items:center;gap:6px;font-weight:500;display:inline-flex}.link-btn.muted{color:var(--muted)}.link{color:var(--accent-text);font-size:12px;font-weight:500}.banner{border-radius:var(--radius);border:1px solid;align-items:center;gap:14px;padding:12px 12px 12px 20px;display:flex}.banner>div{flex:1}.banner-title{font-weight:600}.banner-text{margin-top:2px;font-size:12px}.banner-warn{color:#f5d29a;background:#f5b14c12;border-color:#f5b14c47}.banner-warn>svg{color:var(--warn)}.banner-warn .banner-text{color:#b9a785}.banner-danger{color:#f8b4b4;background:#f8717112;border-color:#f8717147}.banner-danger>svg{color:var(--danger)}.hint{border:1px solid var(--border);border-radius:var(--radius);color:var(--muted);align-items:flex-start;gap:14px;padding:16px 20px;font-size:12px;line-height:1.6;display:flex}.hint svg{flex-shrink:0;margin-top:1px}.empty-card{text-align:center;border:1px dashed var(--border-strong);border-radius:var(--radius);color:var(--muted);flex-direction:column;align-items:center;gap:10px;padding:48px 24px;display:flex}.empty-card .empty-title{color:var(--text);font-size:15px;font-weight:600}.empty-card p{max-width:420px}.empty-card .btn{margin-top:6px}.site-grid,.site-row{grid-template-columns:minmax(220px,1.8fr) 150px minmax(170px,1.1fr) minmax(130px,.8fr) 190px;align-items:center;column-gap:20px;display:grid}.list-head{letter-spacing:.04em;text-transform:uppercase;color:var(--faint);padding:4px 20px 0;font-size:11px;font-weight:600}.site-list{flex-direction:column;gap:10px;margin-top:-8px;list-style:none;display:flex}.site-row{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;min-height:76px;padding:14px 16px 14px 20px;transition:border-color .1s}.site-row:hover{border-color:var(--border-strong)}.col-site{align-items:center;gap:16px;min-width:0;display:flex}.col-actions{justify-content:flex-end;align-items:center;gap:8px;display:flex}.col-actions .btn{width:140px}.row-title{font-size:14px;font-weight:600}.row-main{font-size:13px}.row-sub{color:var(--muted);font-size:12px}.favicon{border-radius:10px;flex-shrink:0;place-items:center;font-weight:700;display:inline-grid}.favicon-md{width:40px;height:40px;font-size:16px}.favicon-lg{border-radius:12px;width:48px;height:48px;font-size:19px}.tone-violet{color:#c4a3ff;background:#2a1f3d}.tone-green{color:#6ee7b7;background:#1d2f2a}.tone-blue{color:#93b4ff;background:#1d2740}.tone-amber{color:#f5b14c;background:#3a2a1a}.tone-rose{color:#fda4bf;background:#3a1d27}.tone-teal{color:#5eead4;background:#16302f}.pill{white-space:nowrap;border-radius:12px;align-items:center;gap:6px;width:fit-content;height:24px;padding:0 11px;font-size:12px;font-weight:500;display:inline-flex}.pill .dot{width:6px;height:6px}.pill-ok{color:var(--ok);background:#34d3991f}.pill-danger{color:var(--danger);background:#f871711f}.pill-muted{color:#a3a9ba;background:#7c829624}.menu-anchor{position:relative}.menu{z-index:20;background:var(--raised);border:1px solid var(--border-strong);border-radius:10px;flex-direction:column;min-width:210px;padding:6px;display:flex;position:absolute;top:40px;right:0;box-shadow:0 12px 32px #00000073}.menu button{text-align:left;color:var(--text-2);border-radius:6px;padding:8px 10px}.menu button:hover:not(:disabled){background:var(--hover)}.menu button.danger{color:var(--danger)}.menu-error{color:var(--danger);padding:6px 10px;font-size:12px}.card{background:var(--surface);border:1px solid var(--border);border-radius:14px}.card-head{border-bottom:1px solid var(--divider);padding:20px 24px 16px}.card-head h2{font-size:15px;font-weight:600}.card-head p{color:var(--muted);margin-top:4px;font-size:12px}.card-head-row{justify-content:space-between;align-items:center;gap:16px;display:flex}.card-body{flex-direction:column;gap:18px;padding:20px 24px 24px;display:flex}.card-body.dimmed{opacity:.55}.field{grid-template-columns:200px 1fr;align-items:start;gap:16px;display:grid}.field-label{color:var(--text-2);flex-direction:column;gap:4px;padding-top:8px;font-weight:500;display:flex}.field-help{color:var(--faint);font-size:12px;font-weight:400}.field-control{flex-direction:column;gap:10px;min-width:0;display:flex}.input{background:var(--inset);border:1px solid var(--border-strong);border-radius:8px;outline:none;width:100%;height:36px;padding:0 14px}.input:focus{border-color:var(--accent)}.input::placeholder{color:var(--faint)}.select{width:auto;min-width:200px}.segmented{background:var(--inset);border:1px solid var(--border);border-radius:8px;gap:2px;padding:3px;display:flex}.segmented button{height:26px;color:var(--muted);border-radius:6px;flex:1;font-size:12px;font-weight:500}.segmented button:hover{color:var(--text-2)}.segmented button.active{color:var(--text);background:#252a3b;font-weight:600}.toggle{background:var(--border-strong);border-radius:11px;flex-shrink:0;width:40px;height:22px;transition:background .15s;position:relative}.toggle span{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:3px;left:3px}.toggle.on{background:var(--accent)}.toggle.on span{transform:translate(18px)}.chips{background:var(--inset);border:1px solid var(--border-strong);cursor:text;border-radius:8px;flex-wrap:wrap;align-items:center;gap:8px;min-height:36px;padding:4px;display:flex}.chips:focus-within{border-color:var(--accent)}.chip{height:26px;color:var(--text-2);background:#1d2130;border-radius:6px;align-items:center;gap:8px;padding:0 6px 0 10px;font-size:12px;display:inline-flex}.chip button{color:var(--faint);place-items:center;display:grid}.chip button:hover{color:var(--text)}.chip-input{background:0 0;border:none;outline:none;flex:1;min-width:120px;height:26px;padding:0 6px}.chip-input::placeholder{color:var(--accent-text)}.checkbox{cursor:pointer;align-items:center;gap:10px;padding-top:8px;display:inline-flex}.checkbox input{opacity:0;pointer-events:none;position:absolute}.checkbox-box{border:1px solid var(--border-strong);color:#0000;border-radius:4px;place-items:center;width:16px;height:16px;display:grid}.checkbox input:checked+.checkbox-box{background:var(--accent);border-color:var(--accent);color:#fff}.checkbox input:focus-visible+.checkbox-box{outline:2px solid var(--accent-text)}.form-error{color:var(--danger);font-size:12px}.save-bar{background:var(--raised);border:1px solid var(--border-strong);border-radius:var(--radius);justify-content:space-between;align-items:center;gap:16px;padding:12px 12px 12px 20px;display:flex;position:sticky;bottom:16px;box-shadow:0 12px 32px #00000073}.save-bar-actions{gap:10px;display:flex}.back{color:var(--muted);align-items:center;gap:4px;width:fit-content;margin-bottom:-8px;font-size:12px;display:inline-flex}.back:hover{color:var(--text)}.site-head{align-items:center;gap:16px;display:flex}.site-head-text{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.site-head-meta{align-items:center;gap:12px;min-width:0;display:flex}.site-head-meta>span:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.site-head-actions{gap:12px;display:flex}.detail-grid{grid-template-columns:minmax(0,1fr) 376px;align-items:start;gap:24px;display:grid}.detail-main,.detail-side{flex-direction:column;gap:20px;display:flex}.events{gap:6px}.event{align-items:flex-start;gap:12px;padding:6px 0;display:flex}.event-icon{border-radius:50%;flex-shrink:0;place-items:center;width:28px;height:28px;display:grid}.tone-bg-ok{color:var(--ok);background:#34d3991f}.tone-bg-danger{color:var(--danger);background:#f8717124}.tone-bg-warn{color:var(--warn);background:#f5b14c24}.tone-bg-accent{color:var(--accent-text);background:#6d7cff29}.event-text{flex:1;min-width:0}.event-title{font-size:13px}.event-site{color:var(--muted)}.event-detail{color:var(--faint);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.event-time{color:var(--muted);white-space:nowrap;font-size:12px}.kv{color:var(--muted);justify-content:space-between;display:flex}.kv span:last-child{color:var(--text)}.data-actions{grid-template-columns:1fr 1fr;gap:12px;margin-top:4px;display:grid}.data-actions .btn{padding:0 10px;font-size:12px}.help-list{color:var(--muted);gap:12px;font-size:12px;line-height:1.6}.help-list strong{color:var(--text-2);font-weight:600}.modal-overlay{z-index:50;background:#05060ab8;place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--surface);border:1px solid #2a2f40;border-radius:16px;width:100%;box-shadow:0 24px 64px #00000080}.modal-login,.modal-picker{flex-direction:column;max-width:min(1560px,96vw);max-height:calc(100vh - 48px);display:flex}.modal-login-body,.modal-picker-body{min-height:0;overflow:auto}.modal-login .remote-view,.modal-picker .remote-view{justify-content:center;display:flex}.modal-login .remote-view img,.modal-picker .remote-view img{width:auto;max-width:100%;max-height:calc(100vh - 320px)}.modal-head{border-bottom:1px solid var(--border);align-items:center;gap:16px;padding:24px 28px 22px 32px;display:flex}.modal-head-text{flex:1}.modal-head h3{font-size:18px;font-weight:600}.modal-login-body{grid-template-columns:260px minmax(0,1fr);gap:32px;padding:24px 32px;display:grid}.modal-picker-body{grid-template-columns:minmax(0,1fr) 320px;gap:28px;padding:24px 32px 28px;display:grid}.modal-foot{border-top:1px solid var(--border);justify-content:space-between;align-items:center;padding:18px 32px 20px;display:flex}.modal-foot-actions{gap:12px;display:flex}.login-side{flex-direction:column;justify-content:space-between;gap:24px;display:flex}.steps{flex-direction:column;gap:32px;display:flex;position:relative}.steps:before{content:"";background:var(--border-strong);width:2px;position:absolute;top:28px;bottom:28px;left:11px}.step{gap:16px;display:flex;position:relative}.step-dot{background:var(--surface);border:2px solid var(--border-strong);width:24px;height:24px;color:var(--muted);border-radius:50%;flex-shrink:0;place-items:center;font-size:12px;font-weight:700;display:grid}.step-done .step-dot{color:var(--ok);background:#193a33;border-color:#193a33}.step-current .step-dot{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:0 0 0 4px #6d7cff2e}.step-title{color:var(--text);padding-top:2px;font-weight:600}.step-done .step-title{color:#a3a9ba}.step-todo .step-title{color:var(--muted)}.step-text{color:#a3a9ba;margin-top:4px;font-size:12px}.step-done .step-text{color:var(--muted)}.step-todo .step-text{color:var(--faint)}.note{background:var(--inset);border:1px solid var(--border);color:var(--muted);border-radius:10px;gap:10px;padding:14px;font-size:12px;line-height:1.6;display:flex}.note svg{flex-shrink:0}.login-main{flex-direction:column;gap:14px;min-width:0;display:flex}.browser-frame{border:1px solid var(--border-strong);background:#f4f5f8;border-radius:12px;overflow:hidden}.browser-bar{background:#1d2130;align-items:center;gap:16px;height:38px;padding:0 14px;display:flex}.browser-dots{gap:6px;display:flex}.browser-dots i{background:#3a3f52;border-radius:50%;width:8px;height:8px}.browser-url{background:var(--inset);color:#a3a9ba;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex:1;align-items:center;gap:8px;height:22px;padding:0 10px;font-size:11px;display:flex;overflow:hidden}.browser-view{background:#f4f5f8;place-items:center;min-height:320px;display:grid;overflow:hidden}.browser-placeholder{text-align:center;color:#5b6070;flex-direction:column;align-items:center;gap:10px;padding:24px;font-size:13px;display:flex}.viewer-shot .browser-placeholder{color:var(--muted)}.login-status{color:var(--text-2);align-items:center;gap:8px;font-weight:500;display:flex}.sessions-layout{grid-template-columns:340px minmax(0,1fr);align-items:start;gap:24px;display:grid}.session-list{flex-direction:column;gap:12px;list-style:none;display:flex}.session-card{border-radius:var(--radius);background:var(--surface);border:1px solid var(--border);cursor:pointer;gap:14px;padding:14px;display:flex}.session-card:hover{border-color:var(--border-strong)}.session-card.selected{border-color:var(--accent);box-shadow:inset 0 0 0 .5px var(--accent);background:#181c2a}.session-thumb{object-fit:cover;object-position:top;background:#eef0f5;border-radius:6px;flex-shrink:0;width:88px;height:60px}.session-card-text{flex:1;gap:4px}.session-card-foot{justify-content:space-between;align-items:center;margin-top:4px;display:flex}.viewer{background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden}.viewer-bar{border-bottom:1px solid var(--divider);align-items:center;gap:12px;padding:14px 20px;display:flex}.live{color:var(--ok);white-space:nowrap;align-items:center;gap:8px;font-size:12px;font-weight:600;display:inline-flex}.live .dot{width:6px;height:6px;box-shadow:0 0 0 3px #34d39933}.live-paused{color:var(--muted)}.live-paused .dot{box-shadow:none}.viewer-url{background:var(--inset);border:1px solid var(--border);min-width:0;height:34px;color:var(--text-2);white-space:nowrap;text-overflow:ellipsis;border-radius:8px;flex:1;align-items:center;gap:8px;padding:0 12px;font-size:12px;display:flex;overflow:hidden}.viewer-url svg{color:var(--muted);flex-shrink:0}.viewer-shot{background:var(--inset);border-radius:8px;place-items:center;min-height:240px;margin:16px 20px;display:grid;overflow:hidden}.viewer-shot img{width:100%;display:block}.viewer-info{border-top:1px solid var(--divider);grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;padding:16px 20px 20px;display:grid}.viewer-info>div{flex-direction:column;gap:4px;min-width:0;display:flex}.viewer-info .label{text-transform:uppercase;letter-spacing:.04em;color:var(--faint);font-size:11px;font-weight:600}.viewer-console{justify-content:space-between;gap:8px;display:flex}.console{border-top:1px solid var(--divider);background:var(--inset);max-height:260px;padding:12px 20px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;overflow:auto}.console-line{color:var(--text-2);gap:12px;padding:2px 0;display:flex}.console-time{color:var(--faint)}.console-type{width:56px;color:var(--muted);flex-shrink:0}.console-error .console-type,.console-error .console-text{color:var(--danger)}.console-warn .console-type,.console-warning .console-type{color:var(--warn)}.console-text{white-space:pre-wrap;word-break:break-word}@media (width<=1100px){.detail-grid,.sessions-layout{grid-template-columns:1fr}.site-grid,.site-row{grid-template-columns:minmax(200px,1fr) 140px minmax(160px,1fr) 180px}.col-keep,.list-head span:nth-child(4){display:none}}.remote-view{outline:none;align-self:start;width:100%;position:relative}.remote-view img{cursor:default;-webkit-user-select:none;user-select:none;width:100%;height:auto;display:block}.remote-view.crosshair img{cursor:crosshair}.remote-view.focused{box-shadow:inset 0 0 0 2px var(--accent)}.remote-hint{color:var(--text-2);pointer-events:none;background:#0d0f14d1;border-radius:999px;align-items:center;gap:6px;padding:6px 12px;font-size:12px;display:inline-flex;position:absolute;bottom:12px;left:50%;transform:translate(-50%)}.remote-broken{color:var(--text-2);background:#0d0f1499;place-items:center;display:grid;position:absolute;inset:0}.browser-btn{color:#a3a9ba;border-radius:6px;place-items:center;width:24px;height:22px;display:grid}.browser-btn:hover:not(:disabled){background:var(--inset);color:var(--text)}.browser-url input{min-width:0;color:inherit;background:0 0;border:none;outline:none;flex:1;font-size:11px}.refresh-select{color:var(--muted);white-space:nowrap;align-items:center;gap:6px;font-size:12px;display:inline-flex}.refresh-select select{background:var(--inset);border:1px solid var(--border-strong);height:28px;color:var(--text-2);border-radius:6px;padding:0 6px;font-size:12px}.btn-sm{gap:6px;height:30px;padding:0 12px;font-size:12px}.signal-tools{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.signal-result{flex-wrap:wrap;gap:10px;margin-left:4px;display:inline-flex}.verdict{align-items:center;gap:4px;font-size:12px;font-weight:500;display:inline-flex}.verdict.ok{color:var(--ok)}.verdict.danger{color:var(--danger)}.picker-icon{width:40px;height:40px;color:var(--accent-text);background:#6d7cff29;border-radius:10px;flex-shrink:0;place-items:center;display:grid}.picker-side{flex-direction:column;gap:12px;min-width:0;max-height:640px;display:flex;overflow:auto}.picker-side h4{font-size:13px;font-weight:600}.picker-wait{align-items:center;gap:6px;display:inline-flex}.candidate{background:var(--inset);border:1px solid var(--border);border-radius:10px;flex-direction:column;gap:8px;padding:12px 14px;display:flex}.candidate.good{border-color:#34d39966}.candidate-label{color:var(--muted);font-size:12px}.candidate-selector{color:var(--text);word-break:break-all;font-family:JetBrains Mono,Consolas,monospace;font-size:12px}.candidate-verdicts{flex-wrap:wrap;gap:10px;display:flex}.candidate .btn{align-self:flex-start;height:30px;font-size:12px}.browser-frame:fullscreen{background:var(--bg);border:none;border-radius:0;flex-direction:column;display:flex}.browser-frame:fullscreen .browser-view{background:var(--bg);flex:1;min-height:0}.browser-frame:fullscreen .remote-view{place-items:center;width:100%;height:100%;display:grid}.browser-frame:fullscreen .remote-view img{width:auto;max-width:100%;height:auto;max-height:calc(100vh - 38px)}.viewer:fullscreen{border:none;border-radius:0;overflow:auto}.viewer:fullscreen .viewer-shot img{width:auto;max-width:100%;max-height:calc(100vh - 190px);margin:0 auto}
|