viberadar 0.3.1 → 0.3.3

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.
@@ -5,6 +5,6 @@ interface ServerOptions {
5
5
  port: number;
6
6
  projectRoot: string;
7
7
  }
8
- export declare function startServer({ data, port, projectRoot }: ServerOptions): Promise<http.Server>;
8
+ export declare function startServer({ data: initialData, port, projectRoot }: ServerOptions): Promise<http.Server>;
9
9
  export {};
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAuC5F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAI7B,OAAO,EAAE,UAAU,EAAe,MAAM,YAAY,CAAC;AAErD,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAwGzG"}
@@ -32,14 +32,72 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.startServer = startServer;
37
40
  const http = __importStar(require("http"));
38
41
  const fs = __importStar(require("fs"));
39
42
  const path = __importStar(require("path"));
43
+ const chokidar_1 = __importDefault(require("chokidar"));
44
+ const scanner_1 = require("../scanner");
40
45
  const DASHBOARD_HTML = fs.readFileSync(path.join(__dirname, '../ui/dashboard.html'), 'utf-8');
41
- function startServer({ data, port, projectRoot }) {
46
+ function startServer({ data: initialData, port, projectRoot }) {
42
47
  return new Promise((resolve, reject) => {
48
+ // ── Mutable data reference ──────────────────────────────────────────────────
49
+ let currentData = initialData;
50
+ // ── SSE clients ─────────────────────────────────────────────────────────────
51
+ const sseClients = new Set();
52
+ function broadcast(event) {
53
+ const msg = `event: ${event}\ndata: {}\n\n`;
54
+ for (const client of sseClients) {
55
+ try {
56
+ client.write(msg);
57
+ }
58
+ catch {
59
+ sseClients.delete(client);
60
+ }
61
+ }
62
+ }
63
+ // ── File watcher + re-scan ──────────────────────────────────────────────────
64
+ let scanDebounce = null;
65
+ async function scheduleRescan(changedFile) {
66
+ if (scanDebounce)
67
+ clearTimeout(scanDebounce);
68
+ scanDebounce = setTimeout(async () => {
69
+ try {
70
+ const label = changedFile
71
+ ? path.relative(projectRoot, changedFile).replace(/\\/g, '/')
72
+ : '…';
73
+ process.stdout.write(`\r 🔄 ${label} changed, rescanning... `);
74
+ currentData = await (0, scanner_1.scanProject)(projectRoot);
75
+ process.stdout.write(`\r ✅ ${currentData.modules.length} modules` +
76
+ (currentData.features ? `, ${currentData.features.length} features` : '') +
77
+ ' \n');
78
+ broadcast('data-updated');
79
+ }
80
+ catch (err) {
81
+ console.error('\nRescan error:', err.message);
82
+ }
83
+ }, 600);
84
+ }
85
+ chokidar_1.default.watch([
86
+ '**/*.{ts,tsx,js,jsx,vue,svelte}',
87
+ 'viberadar.config.json',
88
+ ], {
89
+ cwd: projectRoot,
90
+ ignored: [
91
+ '**/node_modules/**', '**/dist/**', '**/.git/**',
92
+ '**/coverage/**', '**/.next/**', '**/.turbo/**',
93
+ ],
94
+ ignoreInitial: true,
95
+ persistent: true,
96
+ })
97
+ .on('add', f => scheduleRescan(path.join(projectRoot, f)))
98
+ .on('change', f => scheduleRescan(path.join(projectRoot, f)))
99
+ .on('unlink', f => scheduleRescan(path.join(projectRoot, f)));
100
+ // ── HTTP server ─────────────────────────────────────────────────────────────
43
101
  const server = http.createServer((req, res) => {
44
102
  const url = req.url ?? '/';
45
103
  if (url === '/') {
@@ -49,7 +107,19 @@ function startServer({ data, port, projectRoot }) {
49
107
  }
50
108
  if (url === '/api/data') {
51
109
  res.writeHead(200, { 'Content-Type': 'application/json' });
52
- res.end(JSON.stringify(data, null, 2));
110
+ res.end(JSON.stringify(currentData));
111
+ return;
112
+ }
113
+ // Server-Sent Events endpoint
114
+ if (url === '/api/events') {
115
+ res.writeHead(200, {
116
+ 'Content-Type': 'text/event-stream',
117
+ 'Cache-Control': 'no-cache',
118
+ 'Connection': 'keep-alive',
119
+ });
120
+ res.write('data: connected\n\n'); // initial ping
121
+ sseClients.add(res);
122
+ req.on('close', () => sseClients.delete(res));
53
123
  return;
54
124
  }
55
125
  res.writeHead(404);
@@ -63,10 +133,7 @@ function startServer({ data, port, projectRoot }) {
63
133
  reject(err);
64
134
  }
65
135
  });
66
- server.listen(port, '127.0.0.1', () => {
67
- resolve(server);
68
- });
69
- // Graceful shutdown
136
+ server.listen(port, '127.0.0.1', () => resolve(server));
70
137
  process.on('SIGINT', () => {
71
138
  console.log('\n👋 VibeRadar stopped.');
72
139
  server.close(() => process.exit(0));
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA,kCAuCC;AAvDD,2CAA6B;AAC7B,uCAAyB;AACzB,2CAA6B;AAS7B,MAAM,cAAc,GAAG,EAAE,CAAC,YAAY,CACpC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,EAC5C,OAAO,CACR,CAAC;AAEF,SAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAiB;IACpE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;YAE3B,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;gBAChB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC,CAAC;gBACnE,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBACxB,OAAO;YACT,CAAC;YAED,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;gBACxB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;gBACvC,OAAO;YACT,CAAC;YAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;YAChD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC9B,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,IAAI,mDAAmD,CAAC,CAAC,CAAC;YACrF,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE;YACpC,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,oBAAoB;QACpB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;YACxB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;YACvC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,kCAwGC;AAzHD,2CAA6B;AAC7B,uCAAyB;AACzB,2CAA6B;AAC7B,wDAAgC;AAChC,wCAAqD;AAQrD,MAAM,cAAc,GAAG,EAAE,CAAC,YAAY,CACpC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,CAAC,EAC5C,OAAO,CACR,CAAC;AAEF,SAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAiB;IACjF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAErC,+EAA+E;QAC/E,IAAI,WAAW,GAAG,WAAW,CAAC;QAE9B,+EAA+E;QAC/E,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;QAElD,SAAS,SAAS,CAAC,KAAa;YAC9B,MAAM,GAAG,GAAG,UAAU,KAAK,gBAAgB,CAAC;YAC5C,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;gBAChC,IAAI,CAAC;oBAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC;oBAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBAAC,CAAC;YACjE,CAAC;QACH,CAAC;QAED,+EAA+E;QAC/E,IAAI,YAAY,GAAyC,IAAI,CAAC;QAE9D,KAAK,UAAU,cAAc,CAAC,WAAoB;YAChD,IAAI,YAAY;gBAAE,YAAY,CAAC,YAAY,CAAC,CAAC;YAC7C,YAAY,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;gBACnC,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,WAAW;wBACvB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;wBAC7D,CAAC,CAAC,GAAG,CAAC;oBACR,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,8BAA8B,CAAC,CAAC;oBACrE,WAAW,GAAG,MAAM,IAAA,qBAAW,EAAC,WAAW,CAAC,CAAC;oBAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,UAAU,WAAW,CAAC,OAAO,CAAC,MAAM,UAAU;wBAC9C,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,QAAQ,CAAC,MAAM,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;wBACzE,cAAc,CACf,CAAC;oBACF,SAAS,CAAC,cAAc,CAAC,CAAC;gBAC5B,CAAC;gBAAC,OAAO,GAAQ,EAAE,CAAC;oBAClB,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;gBAChD,CAAC;YACH,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC;QAED,kBAAQ,CAAC,KAAK,CAAC;YACb,iCAAiC;YACjC,uBAAuB;SACxB,EAAE;YACD,GAAG,EAAE,WAAW;YAChB,OAAO,EAAE;gBACP,oBAAoB,EAAE,YAAY,EAAE,YAAY;gBAChD,gBAAgB,EAAK,aAAa,EAAE,cAAc;aACnD;YACD,aAAa,EAAE,IAAI;YACnB,UAAU,EAAE,IAAI;SACjB,CAAC;aACC,EAAE,CAAC,KAAK,EAAK,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;aAC5D,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;aAC5D,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAEhE,+EAA+E;QAC/E,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;YAE3B,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;gBAChB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC,CAAC;gBACnE,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBACxB,OAAO;YACT,CAAC;YAED,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;gBACxB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC;gBACrC,OAAO;YACT,CAAC;YAED,8BAA8B;YAC9B,IAAI,GAAG,KAAK,aAAa,EAAE,CAAC;gBAC1B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;oBACjB,cAAc,EAAG,mBAAmB;oBACpC,eAAe,EAAE,UAAU;oBAC3B,YAAY,EAAK,YAAY;iBAC9B,CAAC,CAAC;gBACH,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe;gBACjD,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACpB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC9C,OAAO;YACT,CAAC;YAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;YAChD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC9B,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,IAAI,mDAAmD,CAAC,CAAC,CAAC;YACrF,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAExD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;YACxB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;YACvC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -351,6 +351,12 @@
351
351
  <h1>VibeRadar</h1>
352
352
  <span class="header-project" id="projectName">—</span>
353
353
  <span class="header-time" id="scannedAt"></span>
354
+ <span id="liveDot" title="Connecting…" style="
355
+ width:8px; height:8px; border-radius:50%;
356
+ background:var(--dim); display:inline-block;
357
+ margin-left:8px; transition:background 0.3s;
358
+ flex-shrink:0;
359
+ "></span>
354
360
  </header>
355
361
 
356
362
  <div class="stats-bar" id="statsBar"></div>
@@ -563,6 +569,37 @@ function renderFeatureCards(c) {
563
569
  card.onclick = () => openFeaturePanel(f.key);
564
570
  grid.appendChild(card);
565
571
  });
572
+
573
+ // ── Unmapped card ──────────────────────────────────────────────────────────
574
+ if (!q) {
575
+ const unmappedSrc = D.modules.filter(m =>
576
+ m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
577
+ );
578
+ if (unmappedSrc.length > 0) {
579
+ const isActive = activePanelKey === '__unmapped__';
580
+ const card = document.createElement('div');
581
+ card.className = 'feature-card' + (isActive ? ' active' : '');
582
+ card.style.borderStyle = 'dashed';
583
+ card.style.opacity = '0.75';
584
+ card.innerHTML = `
585
+ <div class="feature-accent" style="background:var(--yellow)"></div>
586
+ <div class="feature-body">
587
+ <div class="feature-title">
588
+ <span style="color:var(--yellow)">⚠ Unmapped</span>
589
+ <span class="feature-file-count">${unmappedSrc.length} ${pluralFiles(unmappedSrc.length)}</span>
590
+ </div>
591
+ <div class="feature-desc">Файлы вне карты фич — не входят ни в одну фичу</div>
592
+ <div class="feature-progress-wrap">
593
+ <div class="feature-progress-bar">
594
+ <div class="feature-progress-fill" style="width:100%;background:var(--border)"></div>
595
+ </div>
596
+ <span class="feature-progress-label" style="color:var(--dim)">нет привязки</span>
597
+ </div>
598
+ </div>`;
599
+ card.onclick = () => openUnmappedPanel(unmappedSrc);
600
+ grid.appendChild(card);
601
+ }
602
+ }
566
603
  }
567
604
 
568
605
  function renderModuleGrid(c) {
@@ -720,6 +757,71 @@ function openModulePanel(m) {
720
757
  document.getElementById('panel').classList.add('open');
721
758
  }
722
759
 
760
+ function openUnmappedPanel(files) {
761
+ activePanelKey = '__unmapped__';
762
+ renderContent();
763
+
764
+ // Group by top-level directory
765
+ const byDir = {};
766
+ files.forEach(m => {
767
+ const parts = m.relativePath.replace(/\\/g, '/').split('/');
768
+ const dir = parts.length > 1 ? parts[0] : '(root)';
769
+ if (!byDir[dir]) byDir[dir] = [];
770
+ byDir[dir].push(m);
771
+ });
772
+ const dirs = Object.keys(byDir).sort();
773
+
774
+ // Build plain-text list for copying to AI agent
775
+ const plainList = files
776
+ .map(m => '- ' + m.relativePath.replace(/\\/g, '/'))
777
+ .join('\n');
778
+
779
+ const promptText =
780
+ `В проекте ${files.length} файлов без привязки к фичам (unmapped).\n` +
781
+ `Изучи список ниже и обнови viberadar.config.json:\n` +
782
+ `добавь эти файлы в существующие фичи или создай новые.\n\n` +
783
+ plainList;
784
+
785
+ document.getElementById('panelContent').innerHTML = `
786
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
787
+ <span style="font-size:16px">⚠</span>
788
+ <div class="panel-title" style="color:var(--yellow)">Unmapped файлы</div>
789
+ </div>
790
+ <div class="panel-subtitle">
791
+ Не входят ни в одну фичу.<br>
792
+ Запусти <code style="color:var(--blue)">npx viberadar init</code> — агент предложит куда добавить.
793
+ </div>
794
+
795
+ <button id="copyUnmapped" style="
796
+ width:100%; padding:8px 12px; margin-bottom:16px;
797
+ background:var(--bg); border:1px solid var(--border);
798
+ border-radius:6px; color:var(--blue); font-size:12px;
799
+ cursor:pointer; text-align:left;
800
+ ">📋 Скопировать список для AI-агента</button>
801
+
802
+ ${dirs.map(dir => `
803
+ <div class="panel-section">
804
+ <div class="panel-section-label">${dir}/ (${byDir[dir].length})</div>
805
+ <div class="file-list">
806
+ ${byDir[dir].map(m => fileItem(m)).join('')}
807
+ </div>
808
+ </div>
809
+ `).join('')}`;
810
+
811
+ document.getElementById('copyUnmapped').onclick = function() {
812
+ navigator.clipboard.writeText(promptText).then(() => {
813
+ this.textContent = '✅ Скопировано! Вставь в AI-агента';
814
+ this.style.color = 'var(--green)';
815
+ setTimeout(() => {
816
+ this.textContent = '📋 Скопировать список для AI-агента';
817
+ this.style.color = 'var(--blue)';
818
+ }, 3000);
819
+ });
820
+ };
821
+
822
+ document.getElementById('panel').classList.add('open');
823
+ }
824
+
723
825
  function closePanel() {
724
826
  activePanelKey = null;
725
827
  document.getElementById('panel').classList.remove('open');
@@ -749,7 +851,69 @@ document.getElementById('searchInput').oninput = e => {
749
851
  document.getElementById('panelClose').onclick = closePanel;
750
852
  document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
751
853
 
752
- init();
854
+ // ─── Live reload ──────────────────────────────────────────────────────────────
855
+ function setLiveDot(color, title) {
856
+ const dot = document.getElementById('liveDot');
857
+ dot.style.background = color;
858
+ dot.title = title;
859
+ }
860
+
861
+ async function refreshData() {
862
+ try {
863
+ const res = await fetch('/api/data');
864
+ D = await res.json();
865
+
866
+ // Update header timestamp
867
+ document.getElementById('scannedAt').textContent =
868
+ new Date(D.scannedAt).toLocaleTimeString();
869
+
870
+ renderStats();
871
+ renderSidebar();
872
+ renderContent();
873
+
874
+ // Re-open panel if it was open
875
+ const panelOpen = document.getElementById('panel').classList.contains('open');
876
+ if (panelOpen && activePanelKey) {
877
+ if (activePanelKey === '__unmapped__') {
878
+ const unmapped = D.modules.filter(m =>
879
+ m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
880
+ );
881
+ openUnmappedPanel(unmapped);
882
+ } else if (view === 'features' && D.features) {
883
+ openFeaturePanel(activePanelKey);
884
+ } else {
885
+ const m = D.modules.find(m => m.id === activePanelKey);
886
+ if (m) openModulePanel(m);
887
+ else closePanel();
888
+ }
889
+ }
890
+
891
+ // Brief green flash on the dot to signal fresh data
892
+ setLiveDot('#3fb950', 'Live — обновлено только что');
893
+ setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
894
+ } catch (err) {
895
+ console.error('Refresh failed:', err);
896
+ }
897
+ }
898
+
899
+ function connectSSE() {
900
+ const es = new EventSource('/api/events');
901
+
902
+ es.onopen = () => setLiveDot('#3fb950', 'Live — автообновление включено');
903
+
904
+ es.addEventListener('data-updated', () => {
905
+ setLiveDot('#e3b341', 'Обновляю данные…');
906
+ refreshData();
907
+ });
908
+
909
+ es.onerror = () => {
910
+ setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
911
+ es.close();
912
+ setTimeout(connectSSE, 3000);
913
+ };
914
+ }
915
+
916
+ init().then(() => connectSSE());
753
917
  </script>
754
918
  </body>
755
919
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {