Initial commit

This commit is contained in:
Jarek Krochmalski
2025-12-28 21:16:03 +01:00
commit 62e3c6439e
552 changed files with 104858 additions and 0 deletions

46
lib/utils/icons.ts Normal file
View File

@@ -0,0 +1,46 @@
import {
Server, Globe, Cloud, Database, HardDrive, Cpu, Network, Box, Container, Monitor,
Laptop, Smartphone, Tablet, Tv, Router, Wifi, Cable, Radio, Satellite, Building,
Building2, Factory, Warehouse, House, Castle, Landmark, Store, School, Hospital,
Terminal, Code, Binary, Braces, FileCode, GitBranch, GitCommitHorizontal, GitPullRequest,
Settings, Cog, Wrench, Hammer, Package, Archive, FolderOpen,
Shield, ShieldCheck, Lock, Key, Eye, EyeOff, TriangleAlert,
Zap, Flame, Snowflake, Sun, Moon, Star, Sparkles, Heart, Crown, Gem,
Anchor, Ship, Plane, Rocket, Car, Bike, TrainFront, Bus, Truck,
Activity, BarChart3, ChartLine, ChartPie, TrendingUp, Gauge, Timer,
Mail, MessageSquare, Phone, Video, Camera, Music, Headphones, Volume2,
MapPin, Map, Compass, Navigation, Flag, Bookmark, Target
} from 'lucide-svelte';
import type { ComponentType } from 'svelte';
// Icon mapping for rendering
const iconMap: Record<string, ComponentType> = {
'server': Server, 'globe': Globe, 'cloud': Cloud, 'database': Database, 'hard-drive': HardDrive,
'cpu': Cpu, 'network': Network, 'box': Box, 'container': Container, 'monitor': Monitor,
'laptop': Laptop, 'smartphone': Smartphone, 'tablet': Tablet, 'tv': Tv, 'router': Router,
'wifi': Wifi, 'cable': Cable, 'radio': Radio, 'satellite': Satellite, 'building': Building,
'building-2': Building2, 'factory': Factory, 'warehouse': Warehouse, 'home': House, 'castle': Castle,
'landmark': Landmark, 'store': Store, 'school': School, 'hospital': Hospital,
'terminal': Terminal, 'code': Code, 'binary': Binary, 'braces': Braces, 'file-code': FileCode,
'git-branch': GitBranch, 'git-commit': GitCommitHorizontal, 'git-pull-request': GitPullRequest,
'settings': Settings, 'cog': Cog, 'wrench': Wrench, 'hammer': Hammer,
'package': Package, 'archive': Archive, 'folder-open': FolderOpen,
'shield': Shield, 'shield-check': ShieldCheck, 'lock': Lock, 'key': Key,
'eye': Eye, 'eye-off': EyeOff, 'alert-triangle': TriangleAlert,
'zap': Zap, 'flame': Flame, 'snowflake': Snowflake, 'sun': Sun, 'moon': Moon,
'star': Star, 'sparkles': Sparkles, 'heart': Heart, 'crown': Crown, 'gem': Gem,
'anchor': Anchor, 'ship': Ship, 'plane': Plane, 'rocket': Rocket, 'car': Car,
'bike': Bike, 'train': TrainFront, 'bus': Bus, 'truck': Truck,
'activity': Activity, 'bar-chart': BarChart3, 'line-chart': ChartLine, 'pie-chart': ChartPie,
'trending-up': TrendingUp, 'gauge': Gauge, 'timer': Timer,
'mail': Mail, 'message-square': MessageSquare, 'phone': Phone, 'video': Video,
'camera': Camera, 'music': Music, 'headphones': Headphones, 'volume-2': Volume2,
'map-pin': MapPin, 'map': Map, 'compass': Compass, 'navigation': Navigation,
'flag': Flag, 'bookmark': Bookmark, 'target': Target
};
export function getIconComponent(iconName: string): ComponentType {
return iconMap[iconName] || Globe;
}
export { iconMap };

15
lib/utils/ip.ts Normal file
View File

