Skip to content

Commit b38f0ba

Browse files
committed
style: format files for CI
1 parent f37c5b6 commit b38f0ba

4 files changed

Lines changed: 159 additions & 174 deletions

File tree

src/lib/data/asvs-assessment.json

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@
5656
"asvsRef": "V1.2.1",
5757
"automatable": true,
5858
"level": "L1",
59-
"top10": [
60-
"A04"
61-
]
59+
"top10": ["A04"]
6260
},
6361
{
6462
"id": "V1.5.1",
@@ -96,10 +94,7 @@
9694
"asvsRef": "V1.5.1",
9795
"automatable": true,
9896
"level": "L1",
99-
"top10": [
100-
"A03",
101-
"A04"
102-
]
97+
"top10": ["A03", "A04"]
10398
},
10499
{
105100
"id": "V2.1.1",
@@ -111,10 +106,7 @@
111106
"asvsRef": "V2.1.1",
112107
"automatable": true,
113108
"level": "L1",
114-
"top10": [
115-
"A02",
116-
"A07"
117-
]
109+
"top10": ["A02", "A07"]
118110
},
119111
{
120112
"id": "V2.1.5",
@@ -1065,4 +1057,4 @@
10651057
"level": "L1"
10661058
}
10671059
]
1068-
}
1060+
}

src/routes/api/seo-dashboard/+server.ts

Lines changed: 142 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -8,157 +8,157 @@
88
import { json } from '@sveltejs/kit';
99
import { SEO_TARGETS } from '$lib/seo-targets.js';
1010
import {
11-
healthQueries,
12-
targetPerformanceQuery,
13-
sitewideQueries,
14-
analyseTarget,
15-
checkRateLimit,
16-
rateLimitHeaders,
17-
type DataHealth,
18-
type QueryRow,
19-
type TargetResult,
20-
type SitewideQuery,
21-
type SitewidePage,
22-
type ContentGap,
23-
type Search404,
24-
type SeoAction
11+
healthQueries,
12+
targetPerformanceQuery,
13+
sitewideQueries,
14+
analyseTarget,
15+
checkRateLimit,
16+
rateLimitHeaders,
17+
type DataHealth,
18+
type QueryRow,
19+
type TargetResult,
20+
type SitewideQuery,
21+
type SitewidePage,
22+
type ContentGap,
23+
type Search404,
24+
type SeoAction
2525
} from '@esolia/core/seo';
2626
import type { RequestHandler } from './$types.js';
2727

2828
// ── Helpers ────────────────────────────────────────────────────────
2929

3030
function getDb(platform: App.Platform | undefined): D1Database | undefined {
31-
// InfoSec: Cloudflare adapter Proxy throws (not returns undefined) during
32-
// prerendering / local dev without D1. Wrap in try-catch.
33-
try {
34-
return platform?.env?.SEARCH_INTEL_DB;
35-
} catch {
36-
return undefined;
37-
}
31+
// InfoSec: Cloudflare adapter Proxy throws (not returns undefined) during
32+
// prerendering / local dev without D1. Wrap in try-catch.
33+
try {
34+
return platform?.env?.SEARCH_INTEL_DB;
35+
} catch {
36+
return undefined;
37+
}
3838
}
3939

4040
// ── Handler ────────────────────────────────────────────────────────
4141

4242
export const GET: RequestHandler = async ({ platform, getClientAddress }) => {
43-
// InfoSec: Application-level rate limiting (defense-in-depth behind WAF rules)
44-
const rl = await checkRateLimit(platform?.env?.CACHE_KV, `seo-dash:${getClientAddress()}`, {
45-
limit: 10,
46-
window: 60
47-
});
48-
if (!rl.allowed) {
49-
return json(
50-
{ error: 'Rate limit exceeded. Try again later.' },
51-
{ status: 429, headers: { 'Cache-Control': 'no-store', ...rateLimitHeaders(rl, 10) } }
52-
);
53-
}
54-
55-
const db = getDb(platform);
56-
57-
if (!db) {
58-
return json(
59-
{
60-
error: 'D1 database unavailable',
61-
hint: 'SEARCH_INTEL_DB binding not found. This endpoint requires Cloudflare Workers with D1.'
62-
},
63-
{
64-
status: 503,
65-
headers: { 'Cache-Control': 'no-store' }
66-
}
67-
);
68-
}
69-
70-
try {
71-
// ── Data health (single batch) ─────────────────────────────
72-
const healthStmts = healthQueries().map((q) => db.prepare(q.sql).bind(...q.params));
73-
const healthResults = await db.batch(healthStmts);
74-
75-
const row0 = healthResults[0]?.results[0] as { cnt: number } | undefined;
76-
const row1 = healthResults[1]?.results[0] as
77-
| { earliest: string | null; latest: string | null }
78-
| undefined;
79-
const row2 = healthResults[2]?.results[0] as { cnt: number } | undefined;
80-
const row3 = healthResults[3]?.results[0] as { cnt: number } | undefined;
81-
const row4 = healthResults[4]?.results[0] as { cnt: number } | undefined;
82-
83-
const dataHealth: DataHealth = {
84-
totalRows: row0?.cnt ?? 0,
85-
earliest: row1?.earliest ?? null,
86-
latest: row1?.latest ?? null,
87-
uniqueQueries: row2?.cnt ?? 0,
88-
uniquePages: row3?.cnt ?? 0,
89-
notFoundCount: row4?.cnt ?? 0
90-
};
91-
92-
// ── Per-target queries (FTS5 trigram pre-filter + LIKE refinement) ──
93-
const targetStmts = SEO_TARGETS.map((t) => {
94-
const q = targetPerformanceQuery(t.keywords);
95-
return db.prepare(q.sql).bind(...q.params);
96-
});
97-
98-
const targetResults = await db.batch(targetStmts);
99-
100-
const targets: TargetResult[] = SEO_TARGETS.map((t, i) => {
101-
const rows = (targetResults[i]?.results ?? []) as QueryRow[];
102-
const totalImpressions = rows.reduce((s, r) => s + r.impressions, 0);
103-
const totalClicks = rows.reduce((s, r) => s + r.clicks, 0);
104-
const avgPosition =
105-
totalImpressions > 0
106-
? rows.reduce((s, r) => s + r.position * r.impressions, 0) / totalImpressions
107-
: 0;
108-
const avgCtr = totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0;
109-
110-
return {
111-
name: t.name,
112-
audience: t.audience,
113-
keywords: t.keywords,
114-
targetPages: t.targetPages,
115-
performance: {
116-
totalImpressions,
117-
totalClicks,
118-
avgPosition: Math.round(avgPosition * 10) / 10,
119-
avgCtr: Math.round(avgCtr * 100) / 100,
120-
topQueries: rows.slice(0, 10)
121-
},
122-
actions: analyseTarget(t, rows)
123-
};
124-
});
125-
126-
// ── Sitewide overview (single batch) ──────────────────────
127-
const swStmts = sitewideQueries().map((q) => db.prepare(q.sql).bind(...q.params));
128-
const sitewideResults = await db.batch(swStmts);
129-
130-
const sitewide = {
131-
topQueries: (sitewideResults[0]?.results ?? []) as SitewideQuery[],
132-
topPages: (sitewideResults[1]?.results ?? []) as SitewidePage[],
133-
contentGaps: (sitewideResults[2]?.results ?? []) as ContentGap[],
134-
search404s: (sitewideResults[3]?.results ?? []) as Search404[],
135-
recentActions: (sitewideResults[4]?.results ?? []) as SeoAction[]
136-
};
137-
138-
return json(
139-
{
140-
generated: new Date().toISOString(),
141-
dataHealth,
142-
targets,
143-
sitewide
144-
},
145-
{
146-
headers: { 'Cache-Control': 'no-store' }
147-
}
148-
);
149-
} catch (err: unknown) {
150-
const message = err instanceof Error ? err.message : String(err);
151-
console.error('SEO dashboard query failed:', message);
152-
return json(
153-
{
154-
error: 'Query failed',
155-
message,
156-
hint: 'Check that the search-intelligence D1 schema has been applied (schema.sql).'
157-
},
158-
{
159-
status: 500,
160-
headers: { 'Cache-Control': 'no-store' }
161-
}
162-
);
163-
}
43+
// InfoSec: Application-level rate limiting (defense-in-depth behind WAF rules)
44+
const rl = await checkRateLimit(platform?.env?.CACHE_KV, `seo-dash:${getClientAddress()}`, {
45+
limit: 10,
46+
window: 60
47+
});
48+
if (!rl.allowed) {
49+
return json(
50+
{ error: 'Rate limit exceeded. Try again later.' },
51+
{ status: 429, headers: { 'Cache-Control': 'no-store', ...rateLimitHeaders(rl, 10) } }
52+
);
53+
}
54+
55+
const db = getDb(platform);
56+
57+
if (!db) {
58+
return json(
59+
{
60+
error: 'D1 database unavailable',
61+
hint: 'SEARCH_INTEL_DB binding not found. This endpoint requires Cloudflare Workers with D1.'
62+
},
63+
{
64+
status: 503,
65+
headers: { 'Cache-Control': 'no-store' }
66+
}
67+
);
68+
}
69+
70+
try {
71+
// ── Data health (single batch) ─────────────────────────────
72+
const healthStmts = healthQueries().map((q) => db.prepare(q.sql).bind(...q.params));
73+
const healthResults = await db.batch(healthStmts);
74+
75+
const row0 = healthResults[0]?.results[0] as { cnt: number } | undefined;
76+
const row1 = healthResults[1]?.results[0] as
77+
| { earliest: string | null; latest: string | null }
78+
| undefined;
79+
const row2 = healthResults[2]?.results[0] as { cnt: number } | undefined;
80+
const row3 = healthResults[3]?.results[0] as { cnt: number } | undefined;
81+
const row4 = healthResults[4]?.results[0] as { cnt: number } | undefined;
82+
83+
const dataHealth: DataHealth = {
84+
totalRows: row0?.cnt ?? 0,
85+
earliest: row1?.earliest ?? null,
86+
latest: row1?.latest ?? null,
87+
uniqueQueries: row2?.cnt ?? 0,
88+
uniquePages: row3?.cnt ?? 0,
89+
notFoundCount: row4?.cnt ?? 0
90+
};
91+
92+
// ── Per-target queries (FTS5 trigram pre-filter + LIKE refinement) ──
93+
const targetStmts = SEO_TARGETS.map((t) => {
94+
const q = targetPerformanceQuery(t.keywords);
95+
return db.prepare(q.sql).bind(...q.params);
96+
});
97+
98+
const targetResults = await db.batch(targetStmts);
99+
100+
const targets: TargetResult[] = SEO_TARGETS.map((t, i) => {
101+
const rows = (targetResults[i]?.results ?? []) as QueryRow[];
102+
const totalImpressions = rows.reduce((s, r) => s + r.impressions, 0);
103+
const totalClicks = rows.reduce((s, r) => s + r.clicks, 0);
104+
const avgPosition =
105+
totalImpressions > 0
106+
? rows.reduce((s, r) => s + r.position * r.impressions, 0) / totalImpressions
107+
: 0;
108+
const avgCtr = totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0;
109+
110+
return {
111+
name: t.name,
112+
audience: t.audience,
113+
keywords: t.keywords,
114+
targetPages: t.targetPages,
115+
performance: {
116+
totalImpressions,
117+
totalClicks,
118+
avgPosition: Math.round(avgPosition * 10) / 10,
119+
avgCtr: Math.round(avgCtr * 100) / 100,
120+
topQueries: rows.slice(0, 10)
121+
},
122+
actions: analyseTarget(t, rows)
123+
};
124+
});
125+
126+
// ── Sitewide overview (single batch) ──────────────────────
127+
const swStmts = sitewideQueries().map((q) => db.prepare(q.sql).bind(...q.params));
128+
const sitewideResults = await db.batch(swStmts);
129+
130+
const sitewide = {
131+
topQueries: (sitewideResults[0]?.results ?? []) as SitewideQuery[],
132+
topPages: (sitewideResults[1]?.results ?? []) as SitewidePage[],
133+
contentGaps: (sitewideResults[2]?.results ?? []) as ContentGap[],
134+
search404s: (sitewideResults[3]?.results ?? []) as Search404[],
135+
recentActions: (sitewideResults[4]?.results ?? []) as SeoAction[]
136+
};
137+
138+
return json(
139+
{
140+
generated: new Date().toISOString(),
141+
dataHealth,
142+
targets,
143+
sitewide
144+
},
145+
{
146+
headers: { 'Cache-Control': 'no-store' }
147+
}
148+
);
149+
} catch (err: unknown) {
150+
const message = err instanceof Error ? err.message : String(err);
151+
console.error('SEO dashboard query failed:', message);
152+
return json(
153+
{
154+
error: 'Query failed',
155+
message,
156+
hint: 'Check that the search-intelligence D1 schema has been applied (schema.sql).'
157+
},
158+
{
159+
status: 500,
160+
headers: { 'Cache-Control': 'no-store' }
161+
}
162+
);
163+
}
164164
};

src/routes/dash/+page.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@
6060
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
6161
<h1 class="mb-2 text-2xl font-semibold text-accent-900 dark:text-accent-100">SEO Dashboard</h1>
6262
<p class="mb-8 text-sm text-accent-500 dark:text-accent-400">
63-
Target keyword tracking &middot; Search performance &middot; Action items
64-
&middot; <a href="/dash/security-assessment" class="underline hover:text-accent-700 dark:hover:text-accent-200">ASVS Assessment</a>
63+
Target keyword tracking &middot; Search performance &middot; Action items &middot; <a
64+
href="/dash/security-assessment"
65+
class="underline hover:text-accent-700 dark:hover:text-accent-200">ASVS Assessment</a
66+
>
6567
</p>
6668

6769
<!-- Pipeline Visualization -->

0 commit comments

Comments
 (0)