mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
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:
+30
-1
@@ -7258,7 +7258,36 @@ app.post('/api/system/networks', async (req: Request, res: Response) => {
|
||||
app.get('/api/templates', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const templates = await templateService.getTemplates();
|
||||
res.json(templates);
|
||||
|
||||
const imageRefs = templates.map(t => t.image).filter((i): i is string => !!i);
|
||||
const scanSummary = DatabaseService.getInstance().getLatestScanSummaryByImageRefs(req.nodeId, imageRefs);
|
||||
|
||||
let featuredIndex = -1;
|
||||
let featuredStars = 0;
|
||||
templates.forEach((t, i) => {
|
||||
const s = t.stars ?? 0;
|
||||
if (s > featuredStars) {
|
||||
featuredStars = s;
|
||||
featuredIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
const enriched = templates.map((t, i) => {
|
||||
const summary = t.image ? scanSummary.get(t.image) : undefined;
|
||||
const scan_status: 'clean' | 'vulnerable' | 'unscanned' = summary
|
||||
? (summary.total === 0 ? 'clean' : 'vulnerable')
|
||||
: 'unscanned';
|
||||
return {
|
||||
...t,
|
||||
scan_status,
|
||||
scan_cve_count: summary?.total ?? 0,
|
||||
scan_critical_count: summary?.critical ?? 0,
|
||||
scan_high_count: summary?.high ?? 0,
|
||||
featured: i === featuredIndex,
|
||||
};
|
||||
});
|
||||
|
||||
res.json(enriched);
|
||||
} catch (error) {
|
||||
console.error('[Templates] Failed to fetch:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch templates' });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user