@@ -0,0 +1,15 @@
/**
* Convert IP address (with optional CIDR) to numeric value for sorting
* e.g., "192.168.1.0/24" -> 3232235776, "10.0.0.1" -> 167772161
*/
export function ipToNumber(ip: string | undefined | null): number {
if (!ip || ip === '-') return Infinity; // Push empty IPs to the end
// Strip CIDR notation if present
const ipOnly = ip.split('/')[0];
const parts = ipOnly.split('.');
if (parts.length !== 4) return Infinity;
return parts.reduce((acc, octet) => {
const num = parseInt(octet, 10);
return isNaN(num) ? Infinity : (acc << 8) + num;
}, 0) >>> 0; // Convert to unsigned 32-bit
}

107
lib/utils/label-colors.ts Normal file
View File

@@ -0,0 +1,107 @@
/**
* Label color utilities for environment labels
*
* Provides consistent, deterministic color assignment based on label string hash.
* Colors are from Tailwind's color palette for visual consistency.
*/
// Tailwind color palette - vibrant, distinguishable colors
const LABEL_COLORS = [
'#ef4444', // red-500
'#f97316', // orange-500
'#eab308', // yellow-500
'#22c55e', // green-500
'#14b8a6', // teal-500
'#3b82f6', // blue-500
'#8b5cf6', // violet-500
'#ec4899', // pink-500
'#06b6d4', // cyan-500
'#84cc16', // lime-500
'#6366f1', // indigo-500
'#d946ef' // fuchsia-500
];
// Lighter variants for backgrounds (with alpha)
const LABEL_BG_COLORS = [
'rgba(239, 68, 68, 0.15)', // red
'rgba(249, 115, 22, 0.15)', // orange
'rgba(234, 179, 8, 0.15)', // yellow
'rgba(34, 197, 94, 0.15)', // green
'rgba(20, 184, 166, 0.15)', // teal
'rgba(59, 130, 246, 0.15)', // blue
'rgba(139, 92, 246, 0.15)', // violet
'rgba(236, 72, 153, 0.15)', // pink
'rgba(6, 182, 212, 0.15)', // cyan
'rgba(132, 204, 22, 0.15)', // lime
'rgba(99, 102, 241, 0.15)', // indigo
'rgba(217, 70, 239, 0.15)' // fuchsia
];
/**
* Generate a hash from a string for consistent color assignment
*/
function hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
/**
* Get the primary color for a label (for text/borders)
*/
export function getLabelColor(label: string): string {
const index = hashString(label) % LABEL_COLORS.length;
return LABEL_COLORS[index];
}
/**
* Get the background color for a label (lighter, with transparency)
*/
export function getLabelBgColor(label: string): string {
const index = hashString(label) % LABEL_BG_COLORS.length;
return LABEL_BG_COLORS[index];
}
/**
* Get both colors for a label as an object
*/
export function getLabelColors(label: string): { color: string; bgColor: string } {
const index = hashString(label) % LABEL_COLORS.length;
return {
color: LABEL_COLORS[index],
bgColor: LABEL_BG_COLORS[index]
};
}
/**
* Maximum number of labels allowed per environment
*/
export const MAX_LABELS = 10;
/**
* Parse labels from JSON string or array (handles both database and API formats)
*/
export function parseLabels(labels: string | string[] | null | undefined): string[] {
if (!labels) return [];
// Already an array - return as-is
if (Array.isArray(labels)) return labels;
// JSON string from database
try {
const parsed = JSON.parse(labels);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
/**
* Serialize labels to JSON string for database storage
*/
export function serializeLabels(labels: string[]): string | null {
if (!labels || labels.length === 0) return null;
return JSON.stringify(labels);
}

160
lib/utils/update-steps.ts Normal file
View File

@@ -0,0 +1,160 @@
/**
* Shared utilities for update step visualization across:
* - BatchUpdateModal (manual update from containers grid)
* - Scheduled container auto-updates
* - Scheduled environment update checks
*/
import {
Download,
Shield,
Square,
Trash2,
Box,
Play,
CheckCircle2,
XCircle,
ShieldBan,
Circle,
Loader2,
ShieldAlert,
ShieldCheck,
ShieldOff,
ShieldX
} from 'lucide-svelte';
import type { ComponentType } from 'svelte';
import type { VulnerabilityCriteria } from '$lib/server/db';
// Step types for update process
export type StepType =
| 'pulling'
| 'scanning'
| 'stopping'
| 'removing'
| 'creating'
| 'starting'
| 'done'
| 'failed'
| 'blocked'
| 'checked'
| 'skipped'
| 'updated';
// Get icon component for a step
export function getStepIcon(step: StepType): ComponentType {
switch (step) {
case 'pulling':
return Download;
case 'scanning':
return Shield;
case 'stopping':
return Square;
case 'removing':
return Trash2;
case 'creating':
return Box;
case 'starting':
return Play;
case 'done':
case 'updated':
return CheckCircle2;
case 'failed':
return XCircle;
case 'blocked':
return ShieldBan;
case 'checked':
case 'skipped':
return Circle;
default:
return Loader2;
}
}
// Get human-readable label for a step
export function getStepLabel(step: StepType): string {
switch (step) {
case 'pulling':
return 'Pulling image';
case 'scanning':
return 'Scanning for vulnerabilities';
case 'stopping':
return 'Stopping';
case 'removing':
return 'Removing';
case 'creating':
return 'Creating';
case 'starting':
return 'Starting';
case 'done':
return 'Done';
case 'updated':
return 'Updated';
case 'failed':
return 'Failed';
case 'blocked':
return 'Blocked by vulnerabilities';
case 'checked':
return 'Checked';
case 'skipped':
return 'Up-to-date';
default:
return step;
}
}
// Get color classes for a step
export function getStepColor(step: StepType): string {
switch (step) {
case 'done':
case 'updated':
return 'text-green-600 dark:text-green-400';
case 'failed':
return 'text-red-600 dark:text-red-400';
case 'blocked':
return 'text-amber-600 dark:text-amber-400';
case 'scanning':
return 'text-purple-600 dark:text-purple-400';
case 'checked':
case 'skipped':
return 'text-muted-foreground';
default:
return 'text-blue-600 dark:text-blue-400';
}
}
// Vulnerability criteria labels
export const vulnerabilityCriteriaLabels: Record<VulnerabilityCriteria, string> = {
never: 'Never block',
any: 'Any vulnerability',
critical_high: 'Critical or high',
critical: 'Critical only',
more_than_current: 'More than current image'
};
// Vulnerability criteria icons with colors and titles
export const vulnerabilityCriteriaIcons: Record<
VulnerabilityCriteria,
{ component: ComponentType; class: string; title: string }
> = {
never: { component: ShieldOff, class: 'w-3.5 h-3.5 text-muted-foreground', title: 'No vulnerability blocking' },
any: { component: ShieldAlert, class: 'w-3.5 h-3.5 text-amber-500', title: 'Block on any vulnerability' },
critical_high: { component: ShieldX, class: 'w-3.5 h-3.5 text-orange-500', title: 'Block on critical or high' },
critical: { component: ShieldX, class: 'w-3.5 h-3.5 text-red-500', title: 'Block on critical only' },
more_than_current: { component: Shield, class: 'w-3.5 h-3.5 text-blue-500', title: 'Block if more than current' }
};
// Get badge variant based on criteria severity
export function getCriteriaBadgeClass(criteria: VulnerabilityCriteria): string {
switch (criteria) {
case 'any':
return 'bg-red-500/10 text-red-600 border-red-500/30';
case 'critical_high':
return 'bg-orange-500/10 text-orange-600 border-orange-500/30';
case 'critical':
return 'bg-amber-500/10 text-amber-600 border-amber-500/30';
case 'more_than_current':
return 'bg-blue-500/10 text-blue-600 border-blue-500/30';
default:
return 'bg-slate-500/10 text-slate-600 border-slate-500/30';
}
}

35
lib/utils/version.ts Normal file
View File

@@ -0,0 +1,35 @@
/**
* Compares two semantic version strings.
* @returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2
*/
export function compareVersions(v1: string, v2: string): number {
const normalize = (v: string) =>
v
.replace(/^v/, '')
.split('.')
.map(Number);
const parts1 = normalize(v1);
const parts2 = normalize(v2);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
/**
* Determines if the "What's New" popup should be shown.
* @param currentVersion - The current app version (from git tag)
* @param lastSeenVersion - The last version the user has seen (from localStorage)
*/
export function shouldShowWhatsNew(
currentVersion: string | null,
lastSeenVersion: string | null
): boolean {
if (!currentVersion || currentVersion === 'unknown') return false;
if (!lastSeenVersion) return true; // Never seen any version
return compareVersions(currentVersion, lastSeenVersion) > 0;
}