fix(stats): exclude soft-deleted pages from visible counts (#2235)

This commit is contained in:
xd-Neji
2026-07-17 11:32:17 -07:00
committed by GitHub
parent 0a021f6f6b
commit cfc120fcb3
7 changed files with 76 additions and 12 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ async function fetchSource(engine: BrainEngine, id: string): Promise<SourceRow |
async function countPages(engine: BrainEngine, sourceId: string): Promise<number> {
const rows = await engine.executeRaw<{ n: number }>(
`SELECT COUNT(*)::int AS n FROM pages WHERE source_id = $1`,
`SELECT COUNT(*)::int AS n FROM pages WHERE source_id = $1 AND deleted_at IS NULL`,
[sourceId],
);
return rows[0]?.n ?? 0;
+1 -1
View File
@@ -5029,7 +5029,7 @@ export class PGLiteEngine implements BrainEngine {
`);
const { rows: types } = await this.db.query(
`SELECT type, count(*)::int as count FROM pages GROUP BY type ORDER BY count DESC`
`SELECT type, count(*)::int as count FROM pages WHERE deleted_at IS NULL GROUP BY type ORDER BY count DESC`
);
const pages_by_type: Record<string, number> = {};
for (const t of types as { type: string; count: number }[]) {
+1 -1
View File
@@ -5008,7 +5008,7 @@ export class PostgresEngine implements BrainEngine {
`;
const types = await sql`
SELECT type, count(*)::int as count FROM pages GROUP BY type ORDER BY count DESC
SELECT type, count(*)::int as count FROM pages WHERE deleted_at IS NULL GROUP BY type ORDER BY count DESC
`;
const pages_by_type: Record<string, number> = {};
for (const t of types) {
+12 -4
View File
@@ -212,7 +212,7 @@ async function fetchSourceRow(engine: BrainEngine, id: string): Promise<SourceRo
return { ...r, config: parseConfig(r.config) };
}
async function countPages(engine: BrainEngine, id: string): Promise<number> {
async function countAllPages(engine: BrainEngine, id: string): Promise<number> {
const rows = await engine.executeRaw<{ n: number }>(
`SELECT COUNT(*)::int AS n FROM pages WHERE source_id = $1`,
[id],
@@ -220,6 +220,14 @@ async function countPages(engine: BrainEngine, id: string): Promise<number> {
return rows[0]?.n ?? 0;
}
async function countVisiblePages(engine: BrainEngine, id: string): Promise<number> {
const rows = await engine.executeRaw<{ n: number }>(
`SELECT COUNT(*)::int AS n FROM pages WHERE source_id = $1 AND deleted_at IS NULL`,
[id],
);
return rows[0]?.n ?? 0;
}
/** Default clone dir for a remote-URL source: $GBRAIN_HOME/clones/<id>/ */
export function defaultCloneDir(id: string): string {
return gbrainPath('clones', id);
@@ -575,7 +583,7 @@ export async function listSources(
local_path: r.local_path,
remote_url: typeof cfg.remote_url === 'string' ? cfg.remote_url : null,
federated: cfg.federated === true,
page_count: await countPages(engine, r.id),
page_count: await countVisiblePages(engine, r.id),
last_sync_at: r.last_sync_at ? new Date(r.last_sync_at).toISOString() : null,
});
}
@@ -621,7 +629,7 @@ export async function removeSource(
throw new SourceOpError('not_found', `Source "${opts.id}" not found.`);
}
const pageCount = await countPages(engine, opts.id);
const pageCount = await countAllPages(engine, opts.id);
if (opts.dryRun) {
return {
@@ -720,7 +728,7 @@ export async function getSourceStatus(
local_path: src.local_path,
remote_url: remoteUrl,
federated: isFederated(src.config),
page_count: await countPages(engine, id),
page_count: await countVisiblePages(engine, id),
last_sync_at: src.last_sync_at ? new Date(src.last_sync_at).toISOString() : null,
last_commit: src.last_commit,
archived,
+12 -5
View File
@@ -951,11 +951,18 @@ describe('PGLiteEngine: Stats & Health', () => {
});
test('getStats returns correct counts', async () => {
const stats = await engine.getStats();
expect(stats.page_count).toBe(1);
expect(stats.chunk_count).toBe(1);
expect(stats.tag_count).toBe(1);
expect(stats.pages_by_type.concept).toBe(1);
await engine.putPage('test/stats-deleted', { ...testPage, title: 'Deleted stats page' });
await engine.softDeletePage('test/stats-deleted');
try {
const stats = await engine.getStats();
expect(stats.page_count).toBe(1);
expect(stats.chunk_count).toBe(1);
expect(stats.tag_count).toBe(1);
expect(stats.pages_by_type.concept).toBe(1);
} finally {
await engine.deletePage('test/stats-deleted');
}
});
test('getHealth returns coverage metrics', async () => {
+35
View File
@@ -301,6 +301,41 @@ describe('listSources', () => {
// ---------------------------------------------------------------------------
describe('removeSource — clone-cleanup', () => {
test('counts soft-deleted pages for destructive removal while list/status show active pages', async () => {
await withEnv2(async () => {
await addSource(engine, { id: 'soft-only', localPath: '/tmp/soft-only-fixture' });
await engine.putPage(
'notes/recoverable',
{
type: 'note',
title: 'Recoverable',
compiled_truth: 'still recoverable during the soft-delete window',
timeline: '',
frontmatter: {},
},
{ sourceId: 'soft-only' },
);
expect(await engine.softDeletePage('notes/recoverable', { sourceId: 'soft-only' }))
.toEqual({ slug: 'notes/recoverable' });
const listed = await listSources(engine);
expect(listed.find((s) => s.id === 'soft-only')?.page_count).toBe(0);
const status = await getSourceStatus(engine, 'soft-only');
expect(status.page_count).toBe(0);
const dryRun = await removeSource(engine, { id: 'soft-only', dryRun: true });
expect(dryRun.pages_deleted).toBe(1);
try {
await removeSource(engine, { id: 'soft-only' });
throw new Error('expected throw');
} catch (e) {
expect(e).toBeInstanceOf(SourceOpError);
expect((e as SourceOpError).message).toContain('with 1 pages');
}
});
});
test('removes clone IFF managed (local_path under $GBRAIN_HOME/clones/ + remote_url set)', async () => {
await withEnv2(async () => {
const row = await addSource(engine, {
+14
View File
@@ -172,6 +172,20 @@ describe('sources list', () => {
const select = calls.find(c => c.sql.includes('ORDER BY (id = \'default\') DESC'));
expect(select).toBeDefined();
});
test('counts only visible pages', async () => {
const { engine, calls } = makeStub({
'SELECT id, name, local_path, last_commit, last_sync_at, config, created_at': [
{ id: 'default', name: 'default', local_path: null, last_commit: null, last_sync_at: null, config: '{"federated":true}', created_at: new Date() },
],
'COUNT(*)::int AS n FROM pages': [{ n: 1 }],
});
await runSources(engine, ['list']);
const count = calls.find(c => c.sql.includes('COUNT(*)::int AS n FROM pages'));
expect(count?.sql).toContain('deleted_at IS NULL');
});
});
// ── remove ──────────────────────────────────────────────────