Skip to content

Commit 4cd04c8

Browse files
authored
Merge pull request #26 from MHaggis/MHaggis/fix-link-previews
Track nightly sync runs and fix dashboard sync status
2 parents 634147d + 576847d commit 4cd04c8

3 files changed

Lines changed: 145 additions & 18 deletions

File tree

.github/workflows/nightly-sync.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,6 @@ jobs:
9696
- name: Seed Supabase from SQLite
9797
run: cd web && npx tsx scripts/seed-from-sqlite.ts
9898
env:
99+
SYNC_SOURCE_TYPE: nightly_full
99100
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
100101
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}

web/scripts/seed-from-sqlite.ts

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,130 @@ if (!supabaseUrl || !supabaseKey) {
3434

3535
const supabase = createClient(supabaseUrl, supabaseKey);
3636
const sqlite = new Database(SQLITE_PATH, { readonly: true });
37+
const SYNC_SOURCE_TYPE = process.env.SYNC_SOURCE_TYPE || 'nightly_full';
38+
const MAX_SYNC_ERROR_CHARS = 2000;
3739

3840
async function main() {
39-
console.log('=== Seeding Supabase from SQLite ===');
40-
console.log(`SQLite: ${SQLITE_PATH}`);
41-
console.log(`Supabase: ${supabaseUrl}`);
42-
console.log('');
43-
44-
await seedDetections();
45-
await seedDetectionTechniques();
46-
await seedTechniqueTactics();
47-
await seedAttackTechniques();
48-
await seedAttackActors();
49-
await seedAttackSoftware();
50-
await seedActorTechniques();
51-
await seedSoftwareTechniques();
52-
await seedProcedureReference();
53-
await seedStories();
54-
55-
console.log('\n=== Seed complete! ===');
56-
sqlite.close();
41+
let syncRunId: string | null = null;
42+
let preRunDetections = 0;
43+
44+
try {
45+
console.log('=== Seeding Supabase from SQLite ===');
46+
console.log(`SQLite: ${SQLITE_PATH}`);
47+
console.log(`Supabase: ${supabaseUrl}`);
48+
console.log(`Sync source: ${SYNC_SOURCE_TYPE}`);
49+
console.log('');
50+
51+
preRunDetections = await getDetectionCount();
52+
syncRunId = await startSyncRun(preRunDetections);
53+
54+
await seedDetections();
55+
await seedDetectionTechniques();
56+
await seedTechniqueTactics();
57+
await seedAttackTechniques();
58+
await seedAttackActors();
59+
await seedAttackSoftware();
60+
await seedActorTechniques();
61+
await seedSoftwareTechniques();
62+
await seedProcedureReference();
63+
await seedStories();
64+
65+
const postRunDetections = await getDetectionCount();
66+
const detectionsAdded = Math.max(postRunDetections - preRunDetections, 0);
67+
68+
await completeSyncRun(syncRunId, {
69+
detectionsTotal: postRunDetections,
70+
detectionsAdded,
71+
detectionsUpdated: 0,
72+
});
73+
74+
console.log('\n=== Seed complete! ===');
75+
} catch (err) {
76+
if (syncRunId) {
77+
await failSyncRun(syncRunId, formatSyncError(err));
78+
}
79+
throw err;
80+
} finally {
81+
sqlite.close();
82+
}
83+
}
84+
85+
async function getDetectionCount(): Promise<number> {
86+
const { count, error } = await supabase
87+
.from('detections')
88+
.select('id', { count: 'exact', head: true });
89+
90+
if (error || count === null) {
91+
throw new Error(`Failed to query detection count: ${error?.message || 'count unavailable'}`);
92+
}
93+
94+
return count;
95+
}
96+
97+
async function startSyncRun(preRunDetections: number): Promise<string> {
98+
const { data, error } = await supabase
99+
.from('sync_runs')
100+
.insert({
101+
source_type: SYNC_SOURCE_TYPE,
102+
started_at: new Date().toISOString(),
103+
detections_total: preRunDetections,
104+
detections_added: 0,
105+
detections_updated: 0,
106+
status: 'running',
107+
error: null,
108+
})
109+
.select('id')
110+
.single();
111+
112+
if (error || !data?.id) {
113+
throw new Error(`Failed to create sync run: ${error?.message || 'missing sync run id'}`);
114+
}
115+
116+
console.log(`Started sync run: ${data.id}`);
117+
return data.id as string;
118+
}
119+
120+
async function completeSyncRun(
121+
syncRunId: string,
122+
metrics: { detectionsTotal: number; detectionsAdded: number; detectionsUpdated: number }
123+
): Promise<void> {
124+
const { error } = await supabase
125+
.from('sync_runs')
126+
.update({
127+
completed_at: new Date().toISOString(),
128+
detections_total: metrics.detectionsTotal,
129+
detections_added: metrics.detectionsAdded,
130+
detections_updated: metrics.detectionsUpdated,
131+
status: 'completed',
132+
error: null,
133+
})
134+
.eq('id', syncRunId);
135+
136+
if (error) {
137+
throw new Error(`Failed to finalize sync run ${syncRunId}: ${error.message}`);
138+
}
139+
}
140+
141+
async function failSyncRun(syncRunId: string, errorMessage: string): Promise<void> {
142+
const { error } = await supabase
143+
.from('sync_runs')
144+
.update({
145+
completed_at: new Date().toISOString(),
146+
status: 'failed',
147+
error: errorMessage,
148+
})
149+
.eq('id', syncRunId);
150+
151+
if (error) {
152+
console.error(`Failed to mark sync run ${syncRunId} as failed: ${error.message}`);
153+
}
154+
}
155+
156+
function formatSyncError(err: unknown): string {
157+
const message = err instanceof Error
158+
? `${err.message}${err.stack ? `\n${err.stack}` : ''}`
159+
: String(err);
160+
return message.slice(0, MAX_SYNC_ERROR_CHARS);
57161
}
58162

59163
async function upsertBatch(table: string, rows: Record<string, unknown>[], batchSize = 500) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- Backfill one bootstrap sync run so dashboard has immediate status
2+
-- Idempotent: only inserts if sync_runs currently has zero rows
3+
INSERT INTO sync_runs (
4+
source_type,
5+
started_at,
6+
completed_at,
7+
detections_added,
8+
detections_updated,
9+
detections_total,
10+
status,
11+
error
12+
)
13+
SELECT
14+
'bootstrap',
15+
now(),
16+
now(),
17+
0,
18+
0,
19+
(SELECT COUNT(*) FROM detections),
20+
'completed',
21+
NULL
22+
WHERE NOT EXISTS (SELECT 1 FROM sync_runs);

0 commit comments

Comments
 (0)