) {
- {isTransient ? : serviceTypeIcon(service.type, 16)}
+ {isTransient ? (
+
+ ) : (
+
+ )}
diff --git a/app/frontend/src/features/workspace/canvas/service-visuals.tsx b/app/frontend/src/features/workspace/canvas/service-visuals.tsx
index e50576a..0fe768d 100644
--- a/app/frontend/src/features/workspace/canvas/service-visuals.tsx
+++ b/app/frontend/src/features/workspace/canvas/service-visuals.tsx
@@ -29,3 +29,84 @@ export function serviceTypeColor(type: string): string {
return '#9295a4';
}
}
+
+export type ServiceBrandIcon = {
+ slug: string;
+ color: string;
+};
+
+const TECH_ICONS: (ServiceBrandIcon & { patterns: RegExp[] })[] = [
+ { slug: 'postgresql', color: '#7ab8ff', patterns: [/postgres/i, /postgis/i] },
+ { slug: 'redis', color: '#ff6b6b', patterns: [/redis/i, /dragonfly/i, /valkey/i, /keydb/i] },
+ { slug: 'mysql', color: '#5ba4e5', patterns: [/mysql/i] },
+ { slug: 'mariadb', color: '#4ea5ff', patterns: [/mariadb/i] },
+ { slug: 'mongodb', color: '#47a248', patterns: [/mongo/i] },
+ { slug: 'clickhouse', color: '#ffcc01', patterns: [/clickhouse/i] },
+ { slug: 'couchdb', color: '#e42528', patterns: [/couchdb/i] },
+ { slug: 'sqlite', color: '#69b7e4', patterns: [/sqlite/i] },
+ { slug: 'influxdb', color: '#22adf6', patterns: [/influx/i] },
+ { slug: 'elasticsearch', color: '#fec514', patterns: [/elastic/i, /opensearch/i] },
+ { slug: 'meilisearch', color: '#ff5caa', patterns: [/meili/i] },
+ { slug: 'typesense', color: '#d90368', patterns: [/typesense/i] },
+ { slug: 'qdrant', color: '#dc244c', patterns: [/qdrant/i] },
+ { slug: 'weaviate', color: '#01c9a7', patterns: [/weaviate/i] },
+ { slug: 'minio', color: '#c72e49', patterns: [/minio/i] },
+ { slug: 'rabbitmq', color: '#ff6600', patterns: [/rabbitmq/i, /rabbit/i] },
+ { slug: 'apachekafka', color: '#d6dae4', patterns: [/kafka/i] },
+ { slug: 'natsdotio', color: '#8be1d3', patterns: [/\bnats/i] },
+ { slug: 'nginx', color: '#009639', patterns: [/nginx/i] },
+ { slug: 'caddy', color: '#1f88c0', patterns: [/caddy/i] },
+ { slug: 'traefikproxy', color: '#24a1c1', patterns: [/traefik/i] },
+ { slug: 'nodedotjs', color: '#5fa04e', patterns: [/node(?=[:\-\d]|$)/i, /nodejs/i] },
+ { slug: 'go', color: '#00add8', patterns: [/golang/i, /(?:^|[^a-z])go(?=[:\-\d]|$)/i] },
+ { slug: 'python', color: '#6fa8dc', patterns: [/python/i, /\bpy(?:thon)?(?=[:\-\d]|$)/i] },
+ { slug: 'rust', color: '#f74c00', patterns: [/rust/i] },
+ { slug: 'php', color: '#8892bf', patterns: [/php/i] },
+ { slug: 'ruby', color: '#cc342d', patterns: [/ruby/i, /rails/i] },
+ { slug: 'openjdk', color: '#e6a23c', patterns: [/java/i, /openjdk/i, /jdk/i, /jre/i] },
+ { slug: 'dotnet', color: '#6f4bd8', patterns: [/dotnet/i, /aspnet/i] },
+ { slug: 'deno', color: '#70ffaf', patterns: [/\bdeno/i] },
+ { slug: 'bun', color: '#f9f1e1', patterns: [/bun(?=[:\-\d]|$)/i] },
+ { slug: 'react', color: '#61dafb', patterns: [/react/i] },
+ { slug: 'vuedotjs', color: '#4fc08d', patterns: [/vue/i] },
+ { slug: 'angular', color: '#dd0031', patterns: [/angular/i] },
+ { slug: 'svelte', color: '#ff3e00', patterns: [/svelte/i] },
+ { slug: 'nextdotjs', color: '#e6edf3', patterns: [/nextjs/i, /next\.js/i] },
+ { slug: 'nuxtdotjs', color: '#00dc82', patterns: [/nuxt/i] },
+ { slug: 'astro', color: '#ff5d01', patterns: [/astro/i] },
+ { slug: 'prometheus', color: '#e6522c', patterns: [/prometheus/i] },
+ { slug: 'grafana', color: '#f46800', patterns: [/grafana/i] },
+ { slug: 'uptimekuma', color: '#5cdd8b', patterns: [/uptime.?kuma/i, /\bkuma/i] },
+ { slug: 'n8n', color: '#ea4b71', patterns: [/n8n/i] },
+ { slug: 'wordpress', color: '#4b9cd3', patterns: [/wordpress/i, /\bwp-/i] },
+ { slug: 'ghost', color: '#a8b8c4', patterns: [/ghost/i] },
+ { slug: 'nextcloud', color: '#0082c9', patterns: [/nextcloud/i] },
+ { slug: 'immich', color: '#7c9ef5', patterns: [/immich/i] },
+ { slug: 'supabase', color: '#3fcf8e', patterns: [/supabase/i] },
+ { slug: 'pocketbase', color: '#b8dbe4', patterns: [/pocketbase/i] },
+ { slug: 'keycloak', color: '#d6dae4', patterns: [/keycloak/i] },
+ { slug: 'authentik', color: '#fd4b2d', patterns: [/authentik/i] },
+ { slug: 'portainer', color: '#13bef9', patterns: [/portainer/i] },
+ { slug: 'docker', color: '#2496ed', patterns: [/docker/i, /dind/i] },
+ { slug: 'kubernetes', color: '#326ce5', patterns: [/kubernetes/i, /\bk8s/i] },
+];
+
+export type ServiceIconInput = { image?: string; name?: string; type?: string };
+
+export function serviceIcon(service: ServiceIconInput): ServiceBrandIcon | null {
+ const text = `${service.image ?? ''} ${service.name ?? ''}`.toLowerCase();
+ for (const icon of TECH_ICONS) {
+ if (icon.patterns.some((pattern) => pattern.test(text))) {
+ return icon;
+ }
+ }
+ return null;
+}
+
+export function serviceIconUrl(icon: ServiceBrandIcon): string {
+ return `https://cdn.simpleicons.org/${icon.slug}/${icon.color.replace('#', '')}`;
+}
+
+export function serviceAccent(service: ServiceIconInput): string {
+ return serviceIcon(service)?.color ?? serviceTypeColor(service.type ?? 'service');
+}
diff --git a/app/frontend/src/lib/demo-data.ts b/app/frontend/src/lib/demo-data.ts
index c2f6fd0..d688a50 100644
--- a/app/frontend/src/lib/demo-data.ts
+++ b/app/frontend/src/lib/demo-data.ts
@@ -10,9 +10,9 @@ export const demoProjects: ProjectEntity[] = [
createdAt: '2026-03-12T09:30:00Z',
updatedAt: '2026-03-31T09:10:00Z',
stats: {
- service_count: 4,
+ service_count: 5,
deployment_count: 27,
- running_services: 3,
+ running_services: 4,
last_deployment: '2026-03-31T08:42:00Z',
},
},
@@ -55,9 +55,11 @@ export const demoServicesByProject: Record = {
type: 'web',
status: 'running',
environment: 'production',
- image: 'ghcr.io/containr/web:2026.03.31',
+ image: 'ghcr.io/containr/web-react:v42',
command: 'npm run serve',
gitBranch: 'main',
+ port: 3000,
+ domain: 'web.containr.local',
createdAt: '2026-03-12T10:00:00Z',
updatedAt: '2026-03-31T08:42:00Z',
},
@@ -68,9 +70,11 @@ export const demoServicesByProject: Record = {
type: 'web',
status: 'running',
environment: 'production',
- image: 'ghcr.io/containr/api:2026.03.31',
+ image: 'ghcr.io/containr/api-go:v2.4.1',
command: './server',
gitBranch: 'main',
+ port: 8080,
+ domain: 'api.containr.local',
createdAt: '2026-03-12T10:02:00Z',
updatedAt: '2026-03-31T08:43:00Z',
},
@@ -81,9 +85,10 @@ export const demoServicesByProject: Record = {
type: 'worker',
status: 'building',
environment: 'production',
- image: 'ghcr.io/containr/worker:2026.03.31',
+ image: 'ghcr.io/containr/worker:edge',
command: './worker',
gitBranch: 'main',
+ port: 9100,
createdAt: '2026-03-12T10:05:00Z',
updatedAt: '2026-03-31T08:44:00Z',
},
@@ -94,12 +99,27 @@ export const demoServicesByProject: Record = {
type: 'database',
status: 'running',
environment: 'production',
- image: 'postgres:15-alpine',
+ image: 'postgres:16',
command: 'postgres',
gitBranch: 'main',
+ port: 5432,
createdAt: '2026-03-12T10:08:00Z',
updatedAt: '2026-03-31T08:40:00Z',
},
+ {
+ id: 'demo-svc-redis',
+ projectId: 'demo-project-core',
+ name: 'Redis Cache',
+ type: 'database',
+ status: 'running',
+ environment: 'production',
+ image: 'dragonfly:1.25',
+ command: 'redis-server',
+ gitBranch: 'main',
+ port: 6379,
+ createdAt: '2026-03-12T10:12:00Z',
+ updatedAt: '2026-03-31T08:40:00Z',
+ },
],
'demo-project-growth': [
{
@@ -109,9 +129,11 @@ export const demoServicesByProject: Record = {
type: 'web',
status: 'running',
environment: 'production',
- image: 'ghcr.io/containr/marketing:latest',
- command: 'npm run start',
+ image: 'nginx:1.27-alpine',
+ command: 'nginx -g daemon off;',
gitBranch: 'main',
+ port: 8080,
+ domain: 'growth.containr.local',
createdAt: '2026-03-03T09:00:00Z',
updatedAt: '2026-03-30T12:10:00Z',
},
@@ -122,9 +144,10 @@ export const demoServicesByProject: Record = {
type: 'web',
status: 'running',
environment: 'production',
- image: 'ghcr.io/containr/campaign-api:latest',
+ image: 'node:20-alpine',
command: './api',
gitBranch: 'main',
+ port: 3000,
createdAt: '2026-03-03T09:04:00Z',
updatedAt: '2026-03-30T12:08:00Z',
},
@@ -135,9 +158,10 @@ export const demoServicesByProject: Record = {
type: 'database',
status: 'running',
environment: 'production',
- image: 'redis:7-alpine',
+ image: 'dragonfly:1.25',
command: 'redis-server',
gitBranch: 'main',
+ port: 6379,
createdAt: '2026-03-03T09:07:00Z',
updatedAt: '2026-03-30T12:07:00Z',
},
@@ -150,9 +174,10 @@ export const demoServicesByProject: Record = {
type: 'web',
status: 'running',
environment: 'production',
- image: 'ghcr.io/containr/inference-api:latest',
+ image: 'python:3.12-slim',
command: './serve',
gitBranch: 'main',
+ port: 8000,
createdAt: '2026-02-18T08:00:00Z',
updatedAt: '2026-03-29T18:00:00Z',
},
@@ -163,7 +188,7 @@ export const demoServicesByProject: Record = {
type: 'worker',
status: 'failed',
environment: 'production',
- image: 'ghcr.io/containr/evaluator:latest',
+ image: 'python:3.12-slim',
command: './run-jobs',
gitBranch: 'main',
createdAt: '2026-02-18T08:03:00Z',
@@ -195,6 +220,7 @@ export const demoVariablesByProject: Record