From b76c02f8076a9b15eeb46d5dd2f3417ae5cbe55b Mon Sep 17 00:00:00 2001
From: Admin <1+admin@noreply.git.dcntrlized.org>
Date: Mon, 10 Aug 2026 16:28:02 +0100
Subject: [PATCH] feat(frontend): show database status chip on capability cards
Service cards that declare the database capability render a status chip
for the shared dcntrlized-db infra service, with a Pull action to
install and start the database when it is not installed yet.
- Merge logic hides db-role containers and hidden registry items from
the grid.
- Service model carries capabilities; a new DatabaseChip maps the live
database state onto the existing status metadata.
---
src/frontend/src/app.tsx | 28 ++++-
.../src/components/infra/database-chip.tsx | 106 ++++++++++++++++++
.../src/components/service/service-card.tsx | 7 +-
.../src/components/service/service-grid.tsx | 12 +-
src/frontend/src/lib/domain/merge.ts | 14 ++-
src/frontend/src/lib/types.ts | 13 +++
6 files changed, 172 insertions(+), 8 deletions(-)
create mode 100644 src/frontend/src/components/infra/database-chip.tsx
diff --git a/src/frontend/src/app.tsx b/src/frontend/src/app.tsx
index 032d4ce..475e3fe 100644
--- a/src/frontend/src/app.tsx
+++ b/src/frontend/src/app.tsx
@@ -20,13 +20,15 @@ import { useContainerStore } from '@lib/hooks/useContainerStore'
import { useMasterRegistry } from '@lib/hooks/useMasterRegistry'
import { mergeServices } from '@lib/domain/merge'
import { filterServices, serviceCounts } from '@lib/domain/filter'
-import type { ServiceFilter } from '@lib/types'
+import type { DatabaseStatus, ServiceFilter } from '@lib/types'
import { Header } from '@components/layout/header'
import { Toolbar } from '@components/layout/toolbar'
import { ServiceGrid } from '@components/service/service-grid'
import { Toaster } from '@components/ui/sonner'
import { EmptyState, LoadingState, RegistryErrorState } from '@components/layout/status-states'
+const DATABASE_NAME = 'dcntrlized-db'
+
export function App() {
const { containers, status } = useContainerStore()
const registry = useMasterRegistry()
@@ -44,6 +46,28 @@ export function App() {
[services, query, filter]
)
+ const db = useMemo(() => {
+ const item =
+ registry.state.kind === 'ok'
+ ? (registry.state.items.find((r) => r.name.toLowerCase() === DATABASE_NAME) ?? null)
+ : null
+
+ const container = containers.find(
+ (c) => (c.role ?? '').toLowerCase() === 'db' || c.name.toLowerCase() === DATABASE_NAME
+ )
+
+ let dbStatus: DatabaseStatus = 'uninstalled'
+ if (container) {
+ const s = container.status.toLowerCase()
+ if (s === 'running' || s === 'restarting') dbStatus = 'running'
+ else if (s === 'starting') dbStatus = 'starting'
+ else if (s === 'installed' || s === 'created') dbStatus = 'installed'
+ else dbStatus = 'stopped'
+ }
+
+ return { status: dbStatus, item }
+ }, [containers, registry.state])
+
const loading = registry.state.kind === 'loading' && containers.length === 0
const registryError = registry.state.kind === 'error'
@@ -89,7 +113,7 @@ export function App() {
{visible.length === 0 ? (
) : (
-
+
)}
>
)}
diff --git a/src/frontend/src/components/infra/database-chip.tsx b/src/frontend/src/components/infra/database-chip.tsx
new file mode 100644
index 0000000..103a3cb
--- /dev/null
+++ b/src/frontend/src/components/infra/database-chip.tsx
@@ -0,0 +1,106 @@
+/**
+ * Copyright (C) 2026 Dcntrlized
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Database, Download, LoaderCircle } from 'lucide-react'
+import { Button } from '@components/ui/button'
+import { statusMeta } from '@components/container/status'
+import { toast } from 'sonner'
+import { installContainer, startContainer } from '@lib/api/containers'
+import { useAsyncAction } from '@lib/hooks/useAsyncAction'
+import { cn } from '@lib/utils'
+import type { DatabaseStatus, RegistryItem, Service } from '@lib/types'
+import type { DatabaseState } from '@components/service/service-grid'
+
+function errorMessage(error: unknown): string {
+ return error instanceof Error ? error.message : 'Unknown error'
+}
+
+function dbService(item: RegistryItem): Service {
+ return {
+ id: `registry-${item.name}`,
+ name: item.name,
+ status: 'uninstalled',
+ origin: 'remote',
+ title: item.title || item.name,
+ image: item.image,
+ registryImage: item.image,
+ source: item.source,
+ registryUrl: item.url,
+ latestVersion: item.version,
+ hasUpdate: false,
+ capabilities: [],
+ }
+}
+
+function chipFor(status: DatabaseStatus): string {
+ const meta = statusMeta(status)
+ return cn('inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium', meta.chip)
+}
+
+/**
+ * Status chip for the shared database infrastructure service. Rendered on
+ * service cards that declare the `database` capability; lets the user pull
+ * the database image when it has not been installed yet.
+ */
+export function DatabaseChip({ db }: { db: DatabaseState }) {
+ const pull = useAsyncAction(async () => {
+ if (!db.item) return
+ try {
+ const service = dbService(db.item)
+ await installContainer(service)
+ try {
+ await startContainer(service)
+ toast.success('Shared database installed and started')
+ } catch {
+ toast.success('Shared database installed', { description: 'Start it from the database card when ready' })
+ }
+ } catch (error) {
+ toast.error('Failed to install shared database', { description: errorMessage(error) })
+ }
+ })
+
+ const meta = statusMeta(db.status)
+
+ return (
+