feat(app-store): editorial hero, category rail, security scan signal per tile (#679)

* feat(app-store): editorial hero, category rail, security scan signal per tile

Rework the App Store view into an editorial layout with a 180px category
rail, a featured-template hero, and compact tiles that surface star counts
and vulnerability scan status at a glance. The deploy sheet splits into
Essentials (one-click deploy) and Advanced (full port/volume/env control)
tabs.

Backend enriches /api/templates with scan_status, scan_cve_count, and a
featured flag computed from the highest-star template with recorded stars.
Scan lookups use a single batched SQL query to avoid N+1 round-trips.

* fix(app-store): split firstSentence helper into its own module

The firstSentence helper lived alongside the TemplateLogo component, which
violates react-refresh/only-export-components — a file that exports a
component must not also export non-component values, or Fast Refresh cannot
establish an HMR boundary. Move the helper into appstore/util.ts and update
the two import sites.
This commit is contained in:
Anso
2026-04-18 12:55:50 -04:00
committed by GitHub
parent 5f6fdfcba8
commit ec7620675e
14 changed files with 645 additions and 302 deletions
+38
View File
@@ -2508,6 +2508,44 @@ export class DatabaseService {
);
}
public getLatestScanSummaryByImageRefs(
nodeId: number,
imageRefs: string[],
): Map<string, { total: number; critical: number; high: number; scannedAt: number }> {
const summary = new Map<string, { total: number; critical: number; high: number; scannedAt: number }>();
if (imageRefs.length === 0) return summary;
const placeholders = imageRefs.map(() => '?').join(',');
const rows = this.db
.prepare(
`SELECT image_ref, total_vulnerabilities, critical_count, high_count, scanned_at
FROM vulnerability_scans v1
WHERE node_id = ?
AND image_ref IN (${placeholders})
AND scanned_at = (
SELECT MAX(scanned_at) FROM vulnerability_scans v2
WHERE v2.node_id = v1.node_id AND v2.image_ref = v1.image_ref
)`,
)
.all(nodeId, ...imageRefs) as Array<{
image_ref: string;
total_vulnerabilities: number;
critical_count: number;
high_count: number;
scanned_at: number;
}>;
for (const row of rows) {
summary.set(row.image_ref, {
total: row.total_vulnerabilities,
critical: row.critical_count,
high: row.high_count,
scannedAt: row.scanned_at,
});
}
return summary;
}
public getLatestScanByDigest(digest: string, scannersUsed?: string): VulnerabilityScan | null {
if (!digest) return null;
if (scannersUsed) {