cc-viewer 1.7.12 → 1.7.13

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.
@@ -1,14 +1,14 @@
1
1
  // Local log management routes (moved verbatim from server.js handleRequest).
2
- import { existsSync, realpathSync, statSync, createReadStream, mkdtempSync, rmSync } from 'node:fs';
2
+ import { existsSync, realpathSync, statSync, createReadStream, mkdtempSync, rmSync, readdirSync } from 'node:fs';
3
3
  import { join, basename } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import AdmZip from 'adm-zip';
6
6
  import { LOG_DIR } from '../../findcc.js';
7
7
  import { _projectName, _v2Writer } from '../interceptor.js';
8
- import { listV2Logs, listLocalLogs, countListedV1Files, deleteLogFiles, validateLogPath } from '../lib/log-management.js';
8
+ import { listV2Logs, listV2LogsPage, listLocalLogs, countListedV1Files, deleteLogFiles, validateLogPath } from '../lib/log-management.js';
9
9
  import { countLogEntries, streamRawEntriesAsync, readTailEntries } from '../lib/log-stream.js';
10
10
  import { sseHead, sseWrite, wireEnd } from '../lib/wire-compress.js';
11
- import { dirSizeSync } from '../lib/v2/layout.js';
11
+ import { dirSizeSync, sanitizePathComponent } from '../lib/v2/layout.js';
12
12
  import { extractV2Zip } from '../lib/log-zip.js';
13
13
  import { startConvert, stopConvert, convertStatus } from '../lib/v2/convert-manager.js';
14
14
  import { migrationStatus } from '../lib/v2/migrate-prompt.js';
@@ -32,18 +32,60 @@ async function localLogs(req, res, parsedUrl, isLocal, deps) {
32
32
  res.end(JSON.stringify(v1));
33
33
  return;
34
34
  }
35
- const result = listV2Logs(LOG_DIR, _projectName);
35
+ // Server-side pagination (2026-07-31): ?page=&pageSize= switches the v2
36
+ // list to a per-project page — only the requested page's sessions are
37
+ // summarized. Response shape becomes {items, total, page, pageSize} plus
38
+ // the same _-prefixed side signals below. No params = legacy grouped shape.
39
+ const pageParam = parsedUrl?.searchParams?.get('page');
40
+ let payload;
41
+ if (pageParam != null) {
42
+ // Optional ?project= views another project's logs (the modal's project
43
+ // switcher). Strict-compare against sanitizePathComponent (same pattern
44
+ // as parseV2Ref) so '..'/ separators can't traverse out of LOG_DIR;
45
+ // absent/empty falls back to the active project.
46
+ const rawProject = parsedUrl.searchParams.get('project');
47
+ let target = _projectName;
48
+ if (rawProject) {
49
+ if (rawProject !== sanitizePathComponent(rawProject)) {
50
+ res.writeHead(400, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify({ error: 'Invalid project name' }));
52
+ return;
53
+ }
54
+ target = rawProject;
55
+ }
56
+ const page = Math.max(1, parseInt(pageParam, 10) || 1);
57
+ const pageSize = Math.min(200, Math.max(1, parseInt(parsedUrl.searchParams.get('pageSize'), 10) || 50));
58
+ payload = listV2LogsPage(LOG_DIR, target, { page, pageSize });
59
+ // _currentProject must stay the ACTIVE project (_projectName), not the
60
+ // viewed one (listV2LogsPage sets it to `target`): the frontend's global
61
+ // currentProject drives the sidebar / migration counts / v1 banner, and
62
+ // the viewed project is carried separately by its own logViewProject
63
+ // state. Overwriting it here would let "viewing project A" leak into
64
+ // global state. _viewedProject tells the client which project this page
65
+ // actually lists.
66
+ payload._currentProject = _projectName || '';
67
+ payload._viewedProject = target || '';
68
+ // Project list for the modal's switcher dropdown. LOG_DIR's top level
69
+ // holds one dir per project (recycle dirs live INSIDE each project, so
70
+ // nothing to filter here) — but skip dot-prefixed hidden dirs.
71
+ try {
72
+ payload._allProjects = readdirSync(LOG_DIR, { withFileTypes: true })
73
+ .filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name).sort();
74
+ } catch { payload._allProjects = []; }
75
+ } else {
76
+ payload = listV2Logs(LOG_DIR, _projectName);
77
+ }
36
78
  // Legacy v1 files are not in this list — surface two distinct signals:
37
79
  // - _v1FileCount: v1 files ON DISK (gates the v1-view entry link; the
38
80
  // converter never deletes sources, so this outlives a finished migration)
39
81
  // - _unmigratedV1Count/Bytes: files still AWAITING migration (gates the
40
82
  // migrate button + hint inside the v1 view and the startup prompt)
41
83
  const mig = migrationStatus(LOG_DIR, _projectName || '');
42
- result._unmigratedV1Count = mig.files;
43
- result._unmigratedV1Bytes = mig.totalBytes;
44
- result._v1FileCount = _projectName ? countListedV1Files(join(LOG_DIR, _projectName)) : 0;
84
+ payload._unmigratedV1Count = mig.files;
85
+ payload._unmigratedV1Bytes = mig.totalBytes;
86
+ payload._v1FileCount = _projectName ? countListedV1Files(join(LOG_DIR, _projectName)) : 0;
45
87
  res.writeHead(200, { 'Content-Type': 'application/json' });
46
- res.end(JSON.stringify(result));
88
+ res.end(JSON.stringify(payload));
47
89
  } catch (err) {
48
90
  res.writeHead(500, { 'Content-Type': 'application/json' });
49
91
  res.end(JSON.stringify({ error: err.message }));