From 53d60fdddd0f27d8fe6cfeb22499c0fc90b85c21 Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Sun, 28 Dec 2025 21:40:06 +0100 Subject: [PATCH 01/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d77500..f50dfdc 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Dockhand is a modern, efficient Docker management application providing real-tim - **Frontend**: SvelteKit 2, Svelte 5, shadcn-svelte, TailwindCSS - **Backend**: Bun runtime with SvelteKit API routes - **Database**: SQLite or PostgreSQL via Drizzle ORM -- **Docker**: Dockerode library +- **Docker**: direct docker API calls. ## License From 497fbdb635831d4526b0224c38a88d8e803771fc Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 06:47:22 +0100 Subject: [PATCH 02/30] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f50dfdc..625db3a 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ See [LICENSE.txt](LICENSE.txt) for full terms. ## Links -- **Website**: [https://dockhand.io](https://dockhand.io) -- **Documentation**: [https://dockhand.io/docs](https://dockhand.io/docs) +- **Website**: [https://dockhand.pro](https://dockhand.pro) +- **Documentation**: [https://dockhand.pro/manual](https://dockhand.pro/manual) --- From e536388a7a638b1471477ebb8bf0e7a219c3b20e Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 06:47:46 +0100 Subject: [PATCH 03/30] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 625db3a..ff564a3 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@

- Website • - Documentation • + Website • + DocumentationLicense

From ab8743bdae48d2d3e9808541b051c77e7e908bdb Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 29 Dec 2025 08:40:11 +0100 Subject: [PATCH 04/30] proper src structure, dockerfile, entrypoint --- Dockerfile | 86 ++ bunfig.toml | 9 + components.json | 16 + docker-entrypoint.sh | 122 +++ drizzle.config.ts | 16 + package.json | 110 ++ app.css => src/app.css | 0 app.d.ts => src/app.d.ts | 0 app.html => src/app.html | 0 hooks.server.ts => src/hooks.server.ts | 0 {images => src/images}/logo.webp | Bin {lib => src/lib}/actions/column-resize.ts | 0 {lib => src/lib}/assets/favicon.svg | 0 .../lib}/components/AvatarCropper.svelte | 0 .../components/BatchOperationModal.svelte | 0 {lib => src/lib}/components/CodeEditor.svelte | 0 .../components/ColumnSettingsPopover.svelte | 0 .../lib}/components/CommandPalette.svelte | 0 .../lib}/components/ConfirmPopover.svelte | 0 .../lib}/components/ExecutionLogViewer.svelte | 0 .../lib}/components/MultiSelectFilter.svelte | 0 {lib => src/lib}/components/PageHeader.svelte | 0 .../PasswordStrengthIndicator.svelte | 0 {lib => src/lib}/components/PullTab.svelte | 0 {lib => src/lib}/components/PushTab.svelte | 0 {lib => src/lib}/components/ScanTab.svelte | 0 .../components/ScannerSeverityPills.svelte | 0 {lib => src/lib}/components/Sidebar.svelte | 0 .../lib}/components/StackEnvVarsEditor.svelte | 0 .../lib}/components/StackEnvVarsPanel.svelte | 0 .../lib}/components/ThemeSelector.svelte | 0 .../lib}/components/TimezoneSelector.svelte | 0 .../lib}/components/UpdateContainerRow.svelte | 0 .../components/UpdateStepIndicator.svelte | 0 .../lib}/components/UpdateSummaryStats.svelte | 0 .../VulnerabilityCriteriaBadge.svelte | 0 .../VulnerabilityCriteriaSelector.svelte | 0 .../lib}/components/WhatsNewModal.svelte | 0 .../lib}/components/app-sidebar.svelte | 0 .../lib}/components/cron-editor.svelte | 0 .../lib}/components/data-grid/DataGrid.svelte | 0 .../lib}/components/data-grid/context.ts | 0 .../lib}/components/data-grid/index.ts | 0 .../lib}/components/data-grid/types.ts | 0 {lib => src/lib}/components/host-info.svelte | 0 .../lib}/components/icon-picker.svelte | 0 .../lib}/components/main-content.svelte | 0 .../lib}/components/permission-guard.svelte | 0 .../lib}/components/theme-toggle.svelte | 0 .../ui/accordion/accordion-content.svelte | 0 .../ui/accordion/accordion-item.svelte | 0 .../ui/accordion/accordion-trigger.svelte | 0 .../components/ui/accordion/accordion.svelte | 0 .../lib}/components/ui/accordion/index.ts | 0 .../ui/alert/alert-description.svelte | 0 .../components/ui/alert/alert-title.svelte | 0 .../lib}/components/ui/alert/alert.svelte | 0 {lib => src/lib}/components/ui/alert/index.ts | 0 .../ui/avatar/avatar-fallback.svelte | 0 .../components/ui/avatar/avatar-image.svelte | 0 .../lib}/components/ui/avatar/avatar.svelte | 0 .../lib}/components/ui/avatar/index.ts | 0 .../lib}/components/ui/badge/badge.svelte | 0 {lib => src/lib}/components/ui/badge/index.ts | 0 .../lib}/components/ui/button/button.svelte | 0 .../lib}/components/ui/button/index.ts | 0 .../ui/calendar/calendar-caption.svelte | 0 .../ui/calendar/calendar-cell.svelte | 0 .../ui/calendar/calendar-day.svelte | 0 .../ui/calendar/calendar-grid-body.svelte | 0 .../ui/calendar/calendar-grid-head.svelte | 0 .../ui/calendar/calendar-grid-row.svelte | 0 .../ui/calendar/calendar-grid.svelte | 0 .../ui/calendar/calendar-head-cell.svelte | 0 .../ui/calendar/calendar-header.svelte | 0 .../ui/calendar/calendar-heading.svelte | 0 .../ui/calendar/calendar-month-select.svelte | 0 .../ui/calendar/calendar-month.svelte | 0 .../ui/calendar/calendar-months.svelte | 0 .../ui/calendar/calendar-nav.svelte | 0 .../ui/calendar/calendar-next-button.svelte | 0 .../ui/calendar/calendar-prev-button.svelte | 0 .../ui/calendar/calendar-year-select.svelte | 0 .../components/ui/calendar/calendar.svelte | 0 .../lib}/components/ui/calendar/index.ts | 0 .../components/ui/card/card-action.svelte | 0 .../components/ui/card/card-content.svelte | 0 .../ui/card/card-description.svelte | 0 .../components/ui/card/card-footer.svelte | 0 .../components/ui/card/card-header.svelte | 0 .../lib}/components/ui/card/card-title.svelte | 0 .../lib}/components/ui/card/card.svelte | 0 {lib => src/lib}/components/ui/card/index.ts | 0 .../components/ui/checkbox/checkbox.svelte | 0 .../lib}/components/ui/checkbox/index.ts | 0 .../ui/command/command-dialog.svelte | 0 .../ui/command/command-empty.svelte | 0 .../ui/command/command-group.svelte | 0 .../ui/command/command-input.svelte | 0 .../components/ui/command/command-item.svelte | 0 .../ui/command/command-link-item.svelte | 0 .../components/ui/command/command-list.svelte | 0 .../ui/command/command-loading.svelte | 0 .../ui/command/command-separator.svelte | 0 .../ui/command/command-shortcut.svelte | 0 .../lib}/components/ui/command/command.svelte | 0 .../lib}/components/ui/command/index.ts | 0 .../ui/date-picker/date-picker.svelte | 0 .../lib}/components/ui/date-picker/index.ts | 0 .../components/ui/dialog/dialog-close.svelte | 0 .../ui/dialog/dialog-content.svelte | 0 .../ui/dialog/dialog-description.svelte | 0 .../components/ui/dialog/dialog-footer.svelte | 0 .../components/ui/dialog/dialog-header.svelte | 0 .../ui/dialog/dialog-overlay.svelte | 0 .../components/ui/dialog/dialog-portal.svelte | 0 .../components/ui/dialog/dialog-title.svelte | 0 .../ui/dialog/dialog-trigger.svelte | 0 .../lib}/components/ui/dialog/dialog.svelte | 0 .../lib}/components/ui/dialog/index.ts | 0 .../dropdown-menu-checkbox-group.svelte | 0 .../dropdown-menu-checkbox-item.svelte | 0 .../dropdown-menu-content.svelte | 0 .../dropdown-menu-group-heading.svelte | 0 .../dropdown-menu/dropdown-menu-group.svelte | 0 .../dropdown-menu/dropdown-menu-item.svelte | 0 .../dropdown-menu/dropdown-menu-label.svelte | 0 .../dropdown-menu/dropdown-menu-portal.svelte | 0 .../dropdown-menu-radio-group.svelte | 0 .../dropdown-menu-radio-item.svelte | 0 .../dropdown-menu-separator.svelte | 0 .../dropdown-menu-shortcut.svelte | 0 .../dropdown-menu-sub-content.svelte | 0 .../dropdown-menu-sub-trigger.svelte | 0 .../ui/dropdown-menu/dropdown-menu-sub.svelte | 0 .../dropdown-menu-trigger.svelte | 0 .../ui/dropdown-menu/dropdown-menu.svelte | 0 .../lib}/components/ui/dropdown-menu/index.ts | 0 .../ui/empty-state/empty-state.svelte | 0 .../lib}/components/ui/empty-state/index.ts | 0 .../ui/empty-state/no-environment.svelte | 0 {lib => src/lib}/components/ui/input/index.ts | 0 .../lib}/components/ui/input/input.svelte | 0 {lib => src/lib}/components/ui/label/index.ts | 0 .../lib}/components/ui/label/label.svelte | 0 .../lib}/components/ui/popover/index.ts | 0 .../ui/popover/popover-content.svelte | 0 .../ui/popover/popover-trigger.svelte | 0 .../lib}/components/ui/progress/index.ts | 0 .../components/ui/progress/progress.svelte | 0 .../lib}/components/ui/select/index.ts | 0 .../ui/select/select-content.svelte | 0 .../ui/select/select-group-heading.svelte | 0 .../components/ui/select/select-group.svelte | 0 .../components/ui/select/select-item.svelte | 0 .../components/ui/select/select-label.svelte | 0 .../select/select-scroll-down-button.svelte | 0 .../ui/select/select-scroll-up-button.svelte | 0 .../ui/select/select-separator.svelte | 0 .../ui/select/select-trigger.svelte | 0 .../lib}/components/ui/separator/index.ts | 0 .../components/ui/separator/separator.svelte | 0 {lib => src/lib}/components/ui/sheet/index.ts | 0 .../components/ui/sheet/sheet-close.svelte | 0 .../components/ui/sheet/sheet-content.svelte | 0 .../ui/sheet/sheet-description.svelte | 0 .../components/ui/sheet/sheet-footer.svelte | 0 .../components/ui/sheet/sheet-header.svelte | 0 .../components/ui/sheet/sheet-overlay.svelte | 0 .../components/ui/sheet/sheet-title.svelte | 0 .../components/ui/sheet/sheet-trigger.svelte | 0 .../lib}/components/ui/sidebar/constants.ts | 0 .../components/ui/sidebar/context.svelte.ts | 0 .../lib}/components/ui/sidebar/index.ts | 0 .../ui/sidebar/sidebar-content.svelte | 0 .../ui/sidebar/sidebar-footer.svelte | 0 .../ui/sidebar/sidebar-group-action.svelte | 0 .../ui/sidebar/sidebar-group-content.svelte | 0 .../ui/sidebar/sidebar-group-label.svelte | 0 .../ui/sidebar/sidebar-group.svelte | 0 .../ui/sidebar/sidebar-header.svelte | 0 .../ui/sidebar/sidebar-input.svelte | 0 .../ui/sidebar/sidebar-inset.svelte | 0 .../ui/sidebar/sidebar-menu-action.svelte | 0 .../ui/sidebar/sidebar-menu-badge.svelte | 0 .../ui/sidebar/sidebar-menu-button.svelte | 0 .../ui/sidebar/sidebar-menu-item.svelte | 0 .../ui/sidebar/sidebar-menu-skeleton.svelte | 0 .../ui/sidebar/sidebar-menu-sub-button.svelte | 0 .../ui/sidebar/sidebar-menu-sub-item.svelte | 0 .../ui/sidebar/sidebar-menu-sub.svelte | 0 .../components/ui/sidebar/sidebar-menu.svelte | 0 .../ui/sidebar/sidebar-provider.svelte | 0 .../components/ui/sidebar/sidebar-rail.svelte | 0 .../ui/sidebar/sidebar-separator.svelte | 0 .../ui/sidebar/sidebar-trigger.svelte | 0 .../lib}/components/ui/sidebar/sidebar.svelte | 0 .../lib}/components/ui/skeleton/index.ts | 0 .../components/ui/skeleton/skeleton.svelte | 0 .../lib}/components/ui/sonner/index.ts | 0 .../lib}/components/ui/sonner/sonner.svelte | 0 .../lib}/components/ui/switch/index.ts | 0 .../lib}/components/ui/switch/switch.svelte | 0 {lib => src/lib}/components/ui/table/index.ts | 0 .../components/ui/table/table-body.svelte | 0 .../components/ui/table/table-caption.svelte | 0 .../components/ui/table/table-cell.svelte | 0 .../components/ui/table/table-footer.svelte | 0 .../components/ui/table/table-head.svelte | 0 .../components/ui/table/table-header.svelte | 0 .../lib}/components/ui/table/table-row.svelte | 0 .../lib}/components/ui/table/table.svelte | 0 {lib => src/lib}/components/ui/tabs/index.ts | 0 .../components/ui/tabs/tabs-content.svelte | 0 .../lib}/components/ui/tabs/tabs-list.svelte | 0 .../components/ui/tabs/tabs-trigger.svelte | 0 .../lib}/components/ui/tabs/tabs.svelte | 0 .../lib}/components/ui/textarea/index.ts | 0 .../components/ui/textarea/textarea.svelte | 0 .../lib}/components/ui/toggle-pill/index.ts | 0 .../ui/toggle-pill/toggle-group.svelte | 0 .../ui/toggle-pill/toggle-pill.svelte | 0 .../ui/toggle-pill/toggle-switch.svelte | 0 .../lib}/components/ui/tooltip/index.ts | 0 .../ui/tooltip/tooltip-content.svelte | 0 .../ui/tooltip/tooltip-trigger.svelte | 0 {lib => src/lib}/config/grid-columns.ts | 0 {lib => src/lib}/data/changelog.json | 0 {lib => src/lib}/data/dependencies.json | 0 {lib => src/lib}/hooks/is-mobile.svelte.ts | 0 {lib => src/lib}/index.ts | 0 {lib => src/lib}/server/audit-events.ts | 0 {lib => src/lib}/server/audit.ts | 0 {lib => src/lib}/server/auth.ts | 0 {lib => src/lib}/server/authorize.ts | 0 {lib => src/lib}/server/db.ts | 0 {lib => src/lib}/server/db/connection.ts | 0 {lib => src/lib}/server/db/drizzle.ts | 0 {lib => src/lib}/server/db/schema/index.ts | 0 .../lib}/server/db/schema/pg-schema.ts | 0 {lib => src/lib}/server/docker.ts | 0 {lib => src/lib}/server/event-collector.ts | 0 {lib => src/lib}/server/git.ts | 0 {lib => src/lib}/server/hawser.ts | 0 {lib => src/lib}/server/license.ts | 0 {lib => src/lib}/server/metrics-collector.ts | 0 {lib => src/lib}/server/notifications.ts | 0 {lib => src/lib}/server/scanner.ts | 0 {lib => src/lib}/server/scheduler/index.ts | 0 .../scheduler/tasks/container-update.ts | 0 .../scheduler/tasks/env-update-check.ts | 0 .../server/scheduler/tasks/git-stack-sync.ts | 0 .../server/scheduler/tasks/system-cleanup.ts | 0 .../server/scheduler/tasks/update-utils.ts | 0 {lib => src/lib}/server/stacks.ts | 0 {lib => src/lib}/server/subprocess-manager.ts | 0 .../server/subprocesses/event-subprocess.ts | 0 .../server/subprocesses/metrics-subprocess.ts | 0 {lib => src/lib}/server/uptime.ts | 0 {lib => src/lib}/stores/audit-events.ts | 0 {lib => src/lib}/stores/auth.ts | 0 {lib => src/lib}/stores/dashboard.ts | 0 {lib => src/lib}/stores/environment.ts | 0 {lib => src/lib}/stores/events.ts | 0 {lib => src/lib}/stores/grid-preferences.ts | 0 {lib => src/lib}/stores/license.ts | 0 {lib => src/lib}/stores/settings.ts | 0 {lib => src/lib}/stores/stats.ts | 0 {lib => src/lib}/stores/theme.ts | 0 {lib => src/lib}/themes.ts | 0 {lib => src/lib}/types.ts | 0 {lib => src/lib}/utils.ts | 0 {lib => src/lib}/utils/icons.ts | 0 {lib => src/lib}/utils/ip.ts | 0 {lib => src/lib}/utils/label-colors.ts | 0 {lib => src/lib}/utils/update-steps.ts | 0 {lib => src/lib}/utils/version.ts | 0 {routes => src/routes}/+layout.server.ts | 0 {routes => src/routes}/+layout.svelte | 0 {routes => src/routes}/+layout.ts | 0 {routes => src/routes}/+page.svelte | 0 {routes => src/routes}/activity/+page.svelte | 0 {routes => src/routes}/alerts/+page.svelte | 0 .../routes}/api/activity/+server.ts | 0 .../api/activity/containers/+server.ts | 0 .../routes}/api/activity/events/+server.ts | 0 .../routes}/api/activity/stats/+server.ts | 0 {routes => src/routes}/api/audit/+server.ts | 0 .../routes}/api/audit/events/+server.ts | 0 .../routes}/api/audit/export/+server.ts | 0 .../routes}/api/audit/users/+server.ts | 0 .../routes}/api/auth/ldap/+server.ts | 0 .../routes}/api/auth/ldap/[id]/+server.ts | 0 .../api/auth/ldap/[id]/test/+server.ts | 0 .../routes}/api/auth/login/+server.ts | 0 .../routes}/api/auth/logout/+server.ts | 0 .../routes}/api/auth/oidc/+server.ts | 0 .../routes}/api/auth/oidc/[id]/+server.ts | 0 .../api/auth/oidc/[id]/initiate/+server.ts | 0 .../api/auth/oidc/[id]/test/+server.ts | 0 .../routes}/api/auth/oidc/callback/+server.ts | 0 .../routes}/api/auth/providers/+server.ts | 0 .../routes}/api/auth/session/+server.ts | 0 .../routes}/api/auth/settings/+server.ts | 0 .../routes}/api/auto-update/+server.ts | 0 .../auto-update/[containerName]/+server.ts | 0 {routes => src/routes}/api/batch/+server.ts | 0 .../routes}/api/changelog/+server.ts | 0 .../routes}/api/config-sets/+server.ts | 0 .../routes}/api/config-sets/[id]/+server.ts | 0 .../routes}/api/containers/+server.ts | 0 .../routes}/api/containers/[id]/+server.ts | 0 .../api/containers/[id]/exec/+server.ts | 0 .../api/containers/[id]/files/+server.ts | 0 .../containers/[id]/files/chmod/+server.ts | 0 .../containers/[id]/files/content/+server.ts | 0 .../containers/[id]/files/create/+server.ts | 0 .../containers/[id]/files/delete/+server.ts | 0 .../containers/[id]/files/download/+server.ts | 0 .../containers/[id]/files/rename/+server.ts | 0 .../containers/[id]/files/upload/+server.ts | 0 .../api/containers/[id]/inspect/+server.ts | 0 .../api/containers/[id]/logs/+server.ts | 0 .../containers/[id]/logs/stream/+server.ts | 0 .../api/containers/[id]/pause/+server.ts | 0 .../api/containers/[id]/rename/+server.ts | 0 .../api/containers/[id]/restart/+server.ts | 0 .../api/containers/[id]/start/+server.ts | 0 .../api/containers/[id]/stats/+server.ts | 0 .../api/containers/[id]/stop/+server.ts | 0 .../api/containers/[id]/top/+server.ts | 0 .../api/containers/[id]/unpause/+server.ts | 0 .../api/containers/[id]/update/+server.ts | 0 .../containers/batch-update-stream/+server.ts | 0 .../api/containers/batch-update/+server.ts | 0 .../api/containers/check-updates/+server.ts | 0 .../api/containers/pending-updates/+server.ts | 0 .../routes}/api/containers/sizes/+server.ts | 0 .../routes}/api/containers/stats/+server.ts | 0 .../api/dashboard/preferences/+server.ts | 0 .../routes}/api/dashboard/stats/+server.ts | 0 .../api/dashboard/stats/stream/+server.ts | 0 .../routes}/api/dependencies/+server.ts | 0 .../routes}/api/environments/+server.ts | 0 .../routes}/api/environments/[id]/+server.ts | 0 .../[id]/notifications/+server.ts | 0 .../notifications/[notificationId]/+server.ts | 0 .../api/environments/[id]/test/+server.ts | 0 .../api/environments/[id]/timezone/+server.ts | 0 .../environments/[id]/update-check/+server.ts | 0 .../api/environments/detect-socket/+server.ts | 0 .../routes}/api/environments/test/+server.ts | 0 {routes => src/routes}/api/events/+server.ts | 0 .../routes}/api/git/credentials/+server.ts | 0 .../api/git/credentials/[id]/+server.ts | 0 .../routes}/api/git/repositories/+server.ts | 0 .../api/git/repositories/[id]/+server.ts | 0 .../git/repositories/[id]/deploy/+server.ts | 0 .../api/git/repositories/[id]/sync/+server.ts | 0 .../api/git/repositories/[id]/test/+server.ts | 0 .../api/git/repositories/test/+server.ts | 0 .../routes}/api/git/stacks/+server.ts | 0 .../routes}/api/git/stacks/[id]/+server.ts | 0 .../git/stacks/[id]/deploy-stream/+server.ts | 0 .../api/git/stacks/[id]/deploy/+server.ts | 0 .../api/git/stacks/[id]/env-files/+server.ts | 0 .../api/git/stacks/[id]/sync/+server.ts | 0 .../api/git/stacks/[id]/test/+server.ts | 0 .../api/git/stacks/[id]/webhook/+server.ts | 0 .../routes}/api/git/webhook/[id]/+server.ts | 0 .../routes}/api/hawser/connect/+server.ts | 0 .../routes}/api/hawser/tokens/+server.ts | 0 {routes => src/routes}/api/health/+server.ts | 0 .../routes}/api/health/database/+server.ts | 0 {routes => src/routes}/api/host/+server.ts | 0 {routes => src/routes}/api/images/+server.ts | 0 .../routes}/api/images/[id]/+server.ts | 0 .../routes}/api/images/[id]/export/+server.ts | 0 .../api/images/[id]/history/+server.ts | 0 .../routes}/api/images/[id]/tag/+server.ts | 0 .../routes}/api/images/pull/+server.ts | 0 .../routes}/api/images/push/+server.ts | 0 .../routes}/api/images/scan/+server.ts | 0 .../routes}/api/legal/license/+server.ts | 0 .../routes}/api/legal/privacy/+server.ts | 0 {routes => src/routes}/api/license/+server.ts | 0 .../routes}/api/logs/merged/+server.ts | 0 {routes => src/routes}/api/metrics/+server.ts | 0 .../routes}/api/networks/+server.ts | 0 .../routes}/api/networks/[id]/+server.ts | 0 .../api/networks/[id]/connect/+server.ts | 0 .../api/networks/[id]/disconnect/+server.ts | 0 .../api/networks/[id]/inspect/+server.ts | 0 .../routes}/api/notifications/+server.ts | 0 .../routes}/api/notifications/[id]/+server.ts | 0 .../api/notifications/[id]/test/+server.ts | 0 .../routes}/api/notifications/test/+server.ts | 0 .../api/notifications/trigger-test/+server.ts | 0 .../preferences/favorite-groups/+server.ts | 0 .../api/preferences/favorites/+server.ts | 0 .../routes}/api/preferences/grid/+server.ts | 0 {routes => src/routes}/api/profile/+server.ts | 0 .../routes}/api/profile/avatar/+server.ts | 0 .../api/profile/preferences/+server.ts | 0 .../routes}/api/prune/all/+server.ts | 0 .../routes}/api/prune/containers/+server.ts | 0 .../routes}/api/prune/images/+server.ts | 0 .../routes}/api/prune/networks/+server.ts | 0 .../routes}/api/prune/volumes/+server.ts | 0 .../routes}/api/registries/+server.ts | 0 .../routes}/api/registries/[id]/+server.ts | 0 .../api/registries/[id]/default/+server.ts | 0 .../routes}/api/registry/catalog/+server.ts | 0 .../routes}/api/registry/image/+server.ts | 0 .../routes}/api/registry/search/+server.ts | 0 .../routes}/api/registry/tags/+server.ts | 0 {routes => src/routes}/api/roles/+server.ts | 0 .../routes}/api/roles/[id]/+server.ts | 0 .../routes}/api/schedules/+server.ts | 0 .../api/schedules/[type]/[id]/+server.ts | 0 .../api/schedules/[type]/[id]/run/+server.ts | 0 .../schedules/[type]/[id]/toggle/+server.ts | 0 .../api/schedules/executions/+server.ts | 0 .../api/schedules/executions/[id]/+server.ts | 0 .../routes}/api/schedules/settings/+server.ts | 0 .../routes}/api/schedules/stream/+server.ts | 0 .../schedules/system/[id]/toggle/+server.ts | 0 .../routes}/api/settings/general/+server.ts | 0 .../routes}/api/settings/scanner/+server.ts | 0 {routes => src/routes}/api/stacks/+server.ts | 0 .../routes}/api/stacks/[name]/+server.ts | 0 .../api/stacks/[name]/compose/+server.ts | 0 .../routes}/api/stacks/[name]/down/+server.ts | 0 .../routes}/api/stacks/[name]/env/+server.ts | 0 .../api/stacks/[name]/env/validate/+server.ts | 0 .../api/stacks/[name]/restart/+server.ts | 0 .../api/stacks/[name]/start/+server.ts | 0 .../routes}/api/stacks/[name]/stop/+server.ts | 0 .../routes}/api/stacks/sources/+server.ts | 0 {routes => src/routes}/api/system/+server.ts | 0 .../routes}/api/system/disk/+server.ts | 0 {routes => src/routes}/api/users/+server.ts | 0 .../routes}/api/users/[id]/+server.ts | 0 .../routes}/api/users/[id]/mfa/+server.ts | 0 .../routes}/api/users/[id]/roles/+server.ts | 0 {routes => src/routes}/api/volumes/+server.ts | 0 .../routes}/api/volumes/[name]/+server.ts | 0 .../api/volumes/[name]/browse/+server.ts | 0 .../volumes/[name]/browse/content/+server.ts | 0 .../volumes/[name]/browse/release/+server.ts | 0 .../api/volumes/[name]/clone/+server.ts | 0 .../api/volumes/[name]/export/+server.ts | 0 .../api/volumes/[name]/inspect/+server.ts | 0 {routes => src/routes}/audit/+page.svelte | 0 {routes => src/routes}/audit/+server.ts | 0 {routes => src/routes}/audit/users/+server.ts | 0 .../routes}/containers/+page.svelte | 0 .../containers/AutoUpdateSettings.svelte | 0 .../containers/BatchUpdateModal.svelte | 0 .../containers/ContainerInspectModal.svelte | 0 .../containers/ContainerTerminal.svelte | 0 .../routes}/containers/ContainerTile.svelte | 0 .../containers/CreateContainerModal.svelte | 0 .../containers/EditContainerModal.svelte | 0 .../containers/FileBrowserModal.svelte | 0 .../containers/FileBrowserPanel.svelte | 0 .../routes}/dashboard/DraggableGrid.svelte | 0 .../routes}/dashboard/EnvironmentTile.svelte | 0 .../dashboard/EnvironmentTileSkeleton.svelte | 0 .../dashboard-container-stats.svelte | 0 .../dashboard-cpu-memory-bars.svelte | 0 .../dashboard-cpu-memory-charts.svelte | 0 .../dashboard/dashboard-disk-usage.svelte | 0 .../dashboard/dashboard-events-summary.svelte | 0 .../routes}/dashboard/dashboard-header.svelte | 0 .../dashboard/dashboard-health-banner.svelte | 0 .../routes}/dashboard/dashboard-labels.svelte | 0 .../dashboard/dashboard-offline-state.svelte | 0 .../dashboard/dashboard-recent-events.svelte | 0 .../dashboard/dashboard-resource-stats.svelte | 0 .../dashboard/dashboard-status-icons.svelte | 0 .../dashboard/dashboard-top-containers.svelte | 0 {routes => src/routes}/dashboard/index.ts | 0 .../routes}/environments/+page.svelte | 0 {routes => src/routes}/images/+page.server.ts | 0 {routes => src/routes}/images/+page.svelte | 0 .../routes}/images/ImageHistoryModal.svelte | 0 .../routes}/images/ImageLayersView.svelte | 0 .../images/ImagePullProgressPopover.svelte | 0 .../routes}/images/ImageScanModal.svelte | 0 .../routes}/images/PushToRegistryModal.svelte | 0 .../routes}/images/ScanResultsView.svelte | 0 .../images/VulnerabilityScanModal.svelte | 0 {routes => src/routes}/login/+page.svelte | 0 {routes => src/routes}/logs/+page.svelte | 0 {routes => src/routes}/logs/LogViewer.svelte | 0 {routes => src/routes}/logs/LogsPanel.svelte | 0 {routes => src/routes}/networks/+page.svelte | 0 .../networks/ConnectContainerModal.svelte | 0 .../networks/CreateNetworkModal.svelte | 0 .../networks/NetworkInspectModal.svelte | 0 {routes => src/routes}/profile/+page.svelte | 0 .../profile/ChangePasswordModal.svelte | 0 .../routes}/profile/DisableMfaModal.svelte | 0 .../routes}/profile/MfaSetupModal.svelte | 0 {routes => src/routes}/registry/+page.svelte | 0 .../registry/CopyToRegistryModal.svelte | 0 .../routes}/registry/ImagePullModal.svelte | 0 {routes => src/routes}/schedules/+page.svelte | 0 {routes => src/routes}/settings/+page.svelte | 0 .../routes}/settings/about/AboutTab.svelte | 0 .../settings/about/LicenseModal.svelte | 0 .../settings/about/PrivacyModal.svelte | 0 .../routes}/settings/auth/AuthTab.svelte | 0 .../settings/auth/ldap/LdapModal.svelte | 0 .../settings/auth/ldap/LdapSubTab.svelte | 0 .../settings/auth/oidc/OidcModal.svelte | 0 .../settings/auth/oidc/SsoSubTab.svelte | 0 .../settings/auth/roles/RoleModal.svelte | 0 .../settings/auth/roles/RolesSubTab.svelte | 0 .../settings/auth/users/UserModal.svelte | 0 .../settings/auth/users/UsersSubTab.svelte | 0 .../config-sets/ConfigSetModal.svelte | 0 .../settings/config-sets/ConfigSetsTab.svelte | 0 .../environments/EnvironmentModal.svelte | 0 .../environments/EnvironmentsTab.svelte | 0 .../environments/EventTypesEditor.svelte | 0 .../settings/general/GeneralTab.svelte | 0 .../settings/git/GitCredentialModal.svelte | 0 .../settings/git/GitCredentialsTab.svelte | 0 .../settings/git/GitRepositoriesTab.svelte | 0 .../settings/git/GitRepositoryModal.svelte | 0 .../routes}/settings/git/GitTab.svelte | 0 .../settings/license/LicenseTab.svelte | 0 .../notifications/NotificationModal.svelte | 0 .../notifications/NotificationsTab.svelte | 0 .../settings/registries/RegistriesTab.svelte | 0 .../settings/registries/RegistryModal.svelte | 0 {routes => src/routes}/stacks/+page.svelte | 0 .../routes}/stacks/ComposeGraphViewer.svelte | 0 .../stacks/GitDeployProgressPopover.svelte | 0 .../routes}/stacks/GitStackModal.svelte | 0 .../routes}/stacks/StackModal.svelte | 0 {routes => src/routes}/terminal/+page.svelte | 0 .../routes}/terminal/Terminal.svelte | 0 .../routes}/terminal/TerminalEmulator.svelte | 0 .../routes}/terminal/TerminalPanel.svelte | 0 .../routes}/terminal/[id]/+page.svelte | 0 {routes => src/routes}/volumes/+page.svelte | 0 .../routes}/volumes/CloneVolumeModal.svelte | 0 .../routes}/volumes/CreateVolumeModal.svelte | 0 .../routes}/volumes/VolumeBrowserModal.svelte | 0 .../routes}/volumes/VolumeInspectModal.svelte | 0 svelte.config.js | 15 + tsconfig.json | 20 + vite.config.ts | 996 ++++++++++++++++++ 556 files changed, 1390 insertions(+) create mode 100644 Dockerfile create mode 100644 bunfig.toml create mode 100644 components.json create mode 100644 docker-entrypoint.sh create mode 100644 drizzle.config.ts create mode 100644 package.json rename app.css => src/app.css (100%) rename app.d.ts => src/app.d.ts (100%) rename app.html => src/app.html (100%) rename hooks.server.ts => src/hooks.server.ts (100%) rename {images => src/images}/logo.webp (100%) rename {lib => src/lib}/actions/column-resize.ts (100%) rename {lib => src/lib}/assets/favicon.svg (100%) rename {lib => src/lib}/components/AvatarCropper.svelte (100%) rename {lib => src/lib}/components/BatchOperationModal.svelte (100%) rename {lib => src/lib}/components/CodeEditor.svelte (100%) rename {lib => src/lib}/components/ColumnSettingsPopover.svelte (100%) rename {lib => src/lib}/components/CommandPalette.svelte (100%) rename {lib => src/lib}/components/ConfirmPopover.svelte (100%) rename {lib => src/lib}/components/ExecutionLogViewer.svelte (100%) rename {lib => src/lib}/components/MultiSelectFilter.svelte (100%) rename {lib => src/lib}/components/PageHeader.svelte (100%) rename {lib => src/lib}/components/PasswordStrengthIndicator.svelte (100%) rename {lib => src/lib}/components/PullTab.svelte (100%) rename {lib => src/lib}/components/PushTab.svelte (100%) rename {lib => src/lib}/components/ScanTab.svelte (100%) rename {lib => src/lib}/components/ScannerSeverityPills.svelte (100%) rename {lib => src/lib}/components/Sidebar.svelte (100%) rename {lib => src/lib}/components/StackEnvVarsEditor.svelte (100%) rename {lib => src/lib}/components/StackEnvVarsPanel.svelte (100%) rename {lib => src/lib}/components/ThemeSelector.svelte (100%) rename {lib => src/lib}/components/TimezoneSelector.svelte (100%) rename {lib => src/lib}/components/UpdateContainerRow.svelte (100%) rename {lib => src/lib}/components/UpdateStepIndicator.svelte (100%) rename {lib => src/lib}/components/UpdateSummaryStats.svelte (100%) rename {lib => src/lib}/components/VulnerabilityCriteriaBadge.svelte (100%) rename {lib => src/lib}/components/VulnerabilityCriteriaSelector.svelte (100%) rename {lib => src/lib}/components/WhatsNewModal.svelte (100%) rename {lib => src/lib}/components/app-sidebar.svelte (100%) rename {lib => src/lib}/components/cron-editor.svelte (100%) rename {lib => src/lib}/components/data-grid/DataGrid.svelte (100%) rename {lib => src/lib}/components/data-grid/context.ts (100%) rename {lib => src/lib}/components/data-grid/index.ts (100%) rename {lib => src/lib}/components/data-grid/types.ts (100%) rename {lib => src/lib}/components/host-info.svelte (100%) rename {lib => src/lib}/components/icon-picker.svelte (100%) rename {lib => src/lib}/components/main-content.svelte (100%) rename {lib => src/lib}/components/permission-guard.svelte (100%) rename {lib => src/lib}/components/theme-toggle.svelte (100%) rename {lib => src/lib}/components/ui/accordion/accordion-content.svelte (100%) rename {lib => src/lib}/components/ui/accordion/accordion-item.svelte (100%) rename {lib => src/lib}/components/ui/accordion/accordion-trigger.svelte (100%) rename {lib => src/lib}/components/ui/accordion/accordion.svelte (100%) rename {lib => src/lib}/components/ui/accordion/index.ts (100%) rename {lib => src/lib}/components/ui/alert/alert-description.svelte (100%) rename {lib => src/lib}/components/ui/alert/alert-title.svelte (100%) rename {lib => src/lib}/components/ui/alert/alert.svelte (100%) rename {lib => src/lib}/components/ui/alert/index.ts (100%) rename {lib => src/lib}/components/ui/avatar/avatar-fallback.svelte (100%) rename {lib => src/lib}/components/ui/avatar/avatar-image.svelte (100%) rename {lib => src/lib}/components/ui/avatar/avatar.svelte (100%) rename {lib => src/lib}/components/ui/avatar/index.ts (100%) rename {lib => src/lib}/components/ui/badge/badge.svelte (100%) rename {lib => src/lib}/components/ui/badge/index.ts (100%) rename {lib => src/lib}/components/ui/button/button.svelte (100%) rename {lib => src/lib}/components/ui/button/index.ts (100%) rename {lib => src/lib}/components/ui/calendar/calendar-caption.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-cell.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-day.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-grid-body.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-grid-head.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-grid-row.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-grid.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-head-cell.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-header.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-heading.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-month-select.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-month.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-months.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-nav.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-next-button.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-prev-button.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar-year-select.svelte (100%) rename {lib => src/lib}/components/ui/calendar/calendar.svelte (100%) rename {lib => src/lib}/components/ui/calendar/index.ts (100%) rename {lib => src/lib}/components/ui/card/card-action.svelte (100%) rename {lib => src/lib}/components/ui/card/card-content.svelte (100%) rename {lib => src/lib}/components/ui/card/card-description.svelte (100%) rename {lib => src/lib}/components/ui/card/card-footer.svelte (100%) rename {lib => src/lib}/components/ui/card/card-header.svelte (100%) rename {lib => src/lib}/components/ui/card/card-title.svelte (100%) rename {lib => src/lib}/components/ui/card/card.svelte (100%) rename {lib => src/lib}/components/ui/card/index.ts (100%) rename {lib => src/lib}/components/ui/checkbox/checkbox.svelte (100%) rename {lib => src/lib}/components/ui/checkbox/index.ts (100%) rename {lib => src/lib}/components/ui/command/command-dialog.svelte (100%) rename {lib => src/lib}/components/ui/command/command-empty.svelte (100%) rename {lib => src/lib}/components/ui/command/command-group.svelte (100%) rename {lib => src/lib}/components/ui/command/command-input.svelte (100%) rename {lib => src/lib}/components/ui/command/command-item.svelte (100%) rename {lib => src/lib}/components/ui/command/command-link-item.svelte (100%) rename {lib => src/lib}/components/ui/command/command-list.svelte (100%) rename {lib => src/lib}/components/ui/command/command-loading.svelte (100%) rename {lib => src/lib}/components/ui/command/command-separator.svelte (100%) rename {lib => src/lib}/components/ui/command/command-shortcut.svelte (100%) rename {lib => src/lib}/components/ui/command/command.svelte (100%) rename {lib => src/lib}/components/ui/command/index.ts (100%) rename {lib => src/lib}/components/ui/date-picker/date-picker.svelte (100%) rename {lib => src/lib}/components/ui/date-picker/index.ts (100%) rename {lib => src/lib}/components/ui/dialog/dialog-close.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-content.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-description.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-footer.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-header.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-overlay.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-portal.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-title.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog-trigger.svelte (100%) rename {lib => src/lib}/components/ui/dialog/dialog.svelte (100%) rename {lib => src/lib}/components/ui/dialog/index.ts (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-content.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-group.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-item.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-label.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-portal.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-separator.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-sub.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu-trigger.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/dropdown-menu.svelte (100%) rename {lib => src/lib}/components/ui/dropdown-menu/index.ts (100%) rename {lib => src/lib}/components/ui/empty-state/empty-state.svelte (100%) rename {lib => src/lib}/components/ui/empty-state/index.ts (100%) rename {lib => src/lib}/components/ui/empty-state/no-environment.svelte (100%) rename {lib => src/lib}/components/ui/input/index.ts (100%) rename {lib => src/lib}/components/ui/input/input.svelte (100%) rename {lib => src/lib}/components/ui/label/index.ts (100%) rename {lib => src/lib}/components/ui/label/label.svelte (100%) rename {lib => src/lib}/components/ui/popover/index.ts (100%) rename {lib => src/lib}/components/ui/popover/popover-content.svelte (100%) rename {lib => src/lib}/components/ui/popover/popover-trigger.svelte (100%) rename {lib => src/lib}/components/ui/progress/index.ts (100%) rename {lib => src/lib}/components/ui/progress/progress.svelte (100%) rename {lib => src/lib}/components/ui/select/index.ts (100%) rename {lib => src/lib}/components/ui/select/select-content.svelte (100%) rename {lib => src/lib}/components/ui/select/select-group-heading.svelte (100%) rename {lib => src/lib}/components/ui/select/select-group.svelte (100%) rename {lib => src/lib}/components/ui/select/select-item.svelte (100%) rename {lib => src/lib}/components/ui/select/select-label.svelte (100%) rename {lib => src/lib}/components/ui/select/select-scroll-down-button.svelte (100%) rename {lib => src/lib}/components/ui/select/select-scroll-up-button.svelte (100%) rename {lib => src/lib}/components/ui/select/select-separator.svelte (100%) rename {lib => src/lib}/components/ui/select/select-trigger.svelte (100%) rename {lib => src/lib}/components/ui/separator/index.ts (100%) rename {lib => src/lib}/components/ui/separator/separator.svelte (100%) rename {lib => src/lib}/components/ui/sheet/index.ts (100%) rename {lib => src/lib}/components/ui/sheet/sheet-close.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-content.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-description.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-footer.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-header.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-overlay.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-title.svelte (100%) rename {lib => src/lib}/components/ui/sheet/sheet-trigger.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/constants.ts (100%) rename {lib => src/lib}/components/ui/sidebar/context.svelte.ts (100%) rename {lib => src/lib}/components/ui/sidebar/index.ts (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-content.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-footer.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-group-action.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-group-content.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-group-label.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-group.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-header.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-input.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-inset.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-action.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-badge.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-button.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-item.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-skeleton.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-sub-button.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-sub-item.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu-sub.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-menu.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-provider.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-rail.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-separator.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar-trigger.svelte (100%) rename {lib => src/lib}/components/ui/sidebar/sidebar.svelte (100%) rename {lib => src/lib}/components/ui/skeleton/index.ts (100%) rename {lib => src/lib}/components/ui/skeleton/skeleton.svelte (100%) rename {lib => src/lib}/components/ui/sonner/index.ts (100%) rename {lib => src/lib}/components/ui/sonner/sonner.svelte (100%) rename {lib => src/lib}/components/ui/switch/index.ts (100%) rename {lib => src/lib}/components/ui/switch/switch.svelte (100%) rename {lib => src/lib}/components/ui/table/index.ts (100%) rename {lib => src/lib}/components/ui/table/table-body.svelte (100%) rename {lib => src/lib}/components/ui/table/table-caption.svelte (100%) rename {lib => src/lib}/components/ui/table/table-cell.svelte (100%) rename {lib => src/lib}/components/ui/table/table-footer.svelte (100%) rename {lib => src/lib}/components/ui/table/table-head.svelte (100%) rename {lib => src/lib}/components/ui/table/table-header.svelte (100%) rename {lib => src/lib}/components/ui/table/table-row.svelte (100%) rename {lib => src/lib}/components/ui/table/table.svelte (100%) rename {lib => src/lib}/components/ui/tabs/index.ts (100%) rename {lib => src/lib}/components/ui/tabs/tabs-content.svelte (100%) rename {lib => src/lib}/components/ui/tabs/tabs-list.svelte (100%) rename {lib => src/lib}/components/ui/tabs/tabs-trigger.svelte (100%) rename {lib => src/lib}/components/ui/tabs/tabs.svelte (100%) rename {lib => src/lib}/components/ui/textarea/index.ts (100%) rename {lib => src/lib}/components/ui/textarea/textarea.svelte (100%) rename {lib => src/lib}/components/ui/toggle-pill/index.ts (100%) rename {lib => src/lib}/components/ui/toggle-pill/toggle-group.svelte (100%) rename {lib => src/lib}/components/ui/toggle-pill/toggle-pill.svelte (100%) rename {lib => src/lib}/components/ui/toggle-pill/toggle-switch.svelte (100%) rename {lib => src/lib}/components/ui/tooltip/index.ts (100%) rename {lib => src/lib}/components/ui/tooltip/tooltip-content.svelte (100%) rename {lib => src/lib}/components/ui/tooltip/tooltip-trigger.svelte (100%) rename {lib => src/lib}/config/grid-columns.ts (100%) rename {lib => src/lib}/data/changelog.json (100%) rename {lib => src/lib}/data/dependencies.json (100%) rename {lib => src/lib}/hooks/is-mobile.svelte.ts (100%) rename {lib => src/lib}/index.ts (100%) rename {lib => src/lib}/server/audit-events.ts (100%) rename {lib => src/lib}/server/audit.ts (100%) rename {lib => src/lib}/server/auth.ts (100%) rename {lib => src/lib}/server/authorize.ts (100%) rename {lib => src/lib}/server/db.ts (100%) rename {lib => src/lib}/server/db/connection.ts (100%) rename {lib => src/lib}/server/db/drizzle.ts (100%) rename {lib => src/lib}/server/db/schema/index.ts (100%) rename {lib => src/lib}/server/db/schema/pg-schema.ts (100%) rename {lib => src/lib}/server/docker.ts (100%) rename {lib => src/lib}/server/event-collector.ts (100%) rename {lib => src/lib}/server/git.ts (100%) rename {lib => src/lib}/server/hawser.ts (100%) rename {lib => src/lib}/server/license.ts (100%) rename {lib => src/lib}/server/metrics-collector.ts (100%) rename {lib => src/lib}/server/notifications.ts (100%) rename {lib => src/lib}/server/scanner.ts (100%) rename {lib => src/lib}/server/scheduler/index.ts (100%) rename {lib => src/lib}/server/scheduler/tasks/container-update.ts (100%) rename {lib => src/lib}/server/scheduler/tasks/env-update-check.ts (100%) rename {lib => src/lib}/server/scheduler/tasks/git-stack-sync.ts (100%) rename {lib => src/lib}/server/scheduler/tasks/system-cleanup.ts (100%) rename {lib => src/lib}/server/scheduler/tasks/update-utils.ts (100%) rename {lib => src/lib}/server/stacks.ts (100%) rename {lib => src/lib}/server/subprocess-manager.ts (100%) rename {lib => src/lib}/server/subprocesses/event-subprocess.ts (100%) rename {lib => src/lib}/server/subprocesses/metrics-subprocess.ts (100%) rename {lib => src/lib}/server/uptime.ts (100%) rename {lib => src/lib}/stores/audit-events.ts (100%) rename {lib => src/lib}/stores/auth.ts (100%) rename {lib => src/lib}/stores/dashboard.ts (100%) rename {lib => src/lib}/stores/environment.ts (100%) rename {lib => src/lib}/stores/events.ts (100%) rename {lib => src/lib}/stores/grid-preferences.ts (100%) rename {lib => src/lib}/stores/license.ts (100%) rename {lib => src/lib}/stores/settings.ts (100%) rename {lib => src/lib}/stores/stats.ts (100%) rename {lib => src/lib}/stores/theme.ts (100%) rename {lib => src/lib}/themes.ts (100%) rename {lib => src/lib}/types.ts (100%) rename {lib => src/lib}/utils.ts (100%) rename {lib => src/lib}/utils/icons.ts (100%) rename {lib => src/lib}/utils/ip.ts (100%) rename {lib => src/lib}/utils/label-colors.ts (100%) rename {lib => src/lib}/utils/update-steps.ts (100%) rename {lib => src/lib}/utils/version.ts (100%) rename {routes => src/routes}/+layout.server.ts (100%) rename {routes => src/routes}/+layout.svelte (100%) rename {routes => src/routes}/+layout.ts (100%) rename {routes => src/routes}/+page.svelte (100%) rename {routes => src/routes}/activity/+page.svelte (100%) rename {routes => src/routes}/alerts/+page.svelte (100%) rename {routes => src/routes}/api/activity/+server.ts (100%) rename {routes => src/routes}/api/activity/containers/+server.ts (100%) rename {routes => src/routes}/api/activity/events/+server.ts (100%) rename {routes => src/routes}/api/activity/stats/+server.ts (100%) rename {routes => src/routes}/api/audit/+server.ts (100%) rename {routes => src/routes}/api/audit/events/+server.ts (100%) rename {routes => src/routes}/api/audit/export/+server.ts (100%) rename {routes => src/routes}/api/audit/users/+server.ts (100%) rename {routes => src/routes}/api/auth/ldap/+server.ts (100%) rename {routes => src/routes}/api/auth/ldap/[id]/+server.ts (100%) rename {routes => src/routes}/api/auth/ldap/[id]/test/+server.ts (100%) rename {routes => src/routes}/api/auth/login/+server.ts (100%) rename {routes => src/routes}/api/auth/logout/+server.ts (100%) rename {routes => src/routes}/api/auth/oidc/+server.ts (100%) rename {routes => src/routes}/api/auth/oidc/[id]/+server.ts (100%) rename {routes => src/routes}/api/auth/oidc/[id]/initiate/+server.ts (100%) rename {routes => src/routes}/api/auth/oidc/[id]/test/+server.ts (100%) rename {routes => src/routes}/api/auth/oidc/callback/+server.ts (100%) rename {routes => src/routes}/api/auth/providers/+server.ts (100%) rename {routes => src/routes}/api/auth/session/+server.ts (100%) rename {routes => src/routes}/api/auth/settings/+server.ts (100%) rename {routes => src/routes}/api/auto-update/+server.ts (100%) rename {routes => src/routes}/api/auto-update/[containerName]/+server.ts (100%) rename {routes => src/routes}/api/batch/+server.ts (100%) rename {routes => src/routes}/api/changelog/+server.ts (100%) rename {routes => src/routes}/api/config-sets/+server.ts (100%) rename {routes => src/routes}/api/config-sets/[id]/+server.ts (100%) rename {routes => src/routes}/api/containers/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/exec/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/chmod/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/content/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/create/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/delete/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/download/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/rename/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/files/upload/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/inspect/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/logs/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/logs/stream/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/pause/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/rename/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/restart/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/start/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/stats/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/stop/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/top/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/unpause/+server.ts (100%) rename {routes => src/routes}/api/containers/[id]/update/+server.ts (100%) rename {routes => src/routes}/api/containers/batch-update-stream/+server.ts (100%) rename {routes => src/routes}/api/containers/batch-update/+server.ts (100%) rename {routes => src/routes}/api/containers/check-updates/+server.ts (100%) rename {routes => src/routes}/api/containers/pending-updates/+server.ts (100%) rename {routes => src/routes}/api/containers/sizes/+server.ts (100%) rename {routes => src/routes}/api/containers/stats/+server.ts (100%) rename {routes => src/routes}/api/dashboard/preferences/+server.ts (100%) rename {routes => src/routes}/api/dashboard/stats/+server.ts (100%) rename {routes => src/routes}/api/dashboard/stats/stream/+server.ts (100%) rename {routes => src/routes}/api/dependencies/+server.ts (100%) rename {routes => src/routes}/api/environments/+server.ts (100%) rename {routes => src/routes}/api/environments/[id]/+server.ts (100%) rename {routes => src/routes}/api/environments/[id]/notifications/+server.ts (100%) rename {routes => src/routes}/api/environments/[id]/notifications/[notificationId]/+server.ts (100%) rename {routes => src/routes}/api/environments/[id]/test/+server.ts (100%) rename {routes => src/routes}/api/environments/[id]/timezone/+server.ts (100%) rename {routes => src/routes}/api/environments/[id]/update-check/+server.ts (100%) rename {routes => src/routes}/api/environments/detect-socket/+server.ts (100%) rename {routes => src/routes}/api/environments/test/+server.ts (100%) rename {routes => src/routes}/api/events/+server.ts (100%) rename {routes => src/routes}/api/git/credentials/+server.ts (100%) rename {routes => src/routes}/api/git/credentials/[id]/+server.ts (100%) rename {routes => src/routes}/api/git/repositories/+server.ts (100%) rename {routes => src/routes}/api/git/repositories/[id]/+server.ts (100%) rename {routes => src/routes}/api/git/repositories/[id]/deploy/+server.ts (100%) rename {routes => src/routes}/api/git/repositories/[id]/sync/+server.ts (100%) rename {routes => src/routes}/api/git/repositories/[id]/test/+server.ts (100%) rename {routes => src/routes}/api/git/repositories/test/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/deploy-stream/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/deploy/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/env-files/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/sync/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/test/+server.ts (100%) rename {routes => src/routes}/api/git/stacks/[id]/webhook/+server.ts (100%) rename {routes => src/routes}/api/git/webhook/[id]/+server.ts (100%) rename {routes => src/routes}/api/hawser/connect/+server.ts (100%) rename {routes => src/routes}/api/hawser/tokens/+server.ts (100%) rename {routes => src/routes}/api/health/+server.ts (100%) rename {routes => src/routes}/api/health/database/+server.ts (100%) rename {routes => src/routes}/api/host/+server.ts (100%) rename {routes => src/routes}/api/images/+server.ts (100%) rename {routes => src/routes}/api/images/[id]/+server.ts (100%) rename {routes => src/routes}/api/images/[id]/export/+server.ts (100%) rename {routes => src/routes}/api/images/[id]/history/+server.ts (100%) rename {routes => src/routes}/api/images/[id]/tag/+server.ts (100%) rename {routes => src/routes}/api/images/pull/+server.ts (100%) rename {routes => src/routes}/api/images/push/+server.ts (100%) rename {routes => src/routes}/api/images/scan/+server.ts (100%) rename {routes => src/routes}/api/legal/license/+server.ts (100%) rename {routes => src/routes}/api/legal/privacy/+server.ts (100%) rename {routes => src/routes}/api/license/+server.ts (100%) rename {routes => src/routes}/api/logs/merged/+server.ts (100%) rename {routes => src/routes}/api/metrics/+server.ts (100%) rename {routes => src/routes}/api/networks/+server.ts (100%) rename {routes => src/routes}/api/networks/[id]/+server.ts (100%) rename {routes => src/routes}/api/networks/[id]/connect/+server.ts (100%) rename {routes => src/routes}/api/networks/[id]/disconnect/+server.ts (100%) rename {routes => src/routes}/api/networks/[id]/inspect/+server.ts (100%) rename {routes => src/routes}/api/notifications/+server.ts (100%) rename {routes => src/routes}/api/notifications/[id]/+server.ts (100%) rename {routes => src/routes}/api/notifications/[id]/test/+server.ts (100%) rename {routes => src/routes}/api/notifications/test/+server.ts (100%) rename {routes => src/routes}/api/notifications/trigger-test/+server.ts (100%) rename {routes => src/routes}/api/preferences/favorite-groups/+server.ts (100%) rename {routes => src/routes}/api/preferences/favorites/+server.ts (100%) rename {routes => src/routes}/api/preferences/grid/+server.ts (100%) rename {routes => src/routes}/api/profile/+server.ts (100%) rename {routes => src/routes}/api/profile/avatar/+server.ts (100%) rename {routes => src/routes}/api/profile/preferences/+server.ts (100%) rename {routes => src/routes}/api/prune/all/+server.ts (100%) rename {routes => src/routes}/api/prune/containers/+server.ts (100%) rename {routes => src/routes}/api/prune/images/+server.ts (100%) rename {routes => src/routes}/api/prune/networks/+server.ts (100%) rename {routes => src/routes}/api/prune/volumes/+server.ts (100%) rename {routes => src/routes}/api/registries/+server.ts (100%) rename {routes => src/routes}/api/registries/[id]/+server.ts (100%) rename {routes => src/routes}/api/registries/[id]/default/+server.ts (100%) rename {routes => src/routes}/api/registry/catalog/+server.ts (100%) rename {routes => src/routes}/api/registry/image/+server.ts (100%) rename {routes => src/routes}/api/registry/search/+server.ts (100%) rename {routes => src/routes}/api/registry/tags/+server.ts (100%) rename {routes => src/routes}/api/roles/+server.ts (100%) rename {routes => src/routes}/api/roles/[id]/+server.ts (100%) rename {routes => src/routes}/api/schedules/+server.ts (100%) rename {routes => src/routes}/api/schedules/[type]/[id]/+server.ts (100%) rename {routes => src/routes}/api/schedules/[type]/[id]/run/+server.ts (100%) rename {routes => src/routes}/api/schedules/[type]/[id]/toggle/+server.ts (100%) rename {routes => src/routes}/api/schedules/executions/+server.ts (100%) rename {routes => src/routes}/api/schedules/executions/[id]/+server.ts (100%) rename {routes => src/routes}/api/schedules/settings/+server.ts (100%) rename {routes => src/routes}/api/schedules/stream/+server.ts (100%) rename {routes => src/routes}/api/schedules/system/[id]/toggle/+server.ts (100%) rename {routes => src/routes}/api/settings/general/+server.ts (100%) rename {routes => src/routes}/api/settings/scanner/+server.ts (100%) rename {routes => src/routes}/api/stacks/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/compose/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/down/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/env/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/env/validate/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/restart/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/start/+server.ts (100%) rename {routes => src/routes}/api/stacks/[name]/stop/+server.ts (100%) rename {routes => src/routes}/api/stacks/sources/+server.ts (100%) rename {routes => src/routes}/api/system/+server.ts (100%) rename {routes => src/routes}/api/system/disk/+server.ts (100%) rename {routes => src/routes}/api/users/+server.ts (100%) rename {routes => src/routes}/api/users/[id]/+server.ts (100%) rename {routes => src/routes}/api/users/[id]/mfa/+server.ts (100%) rename {routes => src/routes}/api/users/[id]/roles/+server.ts (100%) rename {routes => src/routes}/api/volumes/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/browse/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/browse/content/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/browse/release/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/clone/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/export/+server.ts (100%) rename {routes => src/routes}/api/volumes/[name]/inspect/+server.ts (100%) rename {routes => src/routes}/audit/+page.svelte (100%) rename {routes => src/routes}/audit/+server.ts (100%) rename {routes => src/routes}/audit/users/+server.ts (100%) rename {routes => src/routes}/containers/+page.svelte (100%) rename {routes => src/routes}/containers/AutoUpdateSettings.svelte (100%) rename {routes => src/routes}/containers/BatchUpdateModal.svelte (100%) rename {routes => src/routes}/containers/ContainerInspectModal.svelte (100%) rename {routes => src/routes}/containers/ContainerTerminal.svelte (100%) rename {routes => src/routes}/containers/ContainerTile.svelte (100%) rename {routes => src/routes}/containers/CreateContainerModal.svelte (100%) rename {routes => src/routes}/containers/EditContainerModal.svelte (100%) rename {routes => src/routes}/containers/FileBrowserModal.svelte (100%) rename {routes => src/routes}/containers/FileBrowserPanel.svelte (100%) rename {routes => src/routes}/dashboard/DraggableGrid.svelte (100%) rename {routes => src/routes}/dashboard/EnvironmentTile.svelte (100%) rename {routes => src/routes}/dashboard/EnvironmentTileSkeleton.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-container-stats.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-cpu-memory-bars.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-cpu-memory-charts.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-disk-usage.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-events-summary.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-header.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-health-banner.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-labels.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-offline-state.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-recent-events.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-resource-stats.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-status-icons.svelte (100%) rename {routes => src/routes}/dashboard/dashboard-top-containers.svelte (100%) rename {routes => src/routes}/dashboard/index.ts (100%) rename {routes => src/routes}/environments/+page.svelte (100%) rename {routes => src/routes}/images/+page.server.ts (100%) rename {routes => src/routes}/images/+page.svelte (100%) rename {routes => src/routes}/images/ImageHistoryModal.svelte (100%) rename {routes => src/routes}/images/ImageLayersView.svelte (100%) rename {routes => src/routes}/images/ImagePullProgressPopover.svelte (100%) rename {routes => src/routes}/images/ImageScanModal.svelte (100%) rename {routes => src/routes}/images/PushToRegistryModal.svelte (100%) rename {routes => src/routes}/images/ScanResultsView.svelte (100%) rename {routes => src/routes}/images/VulnerabilityScanModal.svelte (100%) rename {routes => src/routes}/login/+page.svelte (100%) rename {routes => src/routes}/logs/+page.svelte (100%) rename {routes => src/routes}/logs/LogViewer.svelte (100%) rename {routes => src/routes}/logs/LogsPanel.svelte (100%) rename {routes => src/routes}/networks/+page.svelte (100%) rename {routes => src/routes}/networks/ConnectContainerModal.svelte (100%) rename {routes => src/routes}/networks/CreateNetworkModal.svelte (100%) rename {routes => src/routes}/networks/NetworkInspectModal.svelte (100%) rename {routes => src/routes}/profile/+page.svelte (100%) rename {routes => src/routes}/profile/ChangePasswordModal.svelte (100%) rename {routes => src/routes}/profile/DisableMfaModal.svelte (100%) rename {routes => src/routes}/profile/MfaSetupModal.svelte (100%) rename {routes => src/routes}/registry/+page.svelte (100%) rename {routes => src/routes}/registry/CopyToRegistryModal.svelte (100%) rename {routes => src/routes}/registry/ImagePullModal.svelte (100%) rename {routes => src/routes}/schedules/+page.svelte (100%) rename {routes => src/routes}/settings/+page.svelte (100%) rename {routes => src/routes}/settings/about/AboutTab.svelte (100%) rename {routes => src/routes}/settings/about/LicenseModal.svelte (100%) rename {routes => src/routes}/settings/about/PrivacyModal.svelte (100%) rename {routes => src/routes}/settings/auth/AuthTab.svelte (100%) rename {routes => src/routes}/settings/auth/ldap/LdapModal.svelte (100%) rename {routes => src/routes}/settings/auth/ldap/LdapSubTab.svelte (100%) rename {routes => src/routes}/settings/auth/oidc/OidcModal.svelte (100%) rename {routes => src/routes}/settings/auth/oidc/SsoSubTab.svelte (100%) rename {routes => src/routes}/settings/auth/roles/RoleModal.svelte (100%) rename {routes => src/routes}/settings/auth/roles/RolesSubTab.svelte (100%) rename {routes => src/routes}/settings/auth/users/UserModal.svelte (100%) rename {routes => src/routes}/settings/auth/users/UsersSubTab.svelte (100%) rename {routes => src/routes}/settings/config-sets/ConfigSetModal.svelte (100%) rename {routes => src/routes}/settings/config-sets/ConfigSetsTab.svelte (100%) rename {routes => src/routes}/settings/environments/EnvironmentModal.svelte (100%) rename {routes => src/routes}/settings/environments/EnvironmentsTab.svelte (100%) rename {routes => src/routes}/settings/environments/EventTypesEditor.svelte (100%) rename {routes => src/routes}/settings/general/GeneralTab.svelte (100%) rename {routes => src/routes}/settings/git/GitCredentialModal.svelte (100%) rename {routes => src/routes}/settings/git/GitCredentialsTab.svelte (100%) rename {routes => src/routes}/settings/git/GitRepositoriesTab.svelte (100%) rename {routes => src/routes}/settings/git/GitRepositoryModal.svelte (100%) rename {routes => src/routes}/settings/git/GitTab.svelte (100%) rename {routes => src/routes}/settings/license/LicenseTab.svelte (100%) rename {routes => src/routes}/settings/notifications/NotificationModal.svelte (100%) rename {routes => src/routes}/settings/notifications/NotificationsTab.svelte (100%) rename {routes => src/routes}/settings/registries/RegistriesTab.svelte (100%) rename {routes => src/routes}/settings/registries/RegistryModal.svelte (100%) rename {routes => src/routes}/stacks/+page.svelte (100%) rename {routes => src/routes}/stacks/ComposeGraphViewer.svelte (100%) rename {routes => src/routes}/stacks/GitDeployProgressPopover.svelte (100%) rename {routes => src/routes}/stacks/GitStackModal.svelte (100%) rename {routes => src/routes}/stacks/StackModal.svelte (100%) rename {routes => src/routes}/terminal/+page.svelte (100%) rename {routes => src/routes}/terminal/Terminal.svelte (100%) rename {routes => src/routes}/terminal/TerminalEmulator.svelte (100%) rename {routes => src/routes}/terminal/TerminalPanel.svelte (100%) rename {routes => src/routes}/terminal/[id]/+page.svelte (100%) rename {routes => src/routes}/volumes/+page.svelte (100%) rename {routes => src/routes}/volumes/CloneVolumeModal.svelte (100%) rename {routes => src/routes}/volumes/CreateVolumeModal.svelte (100%) rename {routes => src/routes}/volumes/VolumeBrowserModal.svelte (100%) rename {routes => src/routes}/volumes/VolumeInspectModal.svelte (100%) create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f8aa0d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# Build stage - using Debian to avoid Alpine musl thread creation issues +# Alpine's musl libc causes rayon/tokio thread pool panics during svelte-adapter-bun build +FROM oven/bun:1.3.5-debian AS builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends jq git && rm -rf /var/lib/apt/lists/* + +# Copy package files and install ALL dependencies (needed for build) +COPY package.json bun.lock* bunfig.toml ./ +RUN bun install --frozen-lockfile + +# Copy source code and build +COPY . . + +# Build with parallelism - dedicated build VM has 16 CPUs and 32GB RAM +# Increased memory limits for parallel compilation with larger semi-space for GC +RUN NODE_OPTIONS="--max-old-space-size=8192 --max-semi-space-size=128" bun run build + +# Production stage - minimal Alpine with Bun runtime +FROM oven/bun:1.3.5-alpine + +WORKDIR /app + +# Install runtime dependencies, create user +# Add sqlite for emergency scripts, git for stack git operations, curl for healthchecks +# Add docker-cli and docker-cli-compose for stack management (uses host's docker socket) +# Add openssh-client for SSH key authentication with git repositories +# Upgrade all packages to latest versions for security patches +RUN apk upgrade --no-cache \ + && apk add --no-cache curl git tini su-exec sqlite docker-cli docker-cli-compose openssh-client iproute2 \ + && addgroup -g 1001 dockhand \ + && adduser -u 1001 -G dockhand -h /home/dockhand -D dockhand + +# Copy package files and install production dependencies +# This is needed because svelte-adapter-bun externalizes some packages (croner, etc.) +# that need to be available at runtime. Installing at build time is more reliable +# than Bun's auto-install which requires network access and writable cache. +COPY package.json bun.lock* ./ +RUN bun install --production --frozen-lockfile + +# Copy built application (Bun adapter output) +COPY --from=builder /app/build ./build + +# Copy bundled subprocess scripts (built by scripts/build-subprocesses.ts) +COPY --from=builder /app/build/subprocesses/ ./subprocesses/ + +# Copy database migrations +COPY drizzle/ ./drizzle/ +COPY drizzle-pg/ ./drizzle-pg/ + +# Copy legal documents +COPY LICENSE.txt PRIVACY.txt ./ + +# Copy entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Copy emergency scripts (only the emergency subfolder, not license generation scripts) +COPY scripts/emergency/ ./scripts/ +RUN chmod +x ./scripts/*.sh 2>/dev/null || true + +# Create directories with proper ownership +RUN mkdir -p /home/dockhand/.dockhand/stacks /app/data \ + && chown -R dockhand:dockhand /app /home/dockhand + +EXPOSE 3000 + +# Runtime configuration +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOST=0.0.0.0 +ENV DATA_DIR=/app/data +ENV HOME=/home/dockhand + +# User/group IDs - customize with -e PUID=1000 -e PGID=1000 +# The entrypoint will recreate the dockhand user with these IDs +ENV PUID=1001 +ENV PGID=1001 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/ || exit 1 + +ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] +CMD ["bun", "run", "./build/index.js"] diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..51bc0ff --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,9 @@ +# Bun configuration for Dockhand + +[install] +# Use exact versions for reproducible builds +exact = true + +[run] +# Enable source maps for better error messages +sourcemap = "external" diff --git a/components.json b/components.json new file mode 100644 index 0000000..c5d91b4 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..ecbc9b2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,122 @@ +#!/bin/sh +set -e + +# Dockhand Docker Entrypoint +# === Configuration === +PUID=${PUID:-1001} +PGID=${PGID:-1001} + +# === Detect if running as root === +RUNNING_AS_ROOT=false +if [ "$(id -u)" = "0" ]; then + RUNNING_AS_ROOT=true +fi + +# === User Setup === +# Root mode: PUID=0 requested OR already running as root with default PUID/PGID +if [ "$PUID" = "0" ]; then + echo "Running as root user (PUID=0)" + RUN_USER="root" +elif [ "$RUNNING_AS_ROOT" = "true" ] && [ "$PUID" = "1001" ] && [ "$PGID" = "1001" ]; then + echo "Running as root user" + RUN_USER="root" +else + RUN_USER="dockhand" + # Only modify if PUID/PGID differ from image defaults (1001:1001) + if [ "$PUID" != "1001" ] || [ "$PGID" != "1001" ]; then + echo "Configuring user with PUID=$PUID PGID=$PGID" + + # Remove existing dockhand user/group (only dockhand, not others) + deluser dockhand 2>/dev/null || true + delgroup dockhand 2>/dev/null || true + + # Check for UID conflicts - warn but don't delete other users + if getent passwd "$PUID" >/dev/null 2>&1; then + EXISTING=$(getent passwd "$PUID" | cut -d: -f1) + echo "WARNING: UID $PUID already in use by '$EXISTING'. Using default UID 1001." + PUID=1001 + fi + + # Handle GID - reuse existing group or create new + if getent group "$PGID" >/dev/null 2>&1; then + TARGET_GROUP=$(getent group "$PGID" | cut -d: -f1) + else + addgroup -g "$PGID" dockhand + TARGET_GROUP="dockhand" + fi + + adduser -u "$PUID" -G "$TARGET_GROUP" -h /home/dockhand -D dockhand + fi + + # === Directory Ownership === + chown -R dockhand:dockhand /app/data /home/dockhand 2>/dev/null || true + + if [ -n "$DATA_DIR" ] && [ "$DATA_DIR" != "/app/data" ] && [ "$DATA_DIR" != "./data" ]; then + mkdir -p "$DATA_DIR" + chown -R dockhand:dockhand "$DATA_DIR" 2>/dev/null || true + fi +fi + +# === Docker Socket Access (Optional) === +# Check if Docker socket is mounted and accessible +# Socket path can be configured via environment-specific settings in the app +SOCKET_PATH="/var/run/docker.sock" + +if [ -S "$SOCKET_PATH" ]; then + # Socket exists - check if readable + if [ "$RUN_USER" != "root" ]; then + if ! su-exec "$RUN_USER" test -r "$SOCKET_PATH" 2>/dev/null; then + SOCKET_GID=$(stat -c '%g' "$SOCKET_PATH" 2>/dev/null || echo "unknown") + echo "WARNING: Docker socket at $SOCKET_PATH is not readable by dockhand user" + echo "" + echo "To use local Docker, fix with one of these options:" + echo "" + echo " 1. Add container to docker group (GID: $SOCKET_GID):" + echo " docker run --group-add $SOCKET_GID ..." + echo "" + echo " 2. Use a socket proxy:" + echo " Configure a 'direct' environment pointing to tcp://socket-proxy:2375" + echo "" + echo " 3. Make socket world-readable (less secure):" + echo " chmod 666 /var/run/docker.sock" + echo "" + echo "Continuing startup - configure environments via the web UI..." + else + echo "Docker socket accessible at $SOCKET_PATH" + fi + else + echo "Docker socket accessible at $SOCKET_PATH" + fi + + # === Detect Docker Host Hostname (for license validation) === + # Query Docker API to get the real host hostname (not container ID) + if [ -z "$DOCKHAND_HOSTNAME" ]; then + DETECTED_HOSTNAME=$(curl -s --unix-socket "$SOCKET_PATH" http://localhost/info 2>/dev/null | sed -n 's/.*"Name":"\([^"]*\)".*/\1/p') + if [ -n "$DETECTED_HOSTNAME" ]; then + export DOCKHAND_HOSTNAME="$DETECTED_HOSTNAME" + echo "Detected Docker host hostname: $DOCKHAND_HOSTNAME" + fi + else + echo "Using configured hostname: $DOCKHAND_HOSTNAME" + fi +else + echo "No Docker socket found at $SOCKET_PATH" + echo "Configure Docker environments via the web UI (Settings > Environments)" +fi + +# === Run Application === +if [ "$RUN_USER" = "root" ]; then + # Running as root - execute directly + if [ "$1" = "" ]; then + exec bun run ./build/index.js + else + exec "$@" + fi +else + # Running as dockhand user + if [ "$1" = "" ]; then + exec su-exec dockhand bun run ./build/index.js + else + exec su-exec dockhand "$@" + fi +fi diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..44f2d39 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'drizzle-kit'; + +const databaseUrl = process.env.DATABASE_URL; +const isPostgres = databaseUrl && (databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://')); + +export default defineConfig({ + // Use different schema files for SQLite vs PostgreSQL + schema: isPostgres + ? './src/lib/server/db/schema/pg-schema.ts' + : './src/lib/server/db/schema/index.ts', + out: isPostgres ? './drizzle-pg' : './drizzle', + dialect: isPostgres ? 'postgresql' : 'sqlite', + dbCredentials: isPostgres + ? { url: databaseUrl! } + : { url: `file:${process.env.DATA_DIR || './data'}/dockhand.db` } +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..24cc896 --- /dev/null +++ b/package.json @@ -0,0 +1,110 @@ +{ + "name": "dockhand", + "private": true, + "version": "1.0.4", + "type": "module", + "scripts": { + "dev": "bunx --bun vite dev", + "prebuild": "bunx license-checker --json --production | jq 'to_entries | map({name: (.key | split(\"@\")[0:-1] | join(\"@\")), version: (.key | split(\"@\")[-1]), license: .value.licenses, repository: .value.repository}) | sort_by(.name)' > src/lib/data/dependencies.json.tmp && mv src/lib/data/dependencies.json.tmp src/lib/data/dependencies.json || true", + "build": "bunx --bun vite build && bun scripts/patch-build.ts && bun scripts/build-subprocesses.ts", + "start": "bun ./build/index.js", + "preview": "bun ./build/index.js", + "prepare": "bunx --bun svelte-kit sync || echo ''", + "check": "bunx --bun svelte-kit sync && bunx --bun svelte-check --tsconfig ./tsconfig.json", + "check:watch": "bunx --bun svelte-kit sync && bunx --bun svelte-check --tsconfig ./tsconfig.json --watch", + "test": "bun test", + "test:smoke": "bun test tests/api-smoke.test.ts", + "test:containers": "bun test tests/container-lifecycle.test.ts", + "test:notifications": "bun test tests/notifications.test.ts", + "test:hawser": "bun test tests/hawser-connection.test.ts", + "test:build": "SKIP_BUILD_TEST=1 bun test tests/build.test.ts", + "test:postgres": "bun test tests/database-postgres.test.ts", + "test:crud": "bun test tests/crud-operations.test.ts", + "test:scheduling": "bun test tests/scheduling.test.ts", + "test:images": "bun test tests/images.test.ts", + "test:volumes": "bun test tests/volumes-networks.test.ts", + "test:stacks": "bun test tests/stacks.test.ts", + "test:stacks:matrix": "bun test tests/stack-matrix.test.ts", + "test:stacks:git": "bun test tests/stack-git-flow.test.ts", + "test:stacks:env": "bun test tests/stack-env-vars.test.ts", + "test:stacks:all": "bun test tests/stack-*.test.ts tests/stacks.test.ts", + "test:files": "bun test tests/container-files.test.ts", + "test:license": "bun test tests/license.test.ts", + "test:activity": "bun test tests/activity-dashboard.test.ts", + "test:all": "bun test tests/", + "test:quick": "bun test tests/api-smoke.test.ts tests/notifications.test.ts", + "test:integration": "bun test tests/api-smoke.test.ts tests/crud-operations.test.ts tests/scheduling.test.ts tests/hawser-connection.test.ts", + "test:e2e": "bunx playwright test tests/e2e/", + "generate:legal": "bun scripts/generate-legal-pages.ts" + }, + "dependencies": { + "@codemirror/autocomplete": "6.20.0", + "@codemirror/commands": "6.10.0", + "@codemirror/lang-css": "6.3.1", + "@codemirror/lang-html": "6.4.11", + "@codemirror/lang-javascript": "6.2.4", + "@codemirror/lang-json": "6.0.2", + "@codemirror/lang-markdown": "6.5.0", + "@codemirror/lang-python": "6.2.1", + "@codemirror/lang-sql": "6.10.0", + "@codemirror/lang-xml": "6.1.0", + "@codemirror/language": "6.11.3", + "@codemirror/search": "6.5.11", + "@lezer/highlight": "1.2.3", + "@lucide/lab": "^0.1.2", + "croner": "9.1.0", + "cronstrue": "3.9.0", + "drizzle-orm": "0.45.0", + "js-yaml": "^4.1.1", + "ldapts": "^8.0.9", + "nodemailer": "^7.0.11", + "otpauth": "^9.4.1", + "postgres": "3.4.7", + "qrcode": "^1.5.4", + "svelte-dnd-action": "0.9.68", + "svelte-sonner": "1.0.7" + }, + "devDependencies": { + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.8", + "@internationalized/date": "^3.10.0", + "@layerstack/tailwind": "^1.0.1", + "@lucide/svelte": "^0.544.0", + "@playwright/test": "1.57.0", + "@sveltejs/kit": "^2.48.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/vite": "^4.1.17", + "@types/bun": "^1.2.5", + "@types/js-yaml": "^4.0.9", + "@types/nodemailer": "^7.0.4", + "@types/qrcode": "^1.5.6", + "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-web-links": "^0.11.0", + "@xterm/xterm": "^5.5.0", + "autoprefixer": "^10.4.22", + "bits-ui": "^2.14.4", + "clsx": "^2.1.1", + "codemirror": "^6.0.2", + "cytoscape": "^3.33.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "drizzle-kit": "0.31.8", + "layerchart": "^1.0.12", + "lucide-svelte": "^0.555.0", + "mode-watcher": "^1.1.0", + "postcss": "^8.5.6", + "svelte": "^5.43.8", + "svelte-adapter-bun": "1.0.1", + "svelte-check": "^4.3.4", + "svelte-easy-crop": "^5.0.0", + "svelte-virtual-scroll-list": "^1.3.0", + "tailwind-merge": "^3.4.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.17", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "vite": "^7.2.2" + } +} diff --git a/app.css b/src/app.css similarity index 100% rename from app.css rename to src/app.css diff --git a/app.d.ts b/src/app.d.ts similarity index 100% rename from app.d.ts rename to src/app.d.ts diff --git a/app.html b/src/app.html similarity index 100% rename from app.html rename to src/app.html diff --git a/hooks.server.ts b/src/hooks.server.ts similarity index 100% rename from hooks.server.ts rename to src/hooks.server.ts diff --git a/images/logo.webp b/src/images/logo.webp similarity index 100% rename from images/logo.webp rename to src/images/logo.webp diff --git a/lib/actions/column-resize.ts b/src/lib/actions/column-resize.ts similarity index 100% rename from lib/actions/column-resize.ts rename to src/lib/actions/column-resize.ts diff --git a/lib/assets/favicon.svg b/src/lib/assets/favicon.svg similarity index 100% rename from lib/assets/favicon.svg rename to src/lib/assets/favicon.svg diff --git a/lib/components/AvatarCropper.svelte b/src/lib/components/AvatarCropper.svelte similarity index 100% rename from lib/components/AvatarCropper.svelte rename to src/lib/components/AvatarCropper.svelte diff --git a/lib/components/BatchOperationModal.svelte b/src/lib/components/BatchOperationModal.svelte similarity index 100% rename from lib/components/BatchOperationModal.svelte rename to src/lib/components/BatchOperationModal.svelte diff --git a/lib/components/CodeEditor.svelte b/src/lib/components/CodeEditor.svelte similarity index 100% rename from lib/components/CodeEditor.svelte rename to src/lib/components/CodeEditor.svelte diff --git a/lib/components/ColumnSettingsPopover.svelte b/src/lib/components/ColumnSettingsPopover.svelte similarity index 100% rename from lib/components/ColumnSettingsPopover.svelte rename to src/lib/components/ColumnSettingsPopover.svelte diff --git a/lib/components/CommandPalette.svelte b/src/lib/components/CommandPalette.svelte similarity index 100% rename from lib/components/CommandPalette.svelte rename to src/lib/components/CommandPalette.svelte diff --git a/lib/components/ConfirmPopover.svelte b/src/lib/components/ConfirmPopover.svelte similarity index 100% rename from lib/components/ConfirmPopover.svelte rename to src/lib/components/ConfirmPopover.svelte diff --git a/lib/components/ExecutionLogViewer.svelte b/src/lib/components/ExecutionLogViewer.svelte similarity index 100% rename from lib/components/ExecutionLogViewer.svelte rename to src/lib/components/ExecutionLogViewer.svelte diff --git a/lib/components/MultiSelectFilter.svelte b/src/lib/components/MultiSelectFilter.svelte similarity index 100% rename from lib/components/MultiSelectFilter.svelte rename to src/lib/components/MultiSelectFilter.svelte diff --git a/lib/components/PageHeader.svelte b/src/lib/components/PageHeader.svelte similarity index 100% rename from lib/components/PageHeader.svelte rename to src/lib/components/PageHeader.svelte diff --git a/lib/components/PasswordStrengthIndicator.svelte b/src/lib/components/PasswordStrengthIndicator.svelte similarity index 100% rename from lib/components/PasswordStrengthIndicator.svelte rename to src/lib/components/PasswordStrengthIndicator.svelte diff --git a/lib/components/PullTab.svelte b/src/lib/components/PullTab.svelte similarity index 100% rename from lib/components/PullTab.svelte rename to src/lib/components/PullTab.svelte diff --git a/lib/components/PushTab.svelte b/src/lib/components/PushTab.svelte similarity index 100% rename from lib/components/PushTab.svelte rename to src/lib/components/PushTab.svelte diff --git a/lib/components/ScanTab.svelte b/src/lib/components/ScanTab.svelte similarity index 100% rename from lib/components/ScanTab.svelte rename to src/lib/components/ScanTab.svelte diff --git a/lib/components/ScannerSeverityPills.svelte b/src/lib/components/ScannerSeverityPills.svelte similarity index 100% rename from lib/components/ScannerSeverityPills.svelte rename to src/lib/components/ScannerSeverityPills.svelte diff --git a/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte similarity index 100% rename from lib/components/Sidebar.svelte rename to src/lib/components/Sidebar.svelte diff --git a/lib/components/StackEnvVarsEditor.svelte b/src/lib/components/StackEnvVarsEditor.svelte similarity index 100% rename from lib/components/StackEnvVarsEditor.svelte rename to src/lib/components/StackEnvVarsEditor.svelte diff --git a/lib/components/StackEnvVarsPanel.svelte b/src/lib/components/StackEnvVarsPanel.svelte similarity index 100% rename from lib/components/StackEnvVarsPanel.svelte rename to src/lib/components/StackEnvVarsPanel.svelte diff --git a/lib/components/ThemeSelector.svelte b/src/lib/components/ThemeSelector.svelte similarity index 100% rename from lib/components/ThemeSelector.svelte rename to src/lib/components/ThemeSelector.svelte diff --git a/lib/components/TimezoneSelector.svelte b/src/lib/components/TimezoneSelector.svelte similarity index 100% rename from lib/components/TimezoneSelector.svelte rename to src/lib/components/TimezoneSelector.svelte diff --git a/lib/components/UpdateContainerRow.svelte b/src/lib/components/UpdateContainerRow.svelte similarity index 100% rename from lib/components/UpdateContainerRow.svelte rename to src/lib/components/UpdateContainerRow.svelte diff --git a/lib/components/UpdateStepIndicator.svelte b/src/lib/components/UpdateStepIndicator.svelte similarity index 100% rename from lib/components/UpdateStepIndicator.svelte rename to src/lib/components/UpdateStepIndicator.svelte diff --git a/lib/components/UpdateSummaryStats.svelte b/src/lib/components/UpdateSummaryStats.svelte similarity index 100% rename from lib/components/UpdateSummaryStats.svelte rename to src/lib/components/UpdateSummaryStats.svelte diff --git a/lib/components/VulnerabilityCriteriaBadge.svelte b/src/lib/components/VulnerabilityCriteriaBadge.svelte similarity index 100% rename from lib/components/VulnerabilityCriteriaBadge.svelte rename to src/lib/components/VulnerabilityCriteriaBadge.svelte diff --git a/lib/components/VulnerabilityCriteriaSelector.svelte b/src/lib/components/VulnerabilityCriteriaSelector.svelte similarity index 100% rename from lib/components/VulnerabilityCriteriaSelector.svelte rename to src/lib/components/VulnerabilityCriteriaSelector.svelte diff --git a/lib/components/WhatsNewModal.svelte b/src/lib/components/WhatsNewModal.svelte similarity index 100% rename from lib/components/WhatsNewModal.svelte rename to src/lib/components/WhatsNewModal.svelte diff --git a/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte similarity index 100% rename from lib/components/app-sidebar.svelte rename to src/lib/components/app-sidebar.svelte diff --git a/lib/components/cron-editor.svelte b/src/lib/components/cron-editor.svelte similarity index 100% rename from lib/components/cron-editor.svelte rename to src/lib/components/cron-editor.svelte diff --git a/lib/components/data-grid/DataGrid.svelte b/src/lib/components/data-grid/DataGrid.svelte similarity index 100% rename from lib/components/data-grid/DataGrid.svelte rename to src/lib/components/data-grid/DataGrid.svelte diff --git a/lib/components/data-grid/context.ts b/src/lib/components/data-grid/context.ts similarity index 100% rename from lib/components/data-grid/context.ts rename to src/lib/components/data-grid/context.ts diff --git a/lib/components/data-grid/index.ts b/src/lib/components/data-grid/index.ts similarity index 100% rename from lib/components/data-grid/index.ts rename to src/lib/components/data-grid/index.ts diff --git a/lib/components/data-grid/types.ts b/src/lib/components/data-grid/types.ts similarity index 100% rename from lib/components/data-grid/types.ts rename to src/lib/components/data-grid/types.ts diff --git a/lib/components/host-info.svelte b/src/lib/components/host-info.svelte similarity index 100% rename from lib/components/host-info.svelte rename to src/lib/components/host-info.svelte diff --git a/lib/components/icon-picker.svelte b/src/lib/components/icon-picker.svelte similarity index 100% rename from lib/components/icon-picker.svelte rename to src/lib/components/icon-picker.svelte diff --git a/lib/components/main-content.svelte b/src/lib/components/main-content.svelte similarity index 100% rename from lib/components/main-content.svelte rename to src/lib/components/main-content.svelte diff --git a/lib/components/permission-guard.svelte b/src/lib/components/permission-guard.svelte similarity index 100% rename from lib/components/permission-guard.svelte rename to src/lib/components/permission-guard.svelte diff --git a/lib/components/theme-toggle.svelte b/src/lib/components/theme-toggle.svelte similarity index 100% rename from lib/components/theme-toggle.svelte rename to src/lib/components/theme-toggle.svelte diff --git a/lib/components/ui/accordion/accordion-content.svelte b/src/lib/components/ui/accordion/accordion-content.svelte similarity index 100% rename from lib/components/ui/accordion/accordion-content.svelte rename to src/lib/components/ui/accordion/accordion-content.svelte diff --git a/lib/components/ui/accordion/accordion-item.svelte b/src/lib/components/ui/accordion/accordion-item.svelte similarity index 100% rename from lib/components/ui/accordion/accordion-item.svelte rename to src/lib/components/ui/accordion/accordion-item.svelte diff --git a/lib/components/ui/accordion/accordion-trigger.svelte b/src/lib/components/ui/accordion/accordion-trigger.svelte similarity index 100% rename from lib/components/ui/accordion/accordion-trigger.svelte rename to src/lib/components/ui/accordion/accordion-trigger.svelte diff --git a/lib/components/ui/accordion/accordion.svelte b/src/lib/components/ui/accordion/accordion.svelte similarity index 100% rename from lib/components/ui/accordion/accordion.svelte rename to src/lib/components/ui/accordion/accordion.svelte diff --git a/lib/components/ui/accordion/index.ts b/src/lib/components/ui/accordion/index.ts similarity index 100% rename from lib/components/ui/accordion/index.ts rename to src/lib/components/ui/accordion/index.ts diff --git a/lib/components/ui/alert/alert-description.svelte b/src/lib/components/ui/alert/alert-description.svelte similarity index 100% rename from lib/components/ui/alert/alert-description.svelte rename to src/lib/components/ui/alert/alert-description.svelte diff --git a/lib/components/ui/alert/alert-title.svelte b/src/lib/components/ui/alert/alert-title.svelte similarity index 100% rename from lib/components/ui/alert/alert-title.svelte rename to src/lib/components/ui/alert/alert-title.svelte diff --git a/lib/components/ui/alert/alert.svelte b/src/lib/components/ui/alert/alert.svelte similarity index 100% rename from lib/components/ui/alert/alert.svelte rename to src/lib/components/ui/alert/alert.svelte diff --git a/lib/components/ui/alert/index.ts b/src/lib/components/ui/alert/index.ts similarity index 100% rename from lib/components/ui/alert/index.ts rename to src/lib/components/ui/alert/index.ts diff --git a/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte similarity index 100% rename from lib/components/ui/avatar/avatar-fallback.svelte rename to src/lib/components/ui/avatar/avatar-fallback.svelte diff --git a/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte similarity index 100% rename from lib/components/ui/avatar/avatar-image.svelte rename to src/lib/components/ui/avatar/avatar-image.svelte diff --git a/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte similarity index 100% rename from lib/components/ui/avatar/avatar.svelte rename to src/lib/components/ui/avatar/avatar.svelte diff --git a/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts similarity index 100% rename from lib/components/ui/avatar/index.ts rename to src/lib/components/ui/avatar/index.ts diff --git a/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte similarity index 100% rename from lib/components/ui/badge/badge.svelte rename to src/lib/components/ui/badge/badge.svelte diff --git a/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts similarity index 100% rename from lib/components/ui/badge/index.ts rename to src/lib/components/ui/badge/index.ts diff --git a/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte similarity index 100% rename from lib/components/ui/button/button.svelte rename to src/lib/components/ui/button/button.svelte diff --git a/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts similarity index 100% rename from lib/components/ui/button/index.ts rename to src/lib/components/ui/button/index.ts diff --git a/lib/components/ui/calendar/calendar-caption.svelte b/src/lib/components/ui/calendar/calendar-caption.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-caption.svelte rename to src/lib/components/ui/calendar/calendar-caption.svelte diff --git a/lib/components/ui/calendar/calendar-cell.svelte b/src/lib/components/ui/calendar/calendar-cell.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-cell.svelte rename to src/lib/components/ui/calendar/calendar-cell.svelte diff --git a/lib/components/ui/calendar/calendar-day.svelte b/src/lib/components/ui/calendar/calendar-day.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-day.svelte rename to src/lib/components/ui/calendar/calendar-day.svelte diff --git a/lib/components/ui/calendar/calendar-grid-body.svelte b/src/lib/components/ui/calendar/calendar-grid-body.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-grid-body.svelte rename to src/lib/components/ui/calendar/calendar-grid-body.svelte diff --git a/lib/components/ui/calendar/calendar-grid-head.svelte b/src/lib/components/ui/calendar/calendar-grid-head.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-grid-head.svelte rename to src/lib/components/ui/calendar/calendar-grid-head.svelte diff --git a/lib/components/ui/calendar/calendar-grid-row.svelte b/src/lib/components/ui/calendar/calendar-grid-row.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-grid-row.svelte rename to src/lib/components/ui/calendar/calendar-grid-row.svelte diff --git a/lib/components/ui/calendar/calendar-grid.svelte b/src/lib/components/ui/calendar/calendar-grid.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-grid.svelte rename to src/lib/components/ui/calendar/calendar-grid.svelte diff --git a/lib/components/ui/calendar/calendar-head-cell.svelte b/src/lib/components/ui/calendar/calendar-head-cell.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-head-cell.svelte rename to src/lib/components/ui/calendar/calendar-head-cell.svelte diff --git a/lib/components/ui/calendar/calendar-header.svelte b/src/lib/components/ui/calendar/calendar-header.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-header.svelte rename to src/lib/components/ui/calendar/calendar-header.svelte diff --git a/lib/components/ui/calendar/calendar-heading.svelte b/src/lib/components/ui/calendar/calendar-heading.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-heading.svelte rename to src/lib/components/ui/calendar/calendar-heading.svelte diff --git a/lib/components/ui/calendar/calendar-month-select.svelte b/src/lib/components/ui/calendar/calendar-month-select.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-month-select.svelte rename to src/lib/components/ui/calendar/calendar-month-select.svelte diff --git a/lib/components/ui/calendar/calendar-month.svelte b/src/lib/components/ui/calendar/calendar-month.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-month.svelte rename to src/lib/components/ui/calendar/calendar-month.svelte diff --git a/lib/components/ui/calendar/calendar-months.svelte b/src/lib/components/ui/calendar/calendar-months.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-months.svelte rename to src/lib/components/ui/calendar/calendar-months.svelte diff --git a/lib/components/ui/calendar/calendar-nav.svelte b/src/lib/components/ui/calendar/calendar-nav.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-nav.svelte rename to src/lib/components/ui/calendar/calendar-nav.svelte diff --git a/lib/components/ui/calendar/calendar-next-button.svelte b/src/lib/components/ui/calendar/calendar-next-button.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-next-button.svelte rename to src/lib/components/ui/calendar/calendar-next-button.svelte diff --git a/lib/components/ui/calendar/calendar-prev-button.svelte b/src/lib/components/ui/calendar/calendar-prev-button.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-prev-button.svelte rename to src/lib/components/ui/calendar/calendar-prev-button.svelte diff --git a/lib/components/ui/calendar/calendar-year-select.svelte b/src/lib/components/ui/calendar/calendar-year-select.svelte similarity index 100% rename from lib/components/ui/calendar/calendar-year-select.svelte rename to src/lib/components/ui/calendar/calendar-year-select.svelte diff --git a/lib/components/ui/calendar/calendar.svelte b/src/lib/components/ui/calendar/calendar.svelte similarity index 100% rename from lib/components/ui/calendar/calendar.svelte rename to src/lib/components/ui/calendar/calendar.svelte diff --git a/lib/components/ui/calendar/index.ts b/src/lib/components/ui/calendar/index.ts similarity index 100% rename from lib/components/ui/calendar/index.ts rename to src/lib/components/ui/calendar/index.ts diff --git a/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte similarity index 100% rename from lib/components/ui/card/card-action.svelte rename to src/lib/components/ui/card/card-action.svelte diff --git a/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte similarity index 100% rename from lib/components/ui/card/card-content.svelte rename to src/lib/components/ui/card/card-content.svelte diff --git a/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte similarity index 100% rename from lib/components/ui/card/card-description.svelte rename to src/lib/components/ui/card/card-description.svelte diff --git a/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte similarity index 100% rename from lib/components/ui/card/card-footer.svelte rename to src/lib/components/ui/card/card-footer.svelte diff --git a/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte similarity index 100% rename from lib/components/ui/card/card-header.svelte rename to src/lib/components/ui/card/card-header.svelte diff --git a/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte similarity index 100% rename from lib/components/ui/card/card-title.svelte rename to src/lib/components/ui/card/card-title.svelte diff --git a/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte similarity index 100% rename from lib/components/ui/card/card.svelte rename to src/lib/components/ui/card/card.svelte diff --git a/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts similarity index 100% rename from lib/components/ui/card/index.ts rename to src/lib/components/ui/card/index.ts diff --git a/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte similarity index 100% rename from lib/components/ui/checkbox/checkbox.svelte rename to src/lib/components/ui/checkbox/checkbox.svelte diff --git a/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts similarity index 100% rename from lib/components/ui/checkbox/index.ts rename to src/lib/components/ui/checkbox/index.ts diff --git a/lib/components/ui/command/command-dialog.svelte b/src/lib/components/ui/command/command-dialog.svelte similarity index 100% rename from lib/components/ui/command/command-dialog.svelte rename to src/lib/components/ui/command/command-dialog.svelte diff --git a/lib/components/ui/command/command-empty.svelte b/src/lib/components/ui/command/command-empty.svelte similarity index 100% rename from lib/components/ui/command/command-empty.svelte rename to src/lib/components/ui/command/command-empty.svelte diff --git a/lib/components/ui/command/command-group.svelte b/src/lib/components/ui/command/command-group.svelte similarity index 100% rename from lib/components/ui/command/command-group.svelte rename to src/lib/components/ui/command/command-group.svelte diff --git a/lib/components/ui/command/command-input.svelte b/src/lib/components/ui/command/command-input.svelte similarity index 100% rename from lib/components/ui/command/command-input.svelte rename to src/lib/components/ui/command/command-input.svelte diff --git a/lib/components/ui/command/command-item.svelte b/src/lib/components/ui/command/command-item.svelte similarity index 100% rename from lib/components/ui/command/command-item.svelte rename to src/lib/components/ui/command/command-item.svelte diff --git a/lib/components/ui/command/command-link-item.svelte b/src/lib/components/ui/command/command-link-item.svelte similarity index 100% rename from lib/components/ui/command/command-link-item.svelte rename to src/lib/components/ui/command/command-link-item.svelte diff --git a/lib/components/ui/command/command-list.svelte b/src/lib/components/ui/command/command-list.svelte similarity index 100% rename from lib/components/ui/command/command-list.svelte rename to src/lib/components/ui/command/command-list.svelte diff --git a/lib/components/ui/command/command-loading.svelte b/src/lib/components/ui/command/command-loading.svelte similarity index 100% rename from lib/components/ui/command/command-loading.svelte rename to src/lib/components/ui/command/command-loading.svelte diff --git a/lib/components/ui/command/command-separator.svelte b/src/lib/components/ui/command/command-separator.svelte similarity index 100% rename from lib/components/ui/command/command-separator.svelte rename to src/lib/components/ui/command/command-separator.svelte diff --git a/lib/components/ui/command/command-shortcut.svelte b/src/lib/components/ui/command/command-shortcut.svelte similarity index 100% rename from lib/components/ui/command/command-shortcut.svelte rename to src/lib/components/ui/command/command-shortcut.svelte diff --git a/lib/components/ui/command/command.svelte b/src/lib/components/ui/command/command.svelte similarity index 100% rename from lib/components/ui/command/command.svelte rename to src/lib/components/ui/command/command.svelte diff --git a/lib/components/ui/command/index.ts b/src/lib/components/ui/command/index.ts similarity index 100% rename from lib/components/ui/command/index.ts rename to src/lib/components/ui/command/index.ts diff --git a/lib/components/ui/date-picker/date-picker.svelte b/src/lib/components/ui/date-picker/date-picker.svelte similarity index 100% rename from lib/components/ui/date-picker/date-picker.svelte rename to src/lib/components/ui/date-picker/date-picker.svelte diff --git a/lib/components/ui/date-picker/index.ts b/src/lib/components/ui/date-picker/index.ts similarity index 100% rename from lib/components/ui/date-picker/index.ts rename to src/lib/components/ui/date-picker/index.ts diff --git a/lib/components/ui/dialog/dialog-close.svelte b/src/lib/components/ui/dialog/dialog-close.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-close.svelte rename to src/lib/components/ui/dialog/dialog-close.svelte diff --git a/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-content.svelte rename to src/lib/components/ui/dialog/dialog-content.svelte diff --git a/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-description.svelte rename to src/lib/components/ui/dialog/dialog-description.svelte diff --git a/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-footer.svelte rename to src/lib/components/ui/dialog/dialog-footer.svelte diff --git a/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-header.svelte rename to src/lib/components/ui/dialog/dialog-header.svelte diff --git a/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-overlay.svelte rename to src/lib/components/ui/dialog/dialog-overlay.svelte diff --git a/lib/components/ui/dialog/dialog-portal.svelte b/src/lib/components/ui/dialog/dialog-portal.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-portal.svelte rename to src/lib/components/ui/dialog/dialog-portal.svelte diff --git a/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-title.svelte rename to src/lib/components/ui/dialog/dialog-title.svelte diff --git a/lib/components/ui/dialog/dialog-trigger.svelte b/src/lib/components/ui/dialog/dialog-trigger.svelte similarity index 100% rename from lib/components/ui/dialog/dialog-trigger.svelte rename to src/lib/components/ui/dialog/dialog-trigger.svelte diff --git a/lib/components/ui/dialog/dialog.svelte b/src/lib/components/ui/dialog/dialog.svelte similarity index 100% rename from lib/components/ui/dialog/dialog.svelte rename to src/lib/components/ui/dialog/dialog.svelte diff --git a/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts similarity index 100% rename from lib/components/ui/dialog/index.ts rename to src/lib/components/ui/dialog/index.ts diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-content.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-group.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-item.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-label.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte diff --git a/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte similarity index 100% rename from lib/components/ui/dropdown-menu/dropdown-menu.svelte rename to src/lib/components/ui/dropdown-menu/dropdown-menu.svelte diff --git a/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts similarity index 100% rename from lib/components/ui/dropdown-menu/index.ts rename to src/lib/components/ui/dropdown-menu/index.ts diff --git a/lib/components/ui/empty-state/empty-state.svelte b/src/lib/components/ui/empty-state/empty-state.svelte similarity index 100% rename from lib/components/ui/empty-state/empty-state.svelte rename to src/lib/components/ui/empty-state/empty-state.svelte diff --git a/lib/components/ui/empty-state/index.ts b/src/lib/components/ui/empty-state/index.ts similarity index 100% rename from lib/components/ui/empty-state/index.ts rename to src/lib/components/ui/empty-state/index.ts diff --git a/lib/components/ui/empty-state/no-environment.svelte b/src/lib/components/ui/empty-state/no-environment.svelte similarity index 100% rename from lib/components/ui/empty-state/no-environment.svelte rename to src/lib/components/ui/empty-state/no-environment.svelte diff --git a/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts similarity index 100% rename from lib/components/ui/input/index.ts rename to src/lib/components/ui/input/index.ts diff --git a/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte similarity index 100% rename from lib/components/ui/input/input.svelte rename to src/lib/components/ui/input/input.svelte diff --git a/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts similarity index 100% rename from lib/components/ui/label/index.ts rename to src/lib/components/ui/label/index.ts diff --git a/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte similarity index 100% rename from lib/components/ui/label/label.svelte rename to src/lib/components/ui/label/label.svelte diff --git a/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts similarity index 100% rename from lib/components/ui/popover/index.ts rename to src/lib/components/ui/popover/index.ts diff --git a/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte similarity index 100% rename from lib/components/ui/popover/popover-content.svelte rename to src/lib/components/ui/popover/popover-content.svelte diff --git a/lib/components/ui/popover/popover-trigger.svelte b/src/lib/components/ui/popover/popover-trigger.svelte similarity index 100% rename from lib/components/ui/popover/popover-trigger.svelte rename to src/lib/components/ui/popover/popover-trigger.svelte diff --git a/lib/components/ui/progress/index.ts b/src/lib/components/ui/progress/index.ts similarity index 100% rename from lib/components/ui/progress/index.ts rename to src/lib/components/ui/progress/index.ts diff --git a/lib/components/ui/progress/progress.svelte b/src/lib/components/ui/progress/progress.svelte similarity index 100% rename from lib/components/ui/progress/progress.svelte rename to src/lib/components/ui/progress/progress.svelte diff --git a/lib/components/ui/select/index.ts b/src/lib/components/ui/select/index.ts similarity index 100% rename from lib/components/ui/select/index.ts rename to src/lib/components/ui/select/index.ts diff --git a/lib/components/ui/select/select-content.svelte b/src/lib/components/ui/select/select-content.svelte similarity index 100% rename from lib/components/ui/select/select-content.svelte rename to src/lib/components/ui/select/select-content.svelte diff --git a/lib/components/ui/select/select-group-heading.svelte b/src/lib/components/ui/select/select-group-heading.svelte similarity index 100% rename from lib/components/ui/select/select-group-heading.svelte rename to src/lib/components/ui/select/select-group-heading.svelte diff --git a/lib/components/ui/select/select-group.svelte b/src/lib/components/ui/select/select-group.svelte similarity index 100% rename from lib/components/ui/select/select-group.svelte rename to src/lib/components/ui/select/select-group.svelte diff --git a/lib/components/ui/select/select-item.svelte b/src/lib/components/ui/select/select-item.svelte similarity index 100% rename from lib/components/ui/select/select-item.svelte rename to src/lib/components/ui/select/select-item.svelte diff --git a/lib/components/ui/select/select-label.svelte b/src/lib/components/ui/select/select-label.svelte similarity index 100% rename from lib/components/ui/select/select-label.svelte rename to src/lib/components/ui/select/select-label.svelte diff --git a/lib/components/ui/select/select-scroll-down-button.svelte b/src/lib/components/ui/select/select-scroll-down-button.svelte similarity index 100% rename from lib/components/ui/select/select-scroll-down-button.svelte rename to src/lib/components/ui/select/select-scroll-down-button.svelte diff --git a/lib/components/ui/select/select-scroll-up-button.svelte b/src/lib/components/ui/select/select-scroll-up-button.svelte similarity index 100% rename from lib/components/ui/select/select-scroll-up-button.svelte rename to src/lib/components/ui/select/select-scroll-up-button.svelte diff --git a/lib/components/ui/select/select-separator.svelte b/src/lib/components/ui/select/select-separator.svelte similarity index 100% rename from lib/components/ui/select/select-separator.svelte rename to src/lib/components/ui/select/select-separator.svelte diff --git a/lib/components/ui/select/select-trigger.svelte b/src/lib/components/ui/select/select-trigger.svelte similarity index 100% rename from lib/components/ui/select/select-trigger.svelte rename to src/lib/components/ui/select/select-trigger.svelte diff --git a/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts similarity index 100% rename from lib/components/ui/separator/index.ts rename to src/lib/components/ui/separator/index.ts diff --git a/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte similarity index 100% rename from lib/components/ui/separator/separator.svelte rename to src/lib/components/ui/separator/separator.svelte diff --git a/lib/components/ui/sheet/index.ts b/src/lib/components/ui/sheet/index.ts similarity index 100% rename from lib/components/ui/sheet/index.ts rename to src/lib/components/ui/sheet/index.ts diff --git a/lib/components/ui/sheet/sheet-close.svelte b/src/lib/components/ui/sheet/sheet-close.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-close.svelte rename to src/lib/components/ui/sheet/sheet-close.svelte diff --git a/lib/components/ui/sheet/sheet-content.svelte b/src/lib/components/ui/sheet/sheet-content.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-content.svelte rename to src/lib/components/ui/sheet/sheet-content.svelte diff --git a/lib/components/ui/sheet/sheet-description.svelte b/src/lib/components/ui/sheet/sheet-description.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-description.svelte rename to src/lib/components/ui/sheet/sheet-description.svelte diff --git a/lib/components/ui/sheet/sheet-footer.svelte b/src/lib/components/ui/sheet/sheet-footer.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-footer.svelte rename to src/lib/components/ui/sheet/sheet-footer.svelte diff --git a/lib/components/ui/sheet/sheet-header.svelte b/src/lib/components/ui/sheet/sheet-header.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-header.svelte rename to src/lib/components/ui/sheet/sheet-header.svelte diff --git a/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-overlay.svelte rename to src/lib/components/ui/sheet/sheet-overlay.svelte diff --git a/lib/components/ui/sheet/sheet-title.svelte b/src/lib/components/ui/sheet/sheet-title.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-title.svelte rename to src/lib/components/ui/sheet/sheet-title.svelte diff --git a/lib/components/ui/sheet/sheet-trigger.svelte b/src/lib/components/ui/sheet/sheet-trigger.svelte similarity index 100% rename from lib/components/ui/sheet/sheet-trigger.svelte rename to src/lib/components/ui/sheet/sheet-trigger.svelte diff --git a/lib/components/ui/sidebar/constants.ts b/src/lib/components/ui/sidebar/constants.ts similarity index 100% rename from lib/components/ui/sidebar/constants.ts rename to src/lib/components/ui/sidebar/constants.ts diff --git a/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts similarity index 100% rename from lib/components/ui/sidebar/context.svelte.ts rename to src/lib/components/ui/sidebar/context.svelte.ts diff --git a/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts similarity index 100% rename from lib/components/ui/sidebar/index.ts rename to src/lib/components/ui/sidebar/index.ts diff --git a/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-content.svelte rename to src/lib/components/ui/sidebar/sidebar-content.svelte diff --git a/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-footer.svelte rename to src/lib/components/ui/sidebar/sidebar-footer.svelte diff --git a/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-group-action.svelte rename to src/lib/components/ui/sidebar/sidebar-group-action.svelte diff --git a/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-group-content.svelte rename to src/lib/components/ui/sidebar/sidebar-group-content.svelte diff --git a/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-group-label.svelte rename to src/lib/components/ui/sidebar/sidebar-group-label.svelte diff --git a/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-group.svelte rename to src/lib/components/ui/sidebar/sidebar-group.svelte diff --git a/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-header.svelte rename to src/lib/components/ui/sidebar/sidebar-header.svelte diff --git a/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-input.svelte rename to src/lib/components/ui/sidebar/sidebar-input.svelte diff --git a/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-inset.svelte rename to src/lib/components/ui/sidebar/sidebar-inset.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-action.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-action.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-badge.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-badge.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-button.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-button.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-item.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-item.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-skeleton.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-sub-button.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-sub-item.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu-sub.svelte rename to src/lib/components/ui/sidebar/sidebar-menu-sub.svelte diff --git a/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-menu.svelte rename to src/lib/components/ui/sidebar/sidebar-menu.svelte diff --git a/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-provider.svelte rename to src/lib/components/ui/sidebar/sidebar-provider.svelte diff --git a/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-rail.svelte rename to src/lib/components/ui/sidebar/sidebar-rail.svelte diff --git a/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-separator.svelte rename to src/lib/components/ui/sidebar/sidebar-separator.svelte diff --git a/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar-trigger.svelte rename to src/lib/components/ui/sidebar/sidebar-trigger.svelte diff --git a/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte similarity index 100% rename from lib/components/ui/sidebar/sidebar.svelte rename to src/lib/components/ui/sidebar/sidebar.svelte diff --git a/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts similarity index 100% rename from lib/components/ui/skeleton/index.ts rename to src/lib/components/ui/skeleton/index.ts diff --git a/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte similarity index 100% rename from lib/components/ui/skeleton/skeleton.svelte rename to src/lib/components/ui/skeleton/skeleton.svelte diff --git a/lib/components/ui/sonner/index.ts b/src/lib/components/ui/sonner/index.ts similarity index 100% rename from lib/components/ui/sonner/index.ts rename to src/lib/components/ui/sonner/index.ts diff --git a/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte similarity index 100% rename from lib/components/ui/sonner/sonner.svelte rename to src/lib/components/ui/sonner/sonner.svelte diff --git a/lib/components/ui/switch/index.ts b/src/lib/components/ui/switch/index.ts similarity index 100% rename from lib/components/ui/switch/index.ts rename to src/lib/components/ui/switch/index.ts diff --git a/lib/components/ui/switch/switch.svelte b/src/lib/components/ui/switch/switch.svelte similarity index 100% rename from lib/components/ui/switch/switch.svelte rename to src/lib/components/ui/switch/switch.svelte diff --git a/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts similarity index 100% rename from lib/components/ui/table/index.ts rename to src/lib/components/ui/table/index.ts diff --git a/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte similarity index 100% rename from lib/components/ui/table/table-body.svelte rename to src/lib/components/ui/table/table-body.svelte diff --git a/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte similarity index 100% rename from lib/components/ui/table/table-caption.svelte rename to src/lib/components/ui/table/table-caption.svelte diff --git a/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte similarity index 100% rename from lib/components/ui/table/table-cell.svelte rename to src/lib/components/ui/table/table-cell.svelte diff --git a/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte similarity index 100% rename from lib/components/ui/table/table-footer.svelte rename to src/lib/components/ui/table/table-footer.svelte diff --git a/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte similarity index 100% rename from lib/components/ui/table/table-head.svelte rename to src/lib/components/ui/table/table-head.svelte diff --git a/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte similarity index 100% rename from lib/components/ui/table/table-header.svelte rename to src/lib/components/ui/table/table-header.svelte diff --git a/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte similarity index 100% rename from lib/components/ui/table/table-row.svelte rename to src/lib/components/ui/table/table-row.svelte diff --git a/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte similarity index 100% rename from lib/components/ui/table/table.svelte rename to src/lib/components/ui/table/table.svelte diff --git a/lib/components/ui/tabs/index.ts b/src/lib/components/ui/tabs/index.ts similarity index 100% rename from lib/components/ui/tabs/index.ts rename to src/lib/components/ui/tabs/index.ts diff --git a/lib/components/ui/tabs/tabs-content.svelte b/src/lib/components/ui/tabs/tabs-content.svelte similarity index 100% rename from lib/components/ui/tabs/tabs-content.svelte rename to src/lib/components/ui/tabs/tabs-content.svelte diff --git a/lib/components/ui/tabs/tabs-list.svelte b/src/lib/components/ui/tabs/tabs-list.svelte similarity index 100% rename from lib/components/ui/tabs/tabs-list.svelte rename to src/lib/components/ui/tabs/tabs-list.svelte diff --git a/lib/components/ui/tabs/tabs-trigger.svelte b/src/lib/components/ui/tabs/tabs-trigger.svelte similarity index 100% rename from lib/components/ui/tabs/tabs-trigger.svelte rename to src/lib/components/ui/tabs/tabs-trigger.svelte diff --git a/lib/components/ui/tabs/tabs.svelte b/src/lib/components/ui/tabs/tabs.svelte similarity index 100% rename from lib/components/ui/tabs/tabs.svelte rename to src/lib/components/ui/tabs/tabs.svelte diff --git a/lib/components/ui/textarea/index.ts b/src/lib/components/ui/textarea/index.ts similarity index 100% rename from lib/components/ui/textarea/index.ts rename to src/lib/components/ui/textarea/index.ts diff --git a/lib/components/ui/textarea/textarea.svelte b/src/lib/components/ui/textarea/textarea.svelte similarity index 100% rename from lib/components/ui/textarea/textarea.svelte rename to src/lib/components/ui/textarea/textarea.svelte diff --git a/lib/components/ui/toggle-pill/index.ts b/src/lib/components/ui/toggle-pill/index.ts similarity index 100% rename from lib/components/ui/toggle-pill/index.ts rename to src/lib/components/ui/toggle-pill/index.ts diff --git a/lib/components/ui/toggle-pill/toggle-group.svelte b/src/lib/components/ui/toggle-pill/toggle-group.svelte similarity index 100% rename from lib/components/ui/toggle-pill/toggle-group.svelte rename to src/lib/components/ui/toggle-pill/toggle-group.svelte diff --git a/lib/components/ui/toggle-pill/toggle-pill.svelte b/src/lib/components/ui/toggle-pill/toggle-pill.svelte similarity index 100% rename from lib/components/ui/toggle-pill/toggle-pill.svelte rename to src/lib/components/ui/toggle-pill/toggle-pill.svelte diff --git a/lib/components/ui/toggle-pill/toggle-switch.svelte b/src/lib/components/ui/toggle-pill/toggle-switch.svelte similarity index 100% rename from lib/components/ui/toggle-pill/toggle-switch.svelte rename to src/lib/components/ui/toggle-pill/toggle-switch.svelte diff --git a/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts similarity index 100% rename from lib/components/ui/tooltip/index.ts rename to src/lib/components/ui/tooltip/index.ts diff --git a/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte similarity index 100% rename from lib/components/ui/tooltip/tooltip-content.svelte rename to src/lib/components/ui/tooltip/tooltip-content.svelte diff --git a/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte similarity index 100% rename from lib/components/ui/tooltip/tooltip-trigger.svelte rename to src/lib/components/ui/tooltip/tooltip-trigger.svelte diff --git a/lib/config/grid-columns.ts b/src/lib/config/grid-columns.ts similarity index 100% rename from lib/config/grid-columns.ts rename to src/lib/config/grid-columns.ts diff --git a/lib/data/changelog.json b/src/lib/data/changelog.json similarity index 100% rename from lib/data/changelog.json rename to src/lib/data/changelog.json diff --git a/lib/data/dependencies.json b/src/lib/data/dependencies.json similarity index 100% rename from lib/data/dependencies.json rename to src/lib/data/dependencies.json diff --git a/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts similarity index 100% rename from lib/hooks/is-mobile.svelte.ts rename to src/lib/hooks/is-mobile.svelte.ts diff --git a/lib/index.ts b/src/lib/index.ts similarity index 100% rename from lib/index.ts rename to src/lib/index.ts diff --git a/lib/server/audit-events.ts b/src/lib/server/audit-events.ts similarity index 100% rename from lib/server/audit-events.ts rename to src/lib/server/audit-events.ts diff --git a/lib/server/audit.ts b/src/lib/server/audit.ts similarity index 100% rename from lib/server/audit.ts rename to src/lib/server/audit.ts diff --git a/lib/server/auth.ts b/src/lib/server/auth.ts similarity index 100% rename from lib/server/auth.ts rename to src/lib/server/auth.ts diff --git a/lib/server/authorize.ts b/src/lib/server/authorize.ts similarity index 100% rename from lib/server/authorize.ts rename to src/lib/server/authorize.ts diff --git a/lib/server/db.ts b/src/lib/server/db.ts similarity index 100% rename from lib/server/db.ts rename to src/lib/server/db.ts diff --git a/lib/server/db/connection.ts b/src/lib/server/db/connection.ts similarity index 100% rename from lib/server/db/connection.ts rename to src/lib/server/db/connection.ts diff --git a/lib/server/db/drizzle.ts b/src/lib/server/db/drizzle.ts similarity index 100% rename from lib/server/db/drizzle.ts rename to src/lib/server/db/drizzle.ts diff --git a/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts similarity index 100% rename from lib/server/db/schema/index.ts rename to src/lib/server/db/schema/index.ts diff --git a/lib/server/db/schema/pg-schema.ts b/src/lib/server/db/schema/pg-schema.ts similarity index 100% rename from lib/server/db/schema/pg-schema.ts rename to src/lib/server/db/schema/pg-schema.ts diff --git a/lib/server/docker.ts b/src/lib/server/docker.ts similarity index 100% rename from lib/server/docker.ts rename to src/lib/server/docker.ts diff --git a/lib/server/event-collector.ts b/src/lib/server/event-collector.ts similarity index 100% rename from lib/server/event-collector.ts rename to src/lib/server/event-collector.ts diff --git a/lib/server/git.ts b/src/lib/server/git.ts similarity index 100% rename from lib/server/git.ts rename to src/lib/server/git.ts diff --git a/lib/server/hawser.ts b/src/lib/server/hawser.ts similarity index 100% rename from lib/server/hawser.ts rename to src/lib/server/hawser.ts diff --git a/lib/server/license.ts b/src/lib/server/license.ts similarity index 100% rename from lib/server/license.ts rename to src/lib/server/license.ts diff --git a/lib/server/metrics-collector.ts b/src/lib/server/metrics-collector.ts similarity index 100% rename from lib/server/metrics-collector.ts rename to src/lib/server/metrics-collector.ts diff --git a/lib/server/notifications.ts b/src/lib/server/notifications.ts similarity index 100% rename from lib/server/notifications.ts rename to src/lib/server/notifications.ts diff --git a/lib/server/scanner.ts b/src/lib/server/scanner.ts similarity index 100% rename from lib/server/scanner.ts rename to src/lib/server/scanner.ts diff --git a/lib/server/scheduler/index.ts b/src/lib/server/scheduler/index.ts similarity index 100% rename from lib/server/scheduler/index.ts rename to src/lib/server/scheduler/index.ts diff --git a/lib/server/scheduler/tasks/container-update.ts b/src/lib/server/scheduler/tasks/container-update.ts similarity index 100% rename from lib/server/scheduler/tasks/container-update.ts rename to src/lib/server/scheduler/tasks/container-update.ts diff --git a/lib/server/scheduler/tasks/env-update-check.ts b/src/lib/server/scheduler/tasks/env-update-check.ts similarity index 100% rename from lib/server/scheduler/tasks/env-update-check.ts rename to src/lib/server/scheduler/tasks/env-update-check.ts diff --git a/lib/server/scheduler/tasks/git-stack-sync.ts b/src/lib/server/scheduler/tasks/git-stack-sync.ts similarity index 100% rename from lib/server/scheduler/tasks/git-stack-sync.ts rename to src/lib/server/scheduler/tasks/git-stack-sync.ts diff --git a/lib/server/scheduler/tasks/system-cleanup.ts b/src/lib/server/scheduler/tasks/system-cleanup.ts similarity index 100% rename from lib/server/scheduler/tasks/system-cleanup.ts rename to src/lib/server/scheduler/tasks/system-cleanup.ts diff --git a/lib/server/scheduler/tasks/update-utils.ts b/src/lib/server/scheduler/tasks/update-utils.ts similarity index 100% rename from lib/server/scheduler/tasks/update-utils.ts rename to src/lib/server/scheduler/tasks/update-utils.ts diff --git a/lib/server/stacks.ts b/src/lib/server/stacks.ts similarity index 100% rename from lib/server/stacks.ts rename to src/lib/server/stacks.ts diff --git a/lib/server/subprocess-manager.ts b/src/lib/server/subprocess-manager.ts similarity index 100% rename from lib/server/subprocess-manager.ts rename to src/lib/server/subprocess-manager.ts diff --git a/lib/server/subprocesses/event-subprocess.ts b/src/lib/server/subprocesses/event-subprocess.ts similarity index 100% rename from lib/server/subprocesses/event-subprocess.ts rename to src/lib/server/subprocesses/event-subprocess.ts diff --git a/lib/server/subprocesses/metrics-subprocess.ts b/src/lib/server/subprocesses/metrics-subprocess.ts similarity index 100% rename from lib/server/subprocesses/metrics-subprocess.ts rename to src/lib/server/subprocesses/metrics-subprocess.ts diff --git a/lib/server/uptime.ts b/src/lib/server/uptime.ts similarity index 100% rename from lib/server/uptime.ts rename to src/lib/server/uptime.ts diff --git a/lib/stores/audit-events.ts b/src/lib/stores/audit-events.ts similarity index 100% rename from lib/stores/audit-events.ts rename to src/lib/stores/audit-events.ts diff --git a/lib/stores/auth.ts b/src/lib/stores/auth.ts similarity index 100% rename from lib/stores/auth.ts rename to src/lib/stores/auth.ts diff --git a/lib/stores/dashboard.ts b/src/lib/stores/dashboard.ts similarity index 100% rename from lib/stores/dashboard.ts rename to src/lib/stores/dashboard.ts diff --git a/lib/stores/environment.ts b/src/lib/stores/environment.ts similarity index 100% rename from lib/stores/environment.ts rename to src/lib/stores/environment.ts diff --git a/lib/stores/events.ts b/src/lib/stores/events.ts similarity index 100% rename from lib/stores/events.ts rename to src/lib/stores/events.ts diff --git a/lib/stores/grid-preferences.ts b/src/lib/stores/grid-preferences.ts similarity index 100% rename from lib/stores/grid-preferences.ts rename to src/lib/stores/grid-preferences.ts diff --git a/lib/stores/license.ts b/src/lib/stores/license.ts similarity index 100% rename from lib/stores/license.ts rename to src/lib/stores/license.ts diff --git a/lib/stores/settings.ts b/src/lib/stores/settings.ts similarity index 100% rename from lib/stores/settings.ts rename to src/lib/stores/settings.ts diff --git a/lib/stores/stats.ts b/src/lib/stores/stats.ts similarity index 100% rename from lib/stores/stats.ts rename to src/lib/stores/stats.ts diff --git a/lib/stores/theme.ts b/src/lib/stores/theme.ts similarity index 100% rename from lib/stores/theme.ts rename to src/lib/stores/theme.ts diff --git a/lib/themes.ts b/src/lib/themes.ts similarity index 100% rename from lib/themes.ts rename to src/lib/themes.ts diff --git a/lib/types.ts b/src/lib/types.ts similarity index 100% rename from lib/types.ts rename to src/lib/types.ts diff --git a/lib/utils.ts b/src/lib/utils.ts similarity index 100% rename from lib/utils.ts rename to src/lib/utils.ts diff --git a/lib/utils/icons.ts b/src/lib/utils/icons.ts similarity index 100% rename from lib/utils/icons.ts rename to src/lib/utils/icons.ts diff --git a/lib/utils/ip.ts b/src/lib/utils/ip.ts similarity index 100% rename from lib/utils/ip.ts rename to src/lib/utils/ip.ts diff --git a/lib/utils/label-colors.ts b/src/lib/utils/label-colors.ts similarity index 100% rename from lib/utils/label-colors.ts rename to src/lib/utils/label-colors.ts diff --git a/lib/utils/update-steps.ts b/src/lib/utils/update-steps.ts similarity index 100% rename from lib/utils/update-steps.ts rename to src/lib/utils/update-steps.ts diff --git a/lib/utils/version.ts b/src/lib/utils/version.ts similarity index 100% rename from lib/utils/version.ts rename to src/lib/utils/version.ts diff --git a/routes/+layout.server.ts b/src/routes/+layout.server.ts similarity index 100% rename from routes/+layout.server.ts rename to src/routes/+layout.server.ts diff --git a/routes/+layout.svelte b/src/routes/+layout.svelte similarity index 100% rename from routes/+layout.svelte rename to src/routes/+layout.svelte diff --git a/routes/+layout.ts b/src/routes/+layout.ts similarity index 100% rename from routes/+layout.ts rename to src/routes/+layout.ts diff --git a/routes/+page.svelte b/src/routes/+page.svelte similarity index 100% rename from routes/+page.svelte rename to src/routes/+page.svelte diff --git a/routes/activity/+page.svelte b/src/routes/activity/+page.svelte similarity index 100% rename from routes/activity/+page.svelte rename to src/routes/activity/+page.svelte diff --git a/routes/alerts/+page.svelte b/src/routes/alerts/+page.svelte similarity index 100% rename from routes/alerts/+page.svelte rename to src/routes/alerts/+page.svelte diff --git a/routes/api/activity/+server.ts b/src/routes/api/activity/+server.ts similarity index 100% rename from routes/api/activity/+server.ts rename to src/routes/api/activity/+server.ts diff --git a/routes/api/activity/containers/+server.ts b/src/routes/api/activity/containers/+server.ts similarity index 100% rename from routes/api/activity/containers/+server.ts rename to src/routes/api/activity/containers/+server.ts diff --git a/routes/api/activity/events/+server.ts b/src/routes/api/activity/events/+server.ts similarity index 100% rename from routes/api/activity/events/+server.ts rename to src/routes/api/activity/events/+server.ts diff --git a/routes/api/activity/stats/+server.ts b/src/routes/api/activity/stats/+server.ts similarity index 100% rename from routes/api/activity/stats/+server.ts rename to src/routes/api/activity/stats/+server.ts diff --git a/routes/api/audit/+server.ts b/src/routes/api/audit/+server.ts similarity index 100% rename from routes/api/audit/+server.ts rename to src/routes/api/audit/+server.ts diff --git a/routes/api/audit/events/+server.ts b/src/routes/api/audit/events/+server.ts similarity index 100% rename from routes/api/audit/events/+server.ts rename to src/routes/api/audit/events/+server.ts diff --git a/routes/api/audit/export/+server.ts b/src/routes/api/audit/export/+server.ts similarity index 100% rename from routes/api/audit/export/+server.ts rename to src/routes/api/audit/export/+server.ts diff --git a/routes/api/audit/users/+server.ts b/src/routes/api/audit/users/+server.ts similarity index 100% rename from routes/api/audit/users/+server.ts rename to src/routes/api/audit/users/+server.ts diff --git a/routes/api/auth/ldap/+server.ts b/src/routes/api/auth/ldap/+server.ts similarity index 100% rename from routes/api/auth/ldap/+server.ts rename to src/routes/api/auth/ldap/+server.ts diff --git a/routes/api/auth/ldap/[id]/+server.ts b/src/routes/api/auth/ldap/[id]/+server.ts similarity index 100% rename from routes/api/auth/ldap/[id]/+server.ts rename to src/routes/api/auth/ldap/[id]/+server.ts diff --git a/routes/api/auth/ldap/[id]/test/+server.ts b/src/routes/api/auth/ldap/[id]/test/+server.ts similarity index 100% rename from routes/api/auth/ldap/[id]/test/+server.ts rename to src/routes/api/auth/ldap/[id]/test/+server.ts diff --git a/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts similarity index 100% rename from routes/api/auth/login/+server.ts rename to src/routes/api/auth/login/+server.ts diff --git a/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts similarity index 100% rename from routes/api/auth/logout/+server.ts rename to src/routes/api/auth/logout/+server.ts diff --git a/routes/api/auth/oidc/+server.ts b/src/routes/api/auth/oidc/+server.ts similarity index 100% rename from routes/api/auth/oidc/+server.ts rename to src/routes/api/auth/oidc/+server.ts diff --git a/routes/api/auth/oidc/[id]/+server.ts b/src/routes/api/auth/oidc/[id]/+server.ts similarity index 100% rename from routes/api/auth/oidc/[id]/+server.ts rename to src/routes/api/auth/oidc/[id]/+server.ts diff --git a/routes/api/auth/oidc/[id]/initiate/+server.ts b/src/routes/api/auth/oidc/[id]/initiate/+server.ts similarity index 100% rename from routes/api/auth/oidc/[id]/initiate/+server.ts rename to src/routes/api/auth/oidc/[id]/initiate/+server.ts diff --git a/routes/api/auth/oidc/[id]/test/+server.ts b/src/routes/api/auth/oidc/[id]/test/+server.ts similarity index 100% rename from routes/api/auth/oidc/[id]/test/+server.ts rename to src/routes/api/auth/oidc/[id]/test/+server.ts diff --git a/routes/api/auth/oidc/callback/+server.ts b/src/routes/api/auth/oidc/callback/+server.ts similarity index 100% rename from routes/api/auth/oidc/callback/+server.ts rename to src/routes/api/auth/oidc/callback/+server.ts diff --git a/routes/api/auth/providers/+server.ts b/src/routes/api/auth/providers/+server.ts similarity index 100% rename from routes/api/auth/providers/+server.ts rename to src/routes/api/auth/providers/+server.ts diff --git a/routes/api/auth/session/+server.ts b/src/routes/api/auth/session/+server.ts similarity index 100% rename from routes/api/auth/session/+server.ts rename to src/routes/api/auth/session/+server.ts diff --git a/routes/api/auth/settings/+server.ts b/src/routes/api/auth/settings/+server.ts similarity index 100% rename from routes/api/auth/settings/+server.ts rename to src/routes/api/auth/settings/+server.ts diff --git a/routes/api/auto-update/+server.ts b/src/routes/api/auto-update/+server.ts similarity index 100% rename from routes/api/auto-update/+server.ts rename to src/routes/api/auto-update/+server.ts diff --git a/routes/api/auto-update/[containerName]/+server.ts b/src/routes/api/auto-update/[containerName]/+server.ts similarity index 100% rename from routes/api/auto-update/[containerName]/+server.ts rename to src/routes/api/auto-update/[containerName]/+server.ts diff --git a/routes/api/batch/+server.ts b/src/routes/api/batch/+server.ts similarity index 100% rename from routes/api/batch/+server.ts rename to src/routes/api/batch/+server.ts diff --git a/routes/api/changelog/+server.ts b/src/routes/api/changelog/+server.ts similarity index 100% rename from routes/api/changelog/+server.ts rename to src/routes/api/changelog/+server.ts diff --git a/routes/api/config-sets/+server.ts b/src/routes/api/config-sets/+server.ts similarity index 100% rename from routes/api/config-sets/+server.ts rename to src/routes/api/config-sets/+server.ts diff --git a/routes/api/config-sets/[id]/+server.ts b/src/routes/api/config-sets/[id]/+server.ts similarity index 100% rename from routes/api/config-sets/[id]/+server.ts rename to src/routes/api/config-sets/[id]/+server.ts diff --git a/routes/api/containers/+server.ts b/src/routes/api/containers/+server.ts similarity index 100% rename from routes/api/containers/+server.ts rename to src/routes/api/containers/+server.ts diff --git a/routes/api/containers/[id]/+server.ts b/src/routes/api/containers/[id]/+server.ts similarity index 100% rename from routes/api/containers/[id]/+server.ts rename to src/routes/api/containers/[id]/+server.ts diff --git a/routes/api/containers/[id]/exec/+server.ts b/src/routes/api/containers/[id]/exec/+server.ts similarity index 100% rename from routes/api/containers/[id]/exec/+server.ts rename to src/routes/api/containers/[id]/exec/+server.ts diff --git a/routes/api/containers/[id]/files/+server.ts b/src/routes/api/containers/[id]/files/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/+server.ts rename to src/routes/api/containers/[id]/files/+server.ts diff --git a/routes/api/containers/[id]/files/chmod/+server.ts b/src/routes/api/containers/[id]/files/chmod/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/chmod/+server.ts rename to src/routes/api/containers/[id]/files/chmod/+server.ts diff --git a/routes/api/containers/[id]/files/content/+server.ts b/src/routes/api/containers/[id]/files/content/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/content/+server.ts rename to src/routes/api/containers/[id]/files/content/+server.ts diff --git a/routes/api/containers/[id]/files/create/+server.ts b/src/routes/api/containers/[id]/files/create/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/create/+server.ts rename to src/routes/api/containers/[id]/files/create/+server.ts diff --git a/routes/api/containers/[id]/files/delete/+server.ts b/src/routes/api/containers/[id]/files/delete/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/delete/+server.ts rename to src/routes/api/containers/[id]/files/delete/+server.ts diff --git a/routes/api/containers/[id]/files/download/+server.ts b/src/routes/api/containers/[id]/files/download/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/download/+server.ts rename to src/routes/api/containers/[id]/files/download/+server.ts diff --git a/routes/api/containers/[id]/files/rename/+server.ts b/src/routes/api/containers/[id]/files/rename/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/rename/+server.ts rename to src/routes/api/containers/[id]/files/rename/+server.ts diff --git a/routes/api/containers/[id]/files/upload/+server.ts b/src/routes/api/containers/[id]/files/upload/+server.ts similarity index 100% rename from routes/api/containers/[id]/files/upload/+server.ts rename to src/routes/api/containers/[id]/files/upload/+server.ts diff --git a/routes/api/containers/[id]/inspect/+server.ts b/src/routes/api/containers/[id]/inspect/+server.ts similarity index 100% rename from routes/api/containers/[id]/inspect/+server.ts rename to src/routes/api/containers/[id]/inspect/+server.ts diff --git a/routes/api/containers/[id]/logs/+server.ts b/src/routes/api/containers/[id]/logs/+server.ts similarity index 100% rename from routes/api/containers/[id]/logs/+server.ts rename to src/routes/api/containers/[id]/logs/+server.ts diff --git a/routes/api/containers/[id]/logs/stream/+server.ts b/src/routes/api/containers/[id]/logs/stream/+server.ts similarity index 100% rename from routes/api/containers/[id]/logs/stream/+server.ts rename to src/routes/api/containers/[id]/logs/stream/+server.ts diff --git a/routes/api/containers/[id]/pause/+server.ts b/src/routes/api/containers/[id]/pause/+server.ts similarity index 100% rename from routes/api/containers/[id]/pause/+server.ts rename to src/routes/api/containers/[id]/pause/+server.ts diff --git a/routes/api/containers/[id]/rename/+server.ts b/src/routes/api/containers/[id]/rename/+server.ts similarity index 100% rename from routes/api/containers/[id]/rename/+server.ts rename to src/routes/api/containers/[id]/rename/+server.ts diff --git a/routes/api/containers/[id]/restart/+server.ts b/src/routes/api/containers/[id]/restart/+server.ts similarity index 100% rename from routes/api/containers/[id]/restart/+server.ts rename to src/routes/api/containers/[id]/restart/+server.ts diff --git a/routes/api/containers/[id]/start/+server.ts b/src/routes/api/containers/[id]/start/+server.ts similarity index 100% rename from routes/api/containers/[id]/start/+server.ts rename to src/routes/api/containers/[id]/start/+server.ts diff --git a/routes/api/containers/[id]/stats/+server.ts b/src/routes/api/containers/[id]/stats/+server.ts similarity index 100% rename from routes/api/containers/[id]/stats/+server.ts rename to src/routes/api/containers/[id]/stats/+server.ts diff --git a/routes/api/containers/[id]/stop/+server.ts b/src/routes/api/containers/[id]/stop/+server.ts similarity index 100% rename from routes/api/containers/[id]/stop/+server.ts rename to src/routes/api/containers/[id]/stop/+server.ts diff --git a/routes/api/containers/[id]/top/+server.ts b/src/routes/api/containers/[id]/top/+server.ts similarity index 100% rename from routes/api/containers/[id]/top/+server.ts rename to src/routes/api/containers/[id]/top/+server.ts diff --git a/routes/api/containers/[id]/unpause/+server.ts b/src/routes/api/containers/[id]/unpause/+server.ts similarity index 100% rename from routes/api/containers/[id]/unpause/+server.ts rename to src/routes/api/containers/[id]/unpause/+server.ts diff --git a/routes/api/containers/[id]/update/+server.ts b/src/routes/api/containers/[id]/update/+server.ts similarity index 100% rename from routes/api/containers/[id]/update/+server.ts rename to src/routes/api/containers/[id]/update/+server.ts diff --git a/routes/api/containers/batch-update-stream/+server.ts b/src/routes/api/containers/batch-update-stream/+server.ts similarity index 100% rename from routes/api/containers/batch-update-stream/+server.ts rename to src/routes/api/containers/batch-update-stream/+server.ts diff --git a/routes/api/containers/batch-update/+server.ts b/src/routes/api/containers/batch-update/+server.ts similarity index 100% rename from routes/api/containers/batch-update/+server.ts rename to src/routes/api/containers/batch-update/+server.ts diff --git a/routes/api/containers/check-updates/+server.ts b/src/routes/api/containers/check-updates/+server.ts similarity index 100% rename from routes/api/containers/check-updates/+server.ts rename to src/routes/api/containers/check-updates/+server.ts diff --git a/routes/api/containers/pending-updates/+server.ts b/src/routes/api/containers/pending-updates/+server.ts similarity index 100% rename from routes/api/containers/pending-updates/+server.ts rename to src/routes/api/containers/pending-updates/+server.ts diff --git a/routes/api/containers/sizes/+server.ts b/src/routes/api/containers/sizes/+server.ts similarity index 100% rename from routes/api/containers/sizes/+server.ts rename to src/routes/api/containers/sizes/+server.ts diff --git a/routes/api/containers/stats/+server.ts b/src/routes/api/containers/stats/+server.ts similarity index 100% rename from routes/api/containers/stats/+server.ts rename to src/routes/api/containers/stats/+server.ts diff --git a/routes/api/dashboard/preferences/+server.ts b/src/routes/api/dashboard/preferences/+server.ts similarity index 100% rename from routes/api/dashboard/preferences/+server.ts rename to src/routes/api/dashboard/preferences/+server.ts diff --git a/routes/api/dashboard/stats/+server.ts b/src/routes/api/dashboard/stats/+server.ts similarity index 100% rename from routes/api/dashboard/stats/+server.ts rename to src/routes/api/dashboard/stats/+server.ts diff --git a/routes/api/dashboard/stats/stream/+server.ts b/src/routes/api/dashboard/stats/stream/+server.ts similarity index 100% rename from routes/api/dashboard/stats/stream/+server.ts rename to src/routes/api/dashboard/stats/stream/+server.ts diff --git a/routes/api/dependencies/+server.ts b/src/routes/api/dependencies/+server.ts similarity index 100% rename from routes/api/dependencies/+server.ts rename to src/routes/api/dependencies/+server.ts diff --git a/routes/api/environments/+server.ts b/src/routes/api/environments/+server.ts similarity index 100% rename from routes/api/environments/+server.ts rename to src/routes/api/environments/+server.ts diff --git a/routes/api/environments/[id]/+server.ts b/src/routes/api/environments/[id]/+server.ts similarity index 100% rename from routes/api/environments/[id]/+server.ts rename to src/routes/api/environments/[id]/+server.ts diff --git a/routes/api/environments/[id]/notifications/+server.ts b/src/routes/api/environments/[id]/notifications/+server.ts similarity index 100% rename from routes/api/environments/[id]/notifications/+server.ts rename to src/routes/api/environments/[id]/notifications/+server.ts diff --git a/routes/api/environments/[id]/notifications/[notificationId]/+server.ts b/src/routes/api/environments/[id]/notifications/[notificationId]/+server.ts similarity index 100% rename from routes/api/environments/[id]/notifications/[notificationId]/+server.ts rename to src/routes/api/environments/[id]/notifications/[notificationId]/+server.ts diff --git a/routes/api/environments/[id]/test/+server.ts b/src/routes/api/environments/[id]/test/+server.ts similarity index 100% rename from routes/api/environments/[id]/test/+server.ts rename to src/routes/api/environments/[id]/test/+server.ts diff --git a/routes/api/environments/[id]/timezone/+server.ts b/src/routes/api/environments/[id]/timezone/+server.ts similarity index 100% rename from routes/api/environments/[id]/timezone/+server.ts rename to src/routes/api/environments/[id]/timezone/+server.ts diff --git a/routes/api/environments/[id]/update-check/+server.ts b/src/routes/api/environments/[id]/update-check/+server.ts similarity index 100% rename from routes/api/environments/[id]/update-check/+server.ts rename to src/routes/api/environments/[id]/update-check/+server.ts diff --git a/routes/api/environments/detect-socket/+server.ts b/src/routes/api/environments/detect-socket/+server.ts similarity index 100% rename from routes/api/environments/detect-socket/+server.ts rename to src/routes/api/environments/detect-socket/+server.ts diff --git a/routes/api/environments/test/+server.ts b/src/routes/api/environments/test/+server.ts similarity index 100% rename from routes/api/environments/test/+server.ts rename to src/routes/api/environments/test/+server.ts diff --git a/routes/api/events/+server.ts b/src/routes/api/events/+server.ts similarity index 100% rename from routes/api/events/+server.ts rename to src/routes/api/events/+server.ts diff --git a/routes/api/git/credentials/+server.ts b/src/routes/api/git/credentials/+server.ts similarity index 100% rename from routes/api/git/credentials/+server.ts rename to src/routes/api/git/credentials/+server.ts diff --git a/routes/api/git/credentials/[id]/+server.ts b/src/routes/api/git/credentials/[id]/+server.ts similarity index 100% rename from routes/api/git/credentials/[id]/+server.ts rename to src/routes/api/git/credentials/[id]/+server.ts diff --git a/routes/api/git/repositories/+server.ts b/src/routes/api/git/repositories/+server.ts similarity index 100% rename from routes/api/git/repositories/+server.ts rename to src/routes/api/git/repositories/+server.ts diff --git a/routes/api/git/repositories/[id]/+server.ts b/src/routes/api/git/repositories/[id]/+server.ts similarity index 100% rename from routes/api/git/repositories/[id]/+server.ts rename to src/routes/api/git/repositories/[id]/+server.ts diff --git a/routes/api/git/repositories/[id]/deploy/+server.ts b/src/routes/api/git/repositories/[id]/deploy/+server.ts similarity index 100% rename from routes/api/git/repositories/[id]/deploy/+server.ts rename to src/routes/api/git/repositories/[id]/deploy/+server.ts diff --git a/routes/api/git/repositories/[id]/sync/+server.ts b/src/routes/api/git/repositories/[id]/sync/+server.ts similarity index 100% rename from routes/api/git/repositories/[id]/sync/+server.ts rename to src/routes/api/git/repositories/[id]/sync/+server.ts diff --git a/routes/api/git/repositories/[id]/test/+server.ts b/src/routes/api/git/repositories/[id]/test/+server.ts similarity index 100% rename from routes/api/git/repositories/[id]/test/+server.ts rename to src/routes/api/git/repositories/[id]/test/+server.ts diff --git a/routes/api/git/repositories/test/+server.ts b/src/routes/api/git/repositories/test/+server.ts similarity index 100% rename from routes/api/git/repositories/test/+server.ts rename to src/routes/api/git/repositories/test/+server.ts diff --git a/routes/api/git/stacks/+server.ts b/src/routes/api/git/stacks/+server.ts similarity index 100% rename from routes/api/git/stacks/+server.ts rename to src/routes/api/git/stacks/+server.ts diff --git a/routes/api/git/stacks/[id]/+server.ts b/src/routes/api/git/stacks/[id]/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/+server.ts rename to src/routes/api/git/stacks/[id]/+server.ts diff --git a/routes/api/git/stacks/[id]/deploy-stream/+server.ts b/src/routes/api/git/stacks/[id]/deploy-stream/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/deploy-stream/+server.ts rename to src/routes/api/git/stacks/[id]/deploy-stream/+server.ts diff --git a/routes/api/git/stacks/[id]/deploy/+server.ts b/src/routes/api/git/stacks/[id]/deploy/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/deploy/+server.ts rename to src/routes/api/git/stacks/[id]/deploy/+server.ts diff --git a/routes/api/git/stacks/[id]/env-files/+server.ts b/src/routes/api/git/stacks/[id]/env-files/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/env-files/+server.ts rename to src/routes/api/git/stacks/[id]/env-files/+server.ts diff --git a/routes/api/git/stacks/[id]/sync/+server.ts b/src/routes/api/git/stacks/[id]/sync/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/sync/+server.ts rename to src/routes/api/git/stacks/[id]/sync/+server.ts diff --git a/routes/api/git/stacks/[id]/test/+server.ts b/src/routes/api/git/stacks/[id]/test/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/test/+server.ts rename to src/routes/api/git/stacks/[id]/test/+server.ts diff --git a/routes/api/git/stacks/[id]/webhook/+server.ts b/src/routes/api/git/stacks/[id]/webhook/+server.ts similarity index 100% rename from routes/api/git/stacks/[id]/webhook/+server.ts rename to src/routes/api/git/stacks/[id]/webhook/+server.ts diff --git a/routes/api/git/webhook/[id]/+server.ts b/src/routes/api/git/webhook/[id]/+server.ts similarity index 100% rename from routes/api/git/webhook/[id]/+server.ts rename to src/routes/api/git/webhook/[id]/+server.ts diff --git a/routes/api/hawser/connect/+server.ts b/src/routes/api/hawser/connect/+server.ts similarity index 100% rename from routes/api/hawser/connect/+server.ts rename to src/routes/api/hawser/connect/+server.ts diff --git a/routes/api/hawser/tokens/+server.ts b/src/routes/api/hawser/tokens/+server.ts similarity index 100% rename from routes/api/hawser/tokens/+server.ts rename to src/routes/api/hawser/tokens/+server.ts diff --git a/routes/api/health/+server.ts b/src/routes/api/health/+server.ts similarity index 100% rename from routes/api/health/+server.ts rename to src/routes/api/health/+server.ts diff --git a/routes/api/health/database/+server.ts b/src/routes/api/health/database/+server.ts similarity index 100% rename from routes/api/health/database/+server.ts rename to src/routes/api/health/database/+server.ts diff --git a/routes/api/host/+server.ts b/src/routes/api/host/+server.ts similarity index 100% rename from routes/api/host/+server.ts rename to src/routes/api/host/+server.ts diff --git a/routes/api/images/+server.ts b/src/routes/api/images/+server.ts similarity index 100% rename from routes/api/images/+server.ts rename to src/routes/api/images/+server.ts diff --git a/routes/api/images/[id]/+server.ts b/src/routes/api/images/[id]/+server.ts similarity index 100% rename from routes/api/images/[id]/+server.ts rename to src/routes/api/images/[id]/+server.ts diff --git a/routes/api/images/[id]/export/+server.ts b/src/routes/api/images/[id]/export/+server.ts similarity index 100% rename from routes/api/images/[id]/export/+server.ts rename to src/routes/api/images/[id]/export/+server.ts diff --git a/routes/api/images/[id]/history/+server.ts b/src/routes/api/images/[id]/history/+server.ts similarity index 100% rename from routes/api/images/[id]/history/+server.ts rename to src/routes/api/images/[id]/history/+server.ts diff --git a/routes/api/images/[id]/tag/+server.ts b/src/routes/api/images/[id]/tag/+server.ts similarity index 100% rename from routes/api/images/[id]/tag/+server.ts rename to src/routes/api/images/[id]/tag/+server.ts diff --git a/routes/api/images/pull/+server.ts b/src/routes/api/images/pull/+server.ts similarity index 100% rename from routes/api/images/pull/+server.ts rename to src/routes/api/images/pull/+server.ts diff --git a/routes/api/images/push/+server.ts b/src/routes/api/images/push/+server.ts similarity index 100% rename from routes/api/images/push/+server.ts rename to src/routes/api/images/push/+server.ts diff --git a/routes/api/images/scan/+server.ts b/src/routes/api/images/scan/+server.ts similarity index 100% rename from routes/api/images/scan/+server.ts rename to src/routes/api/images/scan/+server.ts diff --git a/routes/api/legal/license/+server.ts b/src/routes/api/legal/license/+server.ts similarity index 100% rename from routes/api/legal/license/+server.ts rename to src/routes/api/legal/license/+server.ts diff --git a/routes/api/legal/privacy/+server.ts b/src/routes/api/legal/privacy/+server.ts similarity index 100% rename from routes/api/legal/privacy/+server.ts rename to src/routes/api/legal/privacy/+server.ts diff --git a/routes/api/license/+server.ts b/src/routes/api/license/+server.ts similarity index 100% rename from routes/api/license/+server.ts rename to src/routes/api/license/+server.ts diff --git a/routes/api/logs/merged/+server.ts b/src/routes/api/logs/merged/+server.ts similarity index 100% rename from routes/api/logs/merged/+server.ts rename to src/routes/api/logs/merged/+server.ts diff --git a/routes/api/metrics/+server.ts b/src/routes/api/metrics/+server.ts similarity index 100% rename from routes/api/metrics/+server.ts rename to src/routes/api/metrics/+server.ts diff --git a/routes/api/networks/+server.ts b/src/routes/api/networks/+server.ts similarity index 100% rename from routes/api/networks/+server.ts rename to src/routes/api/networks/+server.ts diff --git a/routes/api/networks/[id]/+server.ts b/src/routes/api/networks/[id]/+server.ts similarity index 100% rename from routes/api/networks/[id]/+server.ts rename to src/routes/api/networks/[id]/+server.ts diff --git a/routes/api/networks/[id]/connect/+server.ts b/src/routes/api/networks/[id]/connect/+server.ts similarity index 100% rename from routes/api/networks/[id]/connect/+server.ts rename to src/routes/api/networks/[id]/connect/+server.ts diff --git a/routes/api/networks/[id]/disconnect/+server.ts b/src/routes/api/networks/[id]/disconnect/+server.ts similarity index 100% rename from routes/api/networks/[id]/disconnect/+server.ts rename to src/routes/api/networks/[id]/disconnect/+server.ts diff --git a/routes/api/networks/[id]/inspect/+server.ts b/src/routes/api/networks/[id]/inspect/+server.ts similarity index 100% rename from routes/api/networks/[id]/inspect/+server.ts rename to src/routes/api/networks/[id]/inspect/+server.ts diff --git a/routes/api/notifications/+server.ts b/src/routes/api/notifications/+server.ts similarity index 100% rename from routes/api/notifications/+server.ts rename to src/routes/api/notifications/+server.ts diff --git a/routes/api/notifications/[id]/+server.ts b/src/routes/api/notifications/[id]/+server.ts similarity index 100% rename from routes/api/notifications/[id]/+server.ts rename to src/routes/api/notifications/[id]/+server.ts diff --git a/routes/api/notifications/[id]/test/+server.ts b/src/routes/api/notifications/[id]/test/+server.ts similarity index 100% rename from routes/api/notifications/[id]/test/+server.ts rename to src/routes/api/notifications/[id]/test/+server.ts diff --git a/routes/api/notifications/test/+server.ts b/src/routes/api/notifications/test/+server.ts similarity index 100% rename from routes/api/notifications/test/+server.ts rename to src/routes/api/notifications/test/+server.ts diff --git a/routes/api/notifications/trigger-test/+server.ts b/src/routes/api/notifications/trigger-test/+server.ts similarity index 100% rename from routes/api/notifications/trigger-test/+server.ts rename to src/routes/api/notifications/trigger-test/+server.ts diff --git a/routes/api/preferences/favorite-groups/+server.ts b/src/routes/api/preferences/favorite-groups/+server.ts similarity index 100% rename from routes/api/preferences/favorite-groups/+server.ts rename to src/routes/api/preferences/favorite-groups/+server.ts diff --git a/routes/api/preferences/favorites/+server.ts b/src/routes/api/preferences/favorites/+server.ts similarity index 100% rename from routes/api/preferences/favorites/+server.ts rename to src/routes/api/preferences/favorites/+server.ts diff --git a/routes/api/preferences/grid/+server.ts b/src/routes/api/preferences/grid/+server.ts similarity index 100% rename from routes/api/preferences/grid/+server.ts rename to src/routes/api/preferences/grid/+server.ts diff --git a/routes/api/profile/+server.ts b/src/routes/api/profile/+server.ts similarity index 100% rename from routes/api/profile/+server.ts rename to src/routes/api/profile/+server.ts diff --git a/routes/api/profile/avatar/+server.ts b/src/routes/api/profile/avatar/+server.ts similarity index 100% rename from routes/api/profile/avatar/+server.ts rename to src/routes/api/profile/avatar/+server.ts diff --git a/routes/api/profile/preferences/+server.ts b/src/routes/api/profile/preferences/+server.ts similarity index 100% rename from routes/api/profile/preferences/+server.ts rename to src/routes/api/profile/preferences/+server.ts diff --git a/routes/api/prune/all/+server.ts b/src/routes/api/prune/all/+server.ts similarity index 100% rename from routes/api/prune/all/+server.ts rename to src/routes/api/prune/all/+server.ts diff --git a/routes/api/prune/containers/+server.ts b/src/routes/api/prune/containers/+server.ts similarity index 100% rename from routes/api/prune/containers/+server.ts rename to src/routes/api/prune/containers/+server.ts diff --git a/routes/api/prune/images/+server.ts b/src/routes/api/prune/images/+server.ts similarity index 100% rename from routes/api/prune/images/+server.ts rename to src/routes/api/prune/images/+server.ts diff --git a/routes/api/prune/networks/+server.ts b/src/routes/api/prune/networks/+server.ts similarity index 100% rename from routes/api/prune/networks/+server.ts rename to src/routes/api/prune/networks/+server.ts diff --git a/routes/api/prune/volumes/+server.ts b/src/routes/api/prune/volumes/+server.ts similarity index 100% rename from routes/api/prune/volumes/+server.ts rename to src/routes/api/prune/volumes/+server.ts diff --git a/routes/api/registries/+server.ts b/src/routes/api/registries/+server.ts similarity index 100% rename from routes/api/registries/+server.ts rename to src/routes/api/registries/+server.ts diff --git a/routes/api/registries/[id]/+server.ts b/src/routes/api/registries/[id]/+server.ts similarity index 100% rename from routes/api/registries/[id]/+server.ts rename to src/routes/api/registries/[id]/+server.ts diff --git a/routes/api/registries/[id]/default/+server.ts b/src/routes/api/registries/[id]/default/+server.ts similarity index 100% rename from routes/api/registries/[id]/default/+server.ts rename to src/routes/api/registries/[id]/default/+server.ts diff --git a/routes/api/registry/catalog/+server.ts b/src/routes/api/registry/catalog/+server.ts similarity index 100% rename from routes/api/registry/catalog/+server.ts rename to src/routes/api/registry/catalog/+server.ts diff --git a/routes/api/registry/image/+server.ts b/src/routes/api/registry/image/+server.ts similarity index 100% rename from routes/api/registry/image/+server.ts rename to src/routes/api/registry/image/+server.ts diff --git a/routes/api/registry/search/+server.ts b/src/routes/api/registry/search/+server.ts similarity index 100% rename from routes/api/registry/search/+server.ts rename to src/routes/api/registry/search/+server.ts diff --git a/routes/api/registry/tags/+server.ts b/src/routes/api/registry/tags/+server.ts similarity index 100% rename from routes/api/registry/tags/+server.ts rename to src/routes/api/registry/tags/+server.ts diff --git a/routes/api/roles/+server.ts b/src/routes/api/roles/+server.ts similarity index 100% rename from routes/api/roles/+server.ts rename to src/routes/api/roles/+server.ts diff --git a/routes/api/roles/[id]/+server.ts b/src/routes/api/roles/[id]/+server.ts similarity index 100% rename from routes/api/roles/[id]/+server.ts rename to src/routes/api/roles/[id]/+server.ts diff --git a/routes/api/schedules/+server.ts b/src/routes/api/schedules/+server.ts similarity index 100% rename from routes/api/schedules/+server.ts rename to src/routes/api/schedules/+server.ts diff --git a/routes/api/schedules/[type]/[id]/+server.ts b/src/routes/api/schedules/[type]/[id]/+server.ts similarity index 100% rename from routes/api/schedules/[type]/[id]/+server.ts rename to src/routes/api/schedules/[type]/[id]/+server.ts diff --git a/routes/api/schedules/[type]/[id]/run/+server.ts b/src/routes/api/schedules/[type]/[id]/run/+server.ts similarity index 100% rename from routes/api/schedules/[type]/[id]/run/+server.ts rename to src/routes/api/schedules/[type]/[id]/run/+server.ts diff --git a/routes/api/schedules/[type]/[id]/toggle/+server.ts b/src/routes/api/schedules/[type]/[id]/toggle/+server.ts similarity index 100% rename from routes/api/schedules/[type]/[id]/toggle/+server.ts rename to src/routes/api/schedules/[type]/[id]/toggle/+server.ts diff --git a/routes/api/schedules/executions/+server.ts b/src/routes/api/schedules/executions/+server.ts similarity index 100% rename from routes/api/schedules/executions/+server.ts rename to src/routes/api/schedules/executions/+server.ts diff --git a/routes/api/schedules/executions/[id]/+server.ts b/src/routes/api/schedules/executions/[id]/+server.ts similarity index 100% rename from routes/api/schedules/executions/[id]/+server.ts rename to src/routes/api/schedules/executions/[id]/+server.ts diff --git a/routes/api/schedules/settings/+server.ts b/src/routes/api/schedules/settings/+server.ts similarity index 100% rename from routes/api/schedules/settings/+server.ts rename to src/routes/api/schedules/settings/+server.ts diff --git a/routes/api/schedules/stream/+server.ts b/src/routes/api/schedules/stream/+server.ts similarity index 100% rename from routes/api/schedules/stream/+server.ts rename to src/routes/api/schedules/stream/+server.ts diff --git a/routes/api/schedules/system/[id]/toggle/+server.ts b/src/routes/api/schedules/system/[id]/toggle/+server.ts similarity index 100% rename from routes/api/schedules/system/[id]/toggle/+server.ts rename to src/routes/api/schedules/system/[id]/toggle/+server.ts diff --git a/routes/api/settings/general/+server.ts b/src/routes/api/settings/general/+server.ts similarity index 100% rename from routes/api/settings/general/+server.ts rename to src/routes/api/settings/general/+server.ts diff --git a/routes/api/settings/scanner/+server.ts b/src/routes/api/settings/scanner/+server.ts similarity index 100% rename from routes/api/settings/scanner/+server.ts rename to src/routes/api/settings/scanner/+server.ts diff --git a/routes/api/stacks/+server.ts b/src/routes/api/stacks/+server.ts similarity index 100% rename from routes/api/stacks/+server.ts rename to src/routes/api/stacks/+server.ts diff --git a/routes/api/stacks/[name]/+server.ts b/src/routes/api/stacks/[name]/+server.ts similarity index 100% rename from routes/api/stacks/[name]/+server.ts rename to src/routes/api/stacks/[name]/+server.ts diff --git a/routes/api/stacks/[name]/compose/+server.ts b/src/routes/api/stacks/[name]/compose/+server.ts similarity index 100% rename from routes/api/stacks/[name]/compose/+server.ts rename to src/routes/api/stacks/[name]/compose/+server.ts diff --git a/routes/api/stacks/[name]/down/+server.ts b/src/routes/api/stacks/[name]/down/+server.ts similarity index 100% rename from routes/api/stacks/[name]/down/+server.ts rename to src/routes/api/stacks/[name]/down/+server.ts diff --git a/routes/api/stacks/[name]/env/+server.ts b/src/routes/api/stacks/[name]/env/+server.ts similarity index 100% rename from routes/api/stacks/[name]/env/+server.ts rename to src/routes/api/stacks/[name]/env/+server.ts diff --git a/routes/api/stacks/[name]/env/validate/+server.ts b/src/routes/api/stacks/[name]/env/validate/+server.ts similarity index 100% rename from routes/api/stacks/[name]/env/validate/+server.ts rename to src/routes/api/stacks/[name]/env/validate/+server.ts diff --git a/routes/api/stacks/[name]/restart/+server.ts b/src/routes/api/stacks/[name]/restart/+server.ts similarity index 100% rename from routes/api/stacks/[name]/restart/+server.ts rename to src/routes/api/stacks/[name]/restart/+server.ts diff --git a/routes/api/stacks/[name]/start/+server.ts b/src/routes/api/stacks/[name]/start/+server.ts similarity index 100% rename from routes/api/stacks/[name]/start/+server.ts rename to src/routes/api/stacks/[name]/start/+server.ts diff --git a/routes/api/stacks/[name]/stop/+server.ts b/src/routes/api/stacks/[name]/stop/+server.ts similarity index 100% rename from routes/api/stacks/[name]/stop/+server.ts rename to src/routes/api/stacks/[name]/stop/+server.ts diff --git a/routes/api/stacks/sources/+server.ts b/src/routes/api/stacks/sources/+server.ts similarity index 100% rename from routes/api/stacks/sources/+server.ts rename to src/routes/api/stacks/sources/+server.ts diff --git a/routes/api/system/+server.ts b/src/routes/api/system/+server.ts similarity index 100% rename from routes/api/system/+server.ts rename to src/routes/api/system/+server.ts diff --git a/routes/api/system/disk/+server.ts b/src/routes/api/system/disk/+server.ts similarity index 100% rename from routes/api/system/disk/+server.ts rename to src/routes/api/system/disk/+server.ts diff --git a/routes/api/users/+server.ts b/src/routes/api/users/+server.ts similarity index 100% rename from routes/api/users/+server.ts rename to src/routes/api/users/+server.ts diff --git a/routes/api/users/[id]/+server.ts b/src/routes/api/users/[id]/+server.ts similarity index 100% rename from routes/api/users/[id]/+server.ts rename to src/routes/api/users/[id]/+server.ts diff --git a/routes/api/users/[id]/mfa/+server.ts b/src/routes/api/users/[id]/mfa/+server.ts similarity index 100% rename from routes/api/users/[id]/mfa/+server.ts rename to src/routes/api/users/[id]/mfa/+server.ts diff --git a/routes/api/users/[id]/roles/+server.ts b/src/routes/api/users/[id]/roles/+server.ts similarity index 100% rename from routes/api/users/[id]/roles/+server.ts rename to src/routes/api/users/[id]/roles/+server.ts diff --git a/routes/api/volumes/+server.ts b/src/routes/api/volumes/+server.ts similarity index 100% rename from routes/api/volumes/+server.ts rename to src/routes/api/volumes/+server.ts diff --git a/routes/api/volumes/[name]/+server.ts b/src/routes/api/volumes/[name]/+server.ts similarity index 100% rename from routes/api/volumes/[name]/+server.ts rename to src/routes/api/volumes/[name]/+server.ts diff --git a/routes/api/volumes/[name]/browse/+server.ts b/src/routes/api/volumes/[name]/browse/+server.ts similarity index 100% rename from routes/api/volumes/[name]/browse/+server.ts rename to src/routes/api/volumes/[name]/browse/+server.ts diff --git a/routes/api/volumes/[name]/browse/content/+server.ts b/src/routes/api/volumes/[name]/browse/content/+server.ts similarity index 100% rename from routes/api/volumes/[name]/browse/content/+server.ts rename to src/routes/api/volumes/[name]/browse/content/+server.ts diff --git a/routes/api/volumes/[name]/browse/release/+server.ts b/src/routes/api/volumes/[name]/browse/release/+server.ts similarity index 100% rename from routes/api/volumes/[name]/browse/release/+server.ts rename to src/routes/api/volumes/[name]/browse/release/+server.ts diff --git a/routes/api/volumes/[name]/clone/+server.ts b/src/routes/api/volumes/[name]/clone/+server.ts similarity index 100% rename from routes/api/volumes/[name]/clone/+server.ts rename to src/routes/api/volumes/[name]/clone/+server.ts diff --git a/routes/api/volumes/[name]/export/+server.ts b/src/routes/api/volumes/[name]/export/+server.ts similarity index 100% rename from routes/api/volumes/[name]/export/+server.ts rename to src/routes/api/volumes/[name]/export/+server.ts diff --git a/routes/api/volumes/[name]/inspect/+server.ts b/src/routes/api/volumes/[name]/inspect/+server.ts similarity index 100% rename from routes/api/volumes/[name]/inspect/+server.ts rename to src/routes/api/volumes/[name]/inspect/+server.ts diff --git a/routes/audit/+page.svelte b/src/routes/audit/+page.svelte similarity index 100% rename from routes/audit/+page.svelte rename to src/routes/audit/+page.svelte diff --git a/routes/audit/+server.ts b/src/routes/audit/+server.ts similarity index 100% rename from routes/audit/+server.ts rename to src/routes/audit/+server.ts diff --git a/routes/audit/users/+server.ts b/src/routes/audit/users/+server.ts similarity index 100% rename from routes/audit/users/+server.ts rename to src/routes/audit/users/+server.ts diff --git a/routes/containers/+page.svelte b/src/routes/containers/+page.svelte similarity index 100% rename from routes/containers/+page.svelte rename to src/routes/containers/+page.svelte diff --git a/routes/containers/AutoUpdateSettings.svelte b/src/routes/containers/AutoUpdateSettings.svelte similarity index 100% rename from routes/containers/AutoUpdateSettings.svelte rename to src/routes/containers/AutoUpdateSettings.svelte diff --git a/routes/containers/BatchUpdateModal.svelte b/src/routes/containers/BatchUpdateModal.svelte similarity index 100% rename from routes/containers/BatchUpdateModal.svelte rename to src/routes/containers/BatchUpdateModal.svelte diff --git a/routes/containers/ContainerInspectModal.svelte b/src/routes/containers/ContainerInspectModal.svelte similarity index 100% rename from routes/containers/ContainerInspectModal.svelte rename to src/routes/containers/ContainerInspectModal.svelte diff --git a/routes/containers/ContainerTerminal.svelte b/src/routes/containers/ContainerTerminal.svelte similarity index 100% rename from routes/containers/ContainerTerminal.svelte rename to src/routes/containers/ContainerTerminal.svelte diff --git a/routes/containers/ContainerTile.svelte b/src/routes/containers/ContainerTile.svelte similarity index 100% rename from routes/containers/ContainerTile.svelte rename to src/routes/containers/ContainerTile.svelte diff --git a/routes/containers/CreateContainerModal.svelte b/src/routes/containers/CreateContainerModal.svelte similarity index 100% rename from routes/containers/CreateContainerModal.svelte rename to src/routes/containers/CreateContainerModal.svelte diff --git a/routes/containers/EditContainerModal.svelte b/src/routes/containers/EditContainerModal.svelte similarity index 100% rename from routes/containers/EditContainerModal.svelte rename to src/routes/containers/EditContainerModal.svelte diff --git a/routes/containers/FileBrowserModal.svelte b/src/routes/containers/FileBrowserModal.svelte similarity index 100% rename from routes/containers/FileBrowserModal.svelte rename to src/routes/containers/FileBrowserModal.svelte diff --git a/routes/containers/FileBrowserPanel.svelte b/src/routes/containers/FileBrowserPanel.svelte similarity index 100% rename from routes/containers/FileBrowserPanel.svelte rename to src/routes/containers/FileBrowserPanel.svelte diff --git a/routes/dashboard/DraggableGrid.svelte b/src/routes/dashboard/DraggableGrid.svelte similarity index 100% rename from routes/dashboard/DraggableGrid.svelte rename to src/routes/dashboard/DraggableGrid.svelte diff --git a/routes/dashboard/EnvironmentTile.svelte b/src/routes/dashboard/EnvironmentTile.svelte similarity index 100% rename from routes/dashboard/EnvironmentTile.svelte rename to src/routes/dashboard/EnvironmentTile.svelte diff --git a/routes/dashboard/EnvironmentTileSkeleton.svelte b/src/routes/dashboard/EnvironmentTileSkeleton.svelte similarity index 100% rename from routes/dashboard/EnvironmentTileSkeleton.svelte rename to src/routes/dashboard/EnvironmentTileSkeleton.svelte diff --git a/routes/dashboard/dashboard-container-stats.svelte b/src/routes/dashboard/dashboard-container-stats.svelte similarity index 100% rename from routes/dashboard/dashboard-container-stats.svelte rename to src/routes/dashboard/dashboard-container-stats.svelte diff --git a/routes/dashboard/dashboard-cpu-memory-bars.svelte b/src/routes/dashboard/dashboard-cpu-memory-bars.svelte similarity index 100% rename from routes/dashboard/dashboard-cpu-memory-bars.svelte rename to src/routes/dashboard/dashboard-cpu-memory-bars.svelte diff --git a/routes/dashboard/dashboard-cpu-memory-charts.svelte b/src/routes/dashboard/dashboard-cpu-memory-charts.svelte similarity index 100% rename from routes/dashboard/dashboard-cpu-memory-charts.svelte rename to src/routes/dashboard/dashboard-cpu-memory-charts.svelte diff --git a/routes/dashboard/dashboard-disk-usage.svelte b/src/routes/dashboard/dashboard-disk-usage.svelte similarity index 100% rename from routes/dashboard/dashboard-disk-usage.svelte rename to src/routes/dashboard/dashboard-disk-usage.svelte diff --git a/routes/dashboard/dashboard-events-summary.svelte b/src/routes/dashboard/dashboard-events-summary.svelte similarity index 100% rename from routes/dashboard/dashboard-events-summary.svelte rename to src/routes/dashboard/dashboard-events-summary.svelte diff --git a/routes/dashboard/dashboard-header.svelte b/src/routes/dashboard/dashboard-header.svelte similarity index 100% rename from routes/dashboard/dashboard-header.svelte rename to src/routes/dashboard/dashboard-header.svelte diff --git a/routes/dashboard/dashboard-health-banner.svelte b/src/routes/dashboard/dashboard-health-banner.svelte similarity index 100% rename from routes/dashboard/dashboard-health-banner.svelte rename to src/routes/dashboard/dashboard-health-banner.svelte diff --git a/routes/dashboard/dashboard-labels.svelte b/src/routes/dashboard/dashboard-labels.svelte similarity index 100% rename from routes/dashboard/dashboard-labels.svelte rename to src/routes/dashboard/dashboard-labels.svelte diff --git a/routes/dashboard/dashboard-offline-state.svelte b/src/routes/dashboard/dashboard-offline-state.svelte similarity index 100% rename from routes/dashboard/dashboard-offline-state.svelte rename to src/routes/dashboard/dashboard-offline-state.svelte diff --git a/routes/dashboard/dashboard-recent-events.svelte b/src/routes/dashboard/dashboard-recent-events.svelte similarity index 100% rename from routes/dashboard/dashboard-recent-events.svelte rename to src/routes/dashboard/dashboard-recent-events.svelte diff --git a/routes/dashboard/dashboard-resource-stats.svelte b/src/routes/dashboard/dashboard-resource-stats.svelte similarity index 100% rename from routes/dashboard/dashboard-resource-stats.svelte rename to src/routes/dashboard/dashboard-resource-stats.svelte diff --git a/routes/dashboard/dashboard-status-icons.svelte b/src/routes/dashboard/dashboard-status-icons.svelte similarity index 100% rename from routes/dashboard/dashboard-status-icons.svelte rename to src/routes/dashboard/dashboard-status-icons.svelte diff --git a/routes/dashboard/dashboard-top-containers.svelte b/src/routes/dashboard/dashboard-top-containers.svelte similarity index 100% rename from routes/dashboard/dashboard-top-containers.svelte rename to src/routes/dashboard/dashboard-top-containers.svelte diff --git a/routes/dashboard/index.ts b/src/routes/dashboard/index.ts similarity index 100% rename from routes/dashboard/index.ts rename to src/routes/dashboard/index.ts diff --git a/routes/environments/+page.svelte b/src/routes/environments/+page.svelte similarity index 100% rename from routes/environments/+page.svelte rename to src/routes/environments/+page.svelte diff --git a/routes/images/+page.server.ts b/src/routes/images/+page.server.ts similarity index 100% rename from routes/images/+page.server.ts rename to src/routes/images/+page.server.ts diff --git a/routes/images/+page.svelte b/src/routes/images/+page.svelte similarity index 100% rename from routes/images/+page.svelte rename to src/routes/images/+page.svelte diff --git a/routes/images/ImageHistoryModal.svelte b/src/routes/images/ImageHistoryModal.svelte similarity index 100% rename from routes/images/ImageHistoryModal.svelte rename to src/routes/images/ImageHistoryModal.svelte diff --git a/routes/images/ImageLayersView.svelte b/src/routes/images/ImageLayersView.svelte similarity index 100% rename from routes/images/ImageLayersView.svelte rename to src/routes/images/ImageLayersView.svelte diff --git a/routes/images/ImagePullProgressPopover.svelte b/src/routes/images/ImagePullProgressPopover.svelte similarity index 100% rename from routes/images/ImagePullProgressPopover.svelte rename to src/routes/images/ImagePullProgressPopover.svelte diff --git a/routes/images/ImageScanModal.svelte b/src/routes/images/ImageScanModal.svelte similarity index 100% rename from routes/images/ImageScanModal.svelte rename to src/routes/images/ImageScanModal.svelte diff --git a/routes/images/PushToRegistryModal.svelte b/src/routes/images/PushToRegistryModal.svelte similarity index 100% rename from routes/images/PushToRegistryModal.svelte rename to src/routes/images/PushToRegistryModal.svelte diff --git a/routes/images/ScanResultsView.svelte b/src/routes/images/ScanResultsView.svelte similarity index 100% rename from routes/images/ScanResultsView.svelte rename to src/routes/images/ScanResultsView.svelte diff --git a/routes/images/VulnerabilityScanModal.svelte b/src/routes/images/VulnerabilityScanModal.svelte similarity index 100% rename from routes/images/VulnerabilityScanModal.svelte rename to src/routes/images/VulnerabilityScanModal.svelte diff --git a/routes/login/+page.svelte b/src/routes/login/+page.svelte similarity index 100% rename from routes/login/+page.svelte rename to src/routes/login/+page.svelte diff --git a/routes/logs/+page.svelte b/src/routes/logs/+page.svelte similarity index 100% rename from routes/logs/+page.svelte rename to src/routes/logs/+page.svelte diff --git a/routes/logs/LogViewer.svelte b/src/routes/logs/LogViewer.svelte similarity index 100% rename from routes/logs/LogViewer.svelte rename to src/routes/logs/LogViewer.svelte diff --git a/routes/logs/LogsPanel.svelte b/src/routes/logs/LogsPanel.svelte similarity index 100% rename from routes/logs/LogsPanel.svelte rename to src/routes/logs/LogsPanel.svelte diff --git a/routes/networks/+page.svelte b/src/routes/networks/+page.svelte similarity index 100% rename from routes/networks/+page.svelte rename to src/routes/networks/+page.svelte diff --git a/routes/networks/ConnectContainerModal.svelte b/src/routes/networks/ConnectContainerModal.svelte similarity index 100% rename from routes/networks/ConnectContainerModal.svelte rename to src/routes/networks/ConnectContainerModal.svelte diff --git a/routes/networks/CreateNetworkModal.svelte b/src/routes/networks/CreateNetworkModal.svelte similarity index 100% rename from routes/networks/CreateNetworkModal.svelte rename to src/routes/networks/CreateNetworkModal.svelte diff --git a/routes/networks/NetworkInspectModal.svelte b/src/routes/networks/NetworkInspectModal.svelte similarity index 100% rename from routes/networks/NetworkInspectModal.svelte rename to src/routes/networks/NetworkInspectModal.svelte diff --git a/routes/profile/+page.svelte b/src/routes/profile/+page.svelte similarity index 100% rename from routes/profile/+page.svelte rename to src/routes/profile/+page.svelte diff --git a/routes/profile/ChangePasswordModal.svelte b/src/routes/profile/ChangePasswordModal.svelte similarity index 100% rename from routes/profile/ChangePasswordModal.svelte rename to src/routes/profile/ChangePasswordModal.svelte diff --git a/routes/profile/DisableMfaModal.svelte b/src/routes/profile/DisableMfaModal.svelte similarity index 100% rename from routes/profile/DisableMfaModal.svelte rename to src/routes/profile/DisableMfaModal.svelte diff --git a/routes/profile/MfaSetupModal.svelte b/src/routes/profile/MfaSetupModal.svelte similarity index 100% rename from routes/profile/MfaSetupModal.svelte rename to src/routes/profile/MfaSetupModal.svelte diff --git a/routes/registry/+page.svelte b/src/routes/registry/+page.svelte similarity index 100% rename from routes/registry/+page.svelte rename to src/routes/registry/+page.svelte diff --git a/routes/registry/CopyToRegistryModal.svelte b/src/routes/registry/CopyToRegistryModal.svelte similarity index 100% rename from routes/registry/CopyToRegistryModal.svelte rename to src/routes/registry/CopyToRegistryModal.svelte diff --git a/routes/registry/ImagePullModal.svelte b/src/routes/registry/ImagePullModal.svelte similarity index 100% rename from routes/registry/ImagePullModal.svelte rename to src/routes/registry/ImagePullModal.svelte diff --git a/routes/schedules/+page.svelte b/src/routes/schedules/+page.svelte similarity index 100% rename from routes/schedules/+page.svelte rename to src/routes/schedules/+page.svelte diff --git a/routes/settings/+page.svelte b/src/routes/settings/+page.svelte similarity index 100% rename from routes/settings/+page.svelte rename to src/routes/settings/+page.svelte diff --git a/routes/settings/about/AboutTab.svelte b/src/routes/settings/about/AboutTab.svelte similarity index 100% rename from routes/settings/about/AboutTab.svelte rename to src/routes/settings/about/AboutTab.svelte diff --git a/routes/settings/about/LicenseModal.svelte b/src/routes/settings/about/LicenseModal.svelte similarity index 100% rename from routes/settings/about/LicenseModal.svelte rename to src/routes/settings/about/LicenseModal.svelte diff --git a/routes/settings/about/PrivacyModal.svelte b/src/routes/settings/about/PrivacyModal.svelte similarity index 100% rename from routes/settings/about/PrivacyModal.svelte rename to src/routes/settings/about/PrivacyModal.svelte diff --git a/routes/settings/auth/AuthTab.svelte b/src/routes/settings/auth/AuthTab.svelte similarity index 100% rename from routes/settings/auth/AuthTab.svelte rename to src/routes/settings/auth/AuthTab.svelte diff --git a/routes/settings/auth/ldap/LdapModal.svelte b/src/routes/settings/auth/ldap/LdapModal.svelte similarity index 100% rename from routes/settings/auth/ldap/LdapModal.svelte rename to src/routes/settings/auth/ldap/LdapModal.svelte diff --git a/routes/settings/auth/ldap/LdapSubTab.svelte b/src/routes/settings/auth/ldap/LdapSubTab.svelte similarity index 100% rename from routes/settings/auth/ldap/LdapSubTab.svelte rename to src/routes/settings/auth/ldap/LdapSubTab.svelte diff --git a/routes/settings/auth/oidc/OidcModal.svelte b/src/routes/settings/auth/oidc/OidcModal.svelte similarity index 100% rename from routes/settings/auth/oidc/OidcModal.svelte rename to src/routes/settings/auth/oidc/OidcModal.svelte diff --git a/routes/settings/auth/oidc/SsoSubTab.svelte b/src/routes/settings/auth/oidc/SsoSubTab.svelte similarity index 100% rename from routes/settings/auth/oidc/SsoSubTab.svelte rename to src/routes/settings/auth/oidc/SsoSubTab.svelte diff --git a/routes/settings/auth/roles/RoleModal.svelte b/src/routes/settings/auth/roles/RoleModal.svelte similarity index 100% rename from routes/settings/auth/roles/RoleModal.svelte rename to src/routes/settings/auth/roles/RoleModal.svelte diff --git a/routes/settings/auth/roles/RolesSubTab.svelte b/src/routes/settings/auth/roles/RolesSubTab.svelte similarity index 100% rename from routes/settings/auth/roles/RolesSubTab.svelte rename to src/routes/settings/auth/roles/RolesSubTab.svelte diff --git a/routes/settings/auth/users/UserModal.svelte b/src/routes/settings/auth/users/UserModal.svelte similarity index 100% rename from routes/settings/auth/users/UserModal.svelte rename to src/routes/settings/auth/users/UserModal.svelte diff --git a/routes/settings/auth/users/UsersSubTab.svelte b/src/routes/settings/auth/users/UsersSubTab.svelte similarity index 100% rename from routes/settings/auth/users/UsersSubTab.svelte rename to src/routes/settings/auth/users/UsersSubTab.svelte diff --git a/routes/settings/config-sets/ConfigSetModal.svelte b/src/routes/settings/config-sets/ConfigSetModal.svelte similarity index 100% rename from routes/settings/config-sets/ConfigSetModal.svelte rename to src/routes/settings/config-sets/ConfigSetModal.svelte diff --git a/routes/settings/config-sets/ConfigSetsTab.svelte b/src/routes/settings/config-sets/ConfigSetsTab.svelte similarity index 100% rename from routes/settings/config-sets/ConfigSetsTab.svelte rename to src/routes/settings/config-sets/ConfigSetsTab.svelte diff --git a/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte similarity index 100% rename from routes/settings/environments/EnvironmentModal.svelte rename to src/routes/settings/environments/EnvironmentModal.svelte diff --git a/routes/settings/environments/EnvironmentsTab.svelte b/src/routes/settings/environments/EnvironmentsTab.svelte similarity index 100% rename from routes/settings/environments/EnvironmentsTab.svelte rename to src/routes/settings/environments/EnvironmentsTab.svelte diff --git a/routes/settings/environments/EventTypesEditor.svelte b/src/routes/settings/environments/EventTypesEditor.svelte similarity index 100% rename from routes/settings/environments/EventTypesEditor.svelte rename to src/routes/settings/environments/EventTypesEditor.svelte diff --git a/routes/settings/general/GeneralTab.svelte b/src/routes/settings/general/GeneralTab.svelte similarity index 100% rename from routes/settings/general/GeneralTab.svelte rename to src/routes/settings/general/GeneralTab.svelte diff --git a/routes/settings/git/GitCredentialModal.svelte b/src/routes/settings/git/GitCredentialModal.svelte similarity index 100% rename from routes/settings/git/GitCredentialModal.svelte rename to src/routes/settings/git/GitCredentialModal.svelte diff --git a/routes/settings/git/GitCredentialsTab.svelte b/src/routes/settings/git/GitCredentialsTab.svelte similarity index 100% rename from routes/settings/git/GitCredentialsTab.svelte rename to src/routes/settings/git/GitCredentialsTab.svelte diff --git a/routes/settings/git/GitRepositoriesTab.svelte b/src/routes/settings/git/GitRepositoriesTab.svelte similarity index 100% rename from routes/settings/git/GitRepositoriesTab.svelte rename to src/routes/settings/git/GitRepositoriesTab.svelte diff --git a/routes/settings/git/GitRepositoryModal.svelte b/src/routes/settings/git/GitRepositoryModal.svelte similarity index 100% rename from routes/settings/git/GitRepositoryModal.svelte rename to src/routes/settings/git/GitRepositoryModal.svelte diff --git a/routes/settings/git/GitTab.svelte b/src/routes/settings/git/GitTab.svelte similarity index 100% rename from routes/settings/git/GitTab.svelte rename to src/routes/settings/git/GitTab.svelte diff --git a/routes/settings/license/LicenseTab.svelte b/src/routes/settings/license/LicenseTab.svelte similarity index 100% rename from routes/settings/license/LicenseTab.svelte rename to src/routes/settings/license/LicenseTab.svelte diff --git a/routes/settings/notifications/NotificationModal.svelte b/src/routes/settings/notifications/NotificationModal.svelte similarity index 100% rename from routes/settings/notifications/NotificationModal.svelte rename to src/routes/settings/notifications/NotificationModal.svelte diff --git a/routes/settings/notifications/NotificationsTab.svelte b/src/routes/settings/notifications/NotificationsTab.svelte similarity index 100% rename from routes/settings/notifications/NotificationsTab.svelte rename to src/routes/settings/notifications/NotificationsTab.svelte diff --git a/routes/settings/registries/RegistriesTab.svelte b/src/routes/settings/registries/RegistriesTab.svelte similarity index 100% rename from routes/settings/registries/RegistriesTab.svelte rename to src/routes/settings/registries/RegistriesTab.svelte diff --git a/routes/settings/registries/RegistryModal.svelte b/src/routes/settings/registries/RegistryModal.svelte similarity index 100% rename from routes/settings/registries/RegistryModal.svelte rename to src/routes/settings/registries/RegistryModal.svelte diff --git a/routes/stacks/+page.svelte b/src/routes/stacks/+page.svelte similarity index 100% rename from routes/stacks/+page.svelte rename to src/routes/stacks/+page.svelte diff --git a/routes/stacks/ComposeGraphViewer.svelte b/src/routes/stacks/ComposeGraphViewer.svelte similarity index 100% rename from routes/stacks/ComposeGraphViewer.svelte rename to src/routes/stacks/ComposeGraphViewer.svelte diff --git a/routes/stacks/GitDeployProgressPopover.svelte b/src/routes/stacks/GitDeployProgressPopover.svelte similarity index 100% rename from routes/stacks/GitDeployProgressPopover.svelte rename to src/routes/stacks/GitDeployProgressPopover.svelte diff --git a/routes/stacks/GitStackModal.svelte b/src/routes/stacks/GitStackModal.svelte similarity index 100% rename from routes/stacks/GitStackModal.svelte rename to src/routes/stacks/GitStackModal.svelte diff --git a/routes/stacks/StackModal.svelte b/src/routes/stacks/StackModal.svelte similarity index 100% rename from routes/stacks/StackModal.svelte rename to src/routes/stacks/StackModal.svelte diff --git a/routes/terminal/+page.svelte b/src/routes/terminal/+page.svelte similarity index 100% rename from routes/terminal/+page.svelte rename to src/routes/terminal/+page.svelte diff --git a/routes/terminal/Terminal.svelte b/src/routes/terminal/Terminal.svelte similarity index 100% rename from routes/terminal/Terminal.svelte rename to src/routes/terminal/Terminal.svelte diff --git a/routes/terminal/TerminalEmulator.svelte b/src/routes/terminal/TerminalEmulator.svelte similarity index 100% rename from routes/terminal/TerminalEmulator.svelte rename to src/routes/terminal/TerminalEmulator.svelte diff --git a/routes/terminal/TerminalPanel.svelte b/src/routes/terminal/TerminalPanel.svelte similarity index 100% rename from routes/terminal/TerminalPanel.svelte rename to src/routes/terminal/TerminalPanel.svelte diff --git a/routes/terminal/[id]/+page.svelte b/src/routes/terminal/[id]/+page.svelte similarity index 100% rename from routes/terminal/[id]/+page.svelte rename to src/routes/terminal/[id]/+page.svelte diff --git a/routes/volumes/+page.svelte b/src/routes/volumes/+page.svelte similarity index 100% rename from routes/volumes/+page.svelte rename to src/routes/volumes/+page.svelte diff --git a/routes/volumes/CloneVolumeModal.svelte b/src/routes/volumes/CloneVolumeModal.svelte similarity index 100% rename from routes/volumes/CloneVolumeModal.svelte rename to src/routes/volumes/CloneVolumeModal.svelte diff --git a/routes/volumes/CreateVolumeModal.svelte b/src/routes/volumes/CreateVolumeModal.svelte similarity index 100% rename from routes/volumes/CreateVolumeModal.svelte rename to src/routes/volumes/CreateVolumeModal.svelte diff --git a/routes/volumes/VolumeBrowserModal.svelte b/src/routes/volumes/VolumeBrowserModal.svelte similarity index 100% rename from routes/volumes/VolumeBrowserModal.svelte rename to src/routes/volumes/VolumeBrowserModal.svelte diff --git a/routes/volumes/VolumeInspectModal.svelte b/src/routes/volumes/VolumeInspectModal.svelte similarity index 100% rename from routes/volumes/VolumeInspectModal.svelte rename to src/routes/volumes/VolumeInspectModal.svelte diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..fb3570f --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from 'svelte-adapter-bun'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + + kit: { + adapter: adapter({ + out: 'build' + }) + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5f1c578 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,996 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig, type Plugin } from 'vite'; +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import { Database } from 'bun:sqlite'; + +const WS_PORT = 5174; + +// ============ Docker Target Types ============ + +interface DockerTarget { + type: 'unix' | 'tcp' | 'hawser-edge'; + socket?: string; + host?: string; + port?: number; + hawserToken?: string; + environmentId?: number; +} + +interface EnvironmentRow { + id: number; + is_local?: boolean | number; + connection_type?: string; + socket_path?: string; + host?: string; + port?: number; + hawser_token?: string; +} + +// ============ Docker Target Resolution ============ + +function resolveDockerTarget( + envId: number | undefined, + getEnvironment: (id: number) => EnvironmentRow | null, + defaultSocketPath: string +): DockerTarget { + if (!envId) return { type: 'unix', socket: defaultSocketPath }; + + const env = getEnvironment(envId); + if (!env) return { type: 'unix', socket: defaultSocketPath }; + + const isLocal = typeof env.is_local === 'boolean' ? env.is_local : Boolean(env.is_local); + if (isLocal || env.connection_type === 'socket' || !env.connection_type) { + return { type: 'unix', socket: env.socket_path || defaultSocketPath }; + } + + if (env.connection_type === 'hawser-edge') { + return { type: 'hawser-edge', environmentId: envId }; + } + + return { + type: 'tcp', + host: env.host || 'localhost', + port: env.port || 2375, + hawserToken: env.connection_type === 'hawser-standard' ? env.hawser_token : undefined + }; +} + +// ============ Exec API Helpers ============ + +function buildExecStartHttpRequest(execId: string, target: DockerTarget): string { + const body = JSON.stringify({ Detach: false, Tty: true }); + const tokenHeader = target.type === 'tcp' && target.hawserToken + ? `X-Hawser-Token: ${target.hawserToken}\r\n` + : ''; + return `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\n${tokenHeader}Connection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${body.length}\r\n\r\n${body}`; +} + +// ============ Stream Processing ============ + +function processTerminalOutput( + data: string, + state: { headersStripped: boolean; isChunked: boolean } +): string | null { + let text = data; + + if (!state.headersStripped) { + if (text.toLowerCase().includes('transfer-encoding: chunked')) { + state.isChunked = true; + } + const headerEnd = text.indexOf('\r\n\r\n'); + if (headerEnd > -1) { + text = text.slice(headerEnd + 4); + state.headersStripped = true; + } else if (text.startsWith('HTTP/')) { + return null; + } + } + + if (state.isChunked && text) { + text = text.replace(/^[0-9a-fA-F]+\r\n/gm, '').replace(/\r\n$/g, ''); + } + + return text || null; +} + +// ============ Hawser Edge Exec Messages ============ + +function createExecStartMessage(execId: string, containerId: string, shell: string, user: string, cols = 120, rows = 30) { + return { type: 'exec_start', execId, containerId, cmd: shell, user, cols, rows }; +} + +function createExecInputMessage(execId: string, data: string) { + return { type: 'exec_input', execId, data: Buffer.from(data).toString('base64') }; +} + +function createExecResizeMessage(execId: string, cols: number, rows: number) { + return { type: 'exec_resize', execId, cols, rows }; +} + +function createExecEndMessage(execId: string, reason = 'user_closed') { + return { type: 'exec_end', execId, reason }; +} + +// Get build info +function getGitCommit(): string | null { + // Check COMMIT file (created by CI/CD before docker build) + try { + if (existsSync('COMMIT')) { + const commit = require('fs').readFileSync('COMMIT', 'utf-8').trim(); + if (commit && commit !== 'unknown') { + return commit; + } + } + } catch { + // ignore + } + // Fall back to git command (local dev) + try { + return execSync('git rev-parse --short HEAD').toString().trim(); + } catch { + return null; + } +} + +function getGitBranch(): string | null { + // Check BRANCH file (created by CI/CD before docker build) + try { + if (existsSync('BRANCH')) { + const branch = require('fs').readFileSync('BRANCH', 'utf-8').trim(); + if (branch && branch !== 'unknown') { + return branch; + } + } + } catch { + // ignore + } + // Fall back to git command (local dev) + try { + return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); + } catch { + return null; + } +} + +function getGitTag(): string | null { + // First check env var (set by CI/CD via Docker build-arg) + if (process.env.APP_VERSION) { + return process.env.APP_VERSION; + } + // Check VERSION file (created by CI/CD before docker build) + try { + if (existsSync('VERSION')) { + const version = require('fs').readFileSync('VERSION', 'utf-8').trim(); + if (version && version !== 'unknown') { + return version; + } + } + } catch { + // ignore + } + // Fall back to git tag (local dev) + try { + return execSync('git describe --tags --abbrev=0 2>/dev/null').toString().trim(); + } catch { + return null; + } +} + +// Plugin to externalize bun: protocol modules +function bunExternals(): Plugin { + return { + name: 'bun-externals', + enforce: 'pre', + resolveId(source) { + if (source.startsWith('bun:')) { + return { id: source, external: true }; + } + return null; + } + }; +} + +// Detect Docker socket path +function detectDockerSocket(): string { + if (process.env.DOCKER_SOCKET && existsSync(process.env.DOCKER_SOCKET)) return process.env.DOCKER_SOCKET; + if (process.env.DOCKER_HOST?.startsWith('unix://')) { + const p = process.env.DOCKER_HOST.replace('unix://', ''); + if (existsSync(p)) return p; + } + const candidates = [ + '/var/run/docker.sock', + join(homedir(), '.docker/run/docker.sock'), + join(homedir(), '.orbstack/run/docker.sock'), + '/run/docker.sock' + ]; + for (const s of candidates) { + if (existsSync(s)) return s; + } + return '/var/run/docker.sock'; +} + +// Lazy database connection for environment lookup +let _db: Database | null = null; +function getDb(): Database | null { + if (!_db) { + // Database is in data/db/dockhand.db (same as main app) + const dbPath = join(process.cwd(), 'data', 'db', 'dockhand.db'); + if (existsSync(dbPath)) { + _db = new Database(dbPath, { readonly: true }); + } + } + return _db; +} + +function getEnvironment(id: number): { host: string; port: number; is_local: boolean; connection_type?: string; hawser_token?: string } | null { + const db = getDb(); + if (!db) return null; + const row = db.prepare('SELECT * FROM environments WHERE id = ?').get(id) as any; + return row ? { ...row, is_local: Boolean(row.is_local) } : null; +} + +function getDockerTarget(envId?: number): DockerTarget { + const dockerSocketPath = detectDockerSocket(); + return resolveDockerTarget( + envId, + (id) => getEnvironment(id) as EnvironmentRow | null, + dockerSocketPath + ); +} + +async function createExecForWs(containerId: string, cmd: string[], user: string, target: ReturnType): Promise<{ Id: string }> { + const headers: Record = { 'Content-Type': 'application/json' }; + const fetchOpts: any = { + method: 'POST', + headers, + body: JSON.stringify({ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: cmd, User: user }) + }; + let url: string; + if (target.type === 'unix') { + url = 'http://localhost/containers/' + containerId + '/exec'; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/containers/' + containerId + '/exec'; + if (target.hawserToken) { + headers['X-Hawser-Token'] = target.hawserToken; + } + } + const res = await fetch(url, fetchOpts); + if (!res.ok) throw new Error('Failed to create exec: ' + (await res.text())); + return res.json(); +} + +async function resizeExecForWs(execId: string, cols: number, rows: number, target: ReturnType): Promise { + try { + const fetchOpts: any = { method: 'POST' }; + let url: string; + if (target.type === 'unix') { + url = 'http://localhost/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + if (target.hawserToken) { + fetchOpts.headers = { 'X-Hawser-Token': target.hawserToken }; + } + } + await fetch(url, fetchOpts); + } catch { + // Ignore resize errors + } +} + +// Map to track Docker streams per WebSocket (keyed by unique connection ID) +// Includes WebSocket reference for orphan detection +const dockerStreams = new Map; state: { isChunked: boolean }; ws: any }>(); + +// Counter for unique WebSocket connection IDs +let wsConnectionCounter = 0; + +// Map to track Edge exec sessions (execId -> frontend WebSocket) +const edgeExecSessions = new Map(); + +// Cleanup interval reference - only started in dev mode +let cleanupInterval: ReturnType | null = null; + +// Cleanup function for orphaned sessions +function startCleanupInterval() { + if (cleanupInterval) return; // Already running + + // Cleanup orphaned sessions every 5 minutes to prevent memory leaks + // Only removes sessions where the WebSocket is no longer open (readyState !== 1) + // This catches sessions where close handlers failed to fire + cleanupInterval = setInterval(() => { + let dockerCleaned = 0; + let edgeCleaned = 0; + + for (const [connId, session] of dockerStreams.entries()) { + // readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED + if (session.ws?.readyState !== 1) { + try { + session.stream?.end?.(); + } catch { /* ignore */ } + dockerStreams.delete(connId); + dockerCleaned++; + } + } + + for (const [execId, session] of edgeExecSessions.entries()) { + if (session.ws?.readyState !== 1) { + edgeExecSessions.delete(execId); + edgeCleaned++; + } + } + + if (dockerCleaned > 0 || edgeCleaned > 0) { + console.log(`[WS Cleanup] Removed ${dockerCleaned} orphaned docker streams, ${edgeCleaned} orphaned edge sessions`); + } + }, 5 * 60 * 1000); +} + +// Hawser Edge connection types (mirrors hawser.ts) +interface EdgeConnection { + ws: WebSocket; + environmentId: number; + agentId: string; + agentName: string; + agentVersion: string; + dockerVersion: string; + hostname: string; + capabilities: string[]; + connectedAt: Date; + lastHeartbeat: Date; + pendingRequests: Map; + pendingStreamRequests: Map; + pingInterval?: ReturnType; // Server-side ping to keep connection alive through proxies +} + +// Container event from edge agent (matches hawser.ts) +interface ContainerEventData { + containerId: string; + containerName?: string; + image?: string; + action: string; + actorAttributes?: Record; + timestamp: string; +} + +// Metrics data structure from Hawser agent +interface HawserMetrics { + cpuUsage: number; + cpuCores: number; + memoryTotal: number; + memoryUsed: number; + memoryFree: number; + diskTotal: number; + diskUsed: number; + diskFree: number; + networkRxBytes: number; + networkTxBytes: number; +} + +// Use globalThis to share connections with hawser.ts module +declare global { + var __hawserEdgeConnections: Map | undefined; + var __hawserSendMessage: ((envId: number, message: string) => boolean) | undefined; + var __hawserHandleContainerEvent: ((envId: number, event: ContainerEventData) => Promise) | undefined; + var __hawserHandleMetrics: ((envId: number, metrics: HawserMetrics) => Promise) | undefined; +} +const edgeConnections: Map = + globalThis.__hawserEdgeConnections ?? (globalThis.__hawserEdgeConnections = new Map()); + +// Function to send messages through the WebSocket (needed because ws.send must be called from vite context) +globalThis.__hawserSendMessage = (envId: number, message: string): boolean => { + const conn = edgeConnections.get(envId); + if (!conn || !conn.ws) { + return false; + } + + try { + conn.ws.send(message); + return true; + } catch (e) { + console.error(`[Hawser WS] sendMessage error:`, e); + return false; + } +}; + +// Map WebSocket to environmentId for quick lookup on close/message +const wsToEnvId = new Map(); + +// WebSocket server for terminal connections and Hawser Edge in development mode +function webSocketPlugin(): Plugin { + return { + name: 'websocket', + configureServer() { + // Start cleanup interval for dev mode only + startCleanupInterval(); + + const dockerSocketPath = detectDockerSocket(); + console.log(`[Terminal WS] Detected Docker socket at: ${dockerSocketPath}`); + + // Start a Bun.serve WebSocket server on a separate port + Bun.serve({ + port: WS_PORT, + fetch(req, server) { + // Upgrade HTTP requests to WebSocket + if (server.upgrade(req, { data: { url: req.url } })) { + return; // Return nothing if upgrade succeeds + } + return new Response('WebSocket server', { status: 200 }); + }, + websocket: { + async open(ws) { + const url = new URL((ws.data as any).url, `http://localhost:${WS_PORT}`); + + // Check if this is a Hawser Edge connection + if (url.pathname === '/api/hawser/connect') { + console.log('[Hawser WS] New connection pending authentication'); + // Hawser connections wait for hello message to authenticate + return; + } + + // Assign unique connection ID to this WebSocket + const connId = `ws-${++wsConnectionCounter}`; + (ws.data as any).connId = connId; + + // Terminal connection handling + const pathParts = url.pathname.split('/'); + const containerIdIndex = pathParts.indexOf('containers') + 1; + const containerId = pathParts[containerIdIndex]; + + const shell = url.searchParams.get('shell') || '/bin/sh'; + const user = url.searchParams.get('user') || 'root'; + const envIdParam = url.searchParams.get('envId'); + const envId = envIdParam ? parseInt(envIdParam, 10) : undefined; + + if (!containerId) { + ws.send(JSON.stringify({ type: 'error', message: 'No container ID' })); + ws.close(); + return; + } + + const target = getDockerTarget(envId); + console.log('[Terminal WS] Open connId:', connId, 'container:', containerId, 'target:', target.type); + + try { + // Handle Hawser Edge mode differently - use WebSocket protocol + if (target.type === 'hawser-edge') { + const conn = edgeConnections.get(target.environmentId); + if (!conn) { + ws.send(JSON.stringify({ type: 'error', message: 'Edge agent not connected' })); + ws.close(); + return; + } + + // Generate unique exec ID + const execId = crypto.randomUUID(); + console.log('[Terminal WS] Starting Edge exec:', execId, 'container:', containerId); + + // Track this session + edgeExecSessions.set(execId, { ws, execId, environmentId: target.environmentId }); + (ws.data as any).edgeExecId = execId; + + // Send exec_start to the agent (using shared helper) + const execStartMsg = createExecStartMessage(execId, containerId, shell, user); + conn.ws.send(JSON.stringify(execStartMsg)); + return; + } + + // Direct Docker connection (unix or tcp/hawser-standard) + const exec = await createExecForWs(containerId, [shell], user, target); + const execId = exec.Id; + + // Track connection state (using object for mutability across closures) + let headersStripped = false; + const state = { isChunked: false }; + + // Create socket handler for Docker connection + const socketHandler = { + data(socket: any, data: Buffer) { + if (ws.readyState === 1) { + let text = new TextDecoder().decode(data); + // Skip HTTP headers in first response (only once) + if (!headersStripped) { + // Check for chunked encoding in headers + if (text.toLowerCase().includes('transfer-encoding: chunked')) { + state.isChunked = true; + } + const headerEnd = text.indexOf('\r\n\r\n'); + if (headerEnd > -1) { + text = text.slice(headerEnd + 4); + headersStripped = true; + } else if (text.startsWith('HTTP/')) { + // Headers split across packets, skip this entire packet + return; + } + } + // Strip chunked encoding framing if detected + if (state.isChunked && text) { + // Remove chunk size lines (hex number followed by \r\n) + text = text.replace(/^[0-9a-fA-F]+\r\n/gm, '').replace(/\r\n$/g, ''); + } + if (text) { + ws.send(JSON.stringify({ type: 'output', data: text })); + } + } + }, + close() { + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: 'exit' })); + ws.close(); + } + }, + error() {}, + open(socket: any) { + // Send exec start request (using shared helper) + const httpRequest = buildExecStartHttpRequest(execId, target); + socket.write(httpRequest); + } + }; + + let dockerStream: any; + if (target.type === 'unix') { + dockerStream = await Bun.connect({ unix: target.socket, socket: socketHandler }); + } else if (target.type === 'tcp') { + dockerStream = await Bun.connect({ hostname: target.host, port: target.port, socket: socketHandler }); + } + + dockerStreams.set(connId, { stream: dockerStream, execId, target, state, ws }); + console.log('[Terminal WS] Stream stored for connId:', connId, 'total streams:', dockerStreams.size); + } catch (error: any) { + console.error('[Terminal WS] Error:', error.message); + ws.send(JSON.stringify({ type: 'error', message: error.message })); + ws.close(); + } + }, + async message(ws, message) { + const url = new URL((ws.data as any).url, `http://localhost:${WS_PORT}`); + const connId = (ws.data as any).connId as string | undefined; + console.log('[WS Message] connId:', connId, 'edgeExecId:', (ws.data as any)?.edgeExecId, 'pathname:', url.pathname.slice(0, 50)); + + // Handle Hawser Edge messages + if (url.pathname === '/api/hawser/connect') { + try { + // Debug: Log raw message info + const msgType = typeof message; + const msgLen = typeof message === 'string' ? message.length : + message instanceof ArrayBuffer ? message.byteLength : + (message as Buffer).length || 0; + console.log(`[Hawser WS] Received message: type=${msgType}, length=${msgLen}`); + + // Convert message to string properly (handles both string and ArrayBuffer) + let messageStr: string; + if (typeof message === 'string') { + messageStr = message; + } else if (message instanceof ArrayBuffer) { + messageStr = new TextDecoder().decode(message); + } else if (Buffer.isBuffer(message)) { + messageStr = message.toString('utf-8'); + } else { + // Uint8Array or similar + messageStr = new TextDecoder().decode(new Uint8Array(message as ArrayBuffer)); + } + + console.log(`[Hawser WS] Decoded string length: ${messageStr.length}`); + if (messageStr.length > 0) { + console.log(`[Hawser WS] First 200 chars: ${messageStr.slice(0, 200)}`); + } + + const msg = JSON.parse(messageStr); + console.log(`[Hawser WS] Parsed message type: ${msg.type}`); + await handleHawserMessage(ws, msg); + } catch (error: any) { + console.error('[Hawser WS] Error handling message:', error.message); + // More detailed debug output + const msgType = typeof message; + const msgLen = typeof message === 'string' ? message.length : + message instanceof ArrayBuffer ? message.byteLength : + (message as Buffer).length || 0; + console.error(`[Hawser WS] Message details: type=${msgType}, length=${msgLen}`); + if (typeof message === 'string' && message.length > 0) { + console.error(`[Hawser WS] Message preview: ${message.slice(0, 500)}`); + } else if (message instanceof ArrayBuffer && message.byteLength > 0) { + const preview = new TextDecoder().decode(message.slice(0, 500)); + console.error(`[Hawser WS] ArrayBuffer preview: ${preview}`); + } else if (Buffer.isBuffer(message) && message.length > 0) { + console.error(`[Hawser WS] Buffer preview: ${message.toString('utf-8').slice(0, 500)}`); + } + ws.send(JSON.stringify({ type: 'error', error: error.message })); + } + return; + } + + // Check if this is an Edge exec session + const edgeExecId = (ws.data as any)?.edgeExecId; + if (edgeExecId) { + const session = edgeExecSessions.get(edgeExecId); + if (session) { + const conn = edgeConnections.get(session.environmentId); + if (conn) { + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input') { + // Forward input to agent (using shared helper) + conn.ws.send(JSON.stringify(createExecInputMessage(edgeExecId, msg.data))); + } else if (msg.type === 'resize') { + // Forward resize to agent (using shared helper) + conn.ws.send(JSON.stringify(createExecResizeMessage(edgeExecId, msg.cols, msg.rows))); + } + } catch (e) { + console.error('[Terminal WS] Error handling Edge message:', e); + } + } + } + return; + } + + // Terminal message handling (direct Docker connection) + if (!connId) { + console.log('[Terminal WS] No connId for terminal message'); + return; + } + const d = dockerStreams.get(connId); + if (!d) { + console.log('[Terminal WS] No stream for connId:', connId, 'streams:', [...dockerStreams.keys()]); + return; + } + console.log('[Terminal WS] Found stream for connId:', connId); + + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input' && d.stream) { + // Always write raw input - chunked encoding only affects reading output + d.stream.write(msg.data); + } else if (msg.type === 'resize' && d.execId) { + resizeExecForWs(d.execId, msg.cols, msg.rows, d.target); + } + } catch { + // If not JSON, treat as raw input + if (d.stream) { + d.stream.write(message); + } + } + }, + close(ws) { + // Check if it's a Hawser connection + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + console.log(`[Hawser WS] Agent disconnected: ${conn.agentId}`); + // Clear server-side ping interval + if (conn.pingInterval) { + clearInterval(conn.pingInterval); + conn.pingInterval = undefined; + } + // Reject pending requests + for (const [, pending] of conn.pendingRequests) { + clearTimeout(pending.timeout); + pending.reject(new Error('Connection closed')); + } + // Clean up pending stream requests + for (const [, pending] of conn.pendingStreamRequests) { + pending.onEnd('Connection closed'); + } + edgeConnections.delete(envId); + } + wsToEnvId.delete(ws); + return; + } + + // Check if it's an Edge exec session + const edgeExecId = (ws.data as any)?.edgeExecId; + if (edgeExecId) { + const session = edgeExecSessions.get(edgeExecId); + if (session) { + // Send exec_end to agent (using shared helper) + const conn = edgeConnections.get(session.environmentId); + if (conn) { + conn.ws.send(JSON.stringify(createExecEndMessage(edgeExecId))); + } + edgeExecSessions.delete(edgeExecId); + console.log(`[Terminal WS] Edge exec session closed: ${edgeExecId}`); + } + return; + } + + // Terminal connection cleanup (direct Docker) + const connId = (ws.data as any)?.connId as string | undefined; + if (connId) { + const d = dockerStreams.get(connId); + if (d?.stream) { + d.stream.end(); + } + dockerStreams.delete(connId); + } + } + } + }); + + console.log(`[Terminal WS] WebSocket server running on port ${WS_PORT}`); + } + }; +} + +// Handle Hawser Edge protocol messages +async function handleHawserMessage(ws: any, msg: any) { + if (msg.type === 'hello') { + // Validate token using the app's hawser module + // For dev mode, we'll do a simplified validation + console.log(`[Hawser WS] Hello from agent: ${msg.agentName} (${msg.agentId})`); + + // In dev mode, we need to validate the token against the database + const db = getDb(); + if (!db) { + ws.send(JSON.stringify({ type: 'error', error: 'Database not available' })); + ws.close(); + return; + } + + // Simple token validation (in production this would use argon2 verification) + // For dev mode, just check if a token exists for any environment + const tokens = db.prepare('SELECT * FROM hawser_tokens WHERE is_active = 1').all() as any[]; + + // For dev mode, accept any valid token format and use the first environment with a token + const token = tokens.find((t: any) => msg.token && msg.token.startsWith(t.token_prefix.slice(0, 4))); + + if (!token) { + console.log('[Hawser WS] Invalid token'); + ws.send(JSON.stringify({ type: 'error', error: 'Invalid token' })); + ws.close(); + return; + } + + const environmentId = token.environment_id; + + // Update environment with agent info + try { + db.prepare(`UPDATE environments SET + hawser_last_seen = datetime('now'), + hawser_agent_id = ?, + hawser_agent_name = ?, + hawser_version = ?, + hawser_capabilities = ? + WHERE id = ?`).run( + msg.agentId, + msg.agentName, + msg.version, + JSON.stringify(msg.capabilities || []), + environmentId + ); + } catch (e) { + // Read-only DB in dev mode, ignore + } + + // Close any existing connection for this environment + const existing = edgeConnections.get(environmentId); + if (existing) { + const pendingCount = existing.pendingRequests.size; + const streamCount = existing.pendingStreamRequests.size; + console.log( + `[Hawser WS] Replacing existing connection for environment ${environmentId}. ` + + `Rejecting ${pendingCount} pending requests and ${streamCount} stream requests.` + ); + + // Reject all pending requests before closing + for (const [requestId, pending] of existing.pendingRequests) { + console.log(`[Hawser WS] Rejecting pending request ${requestId} due to connection replacement`); + clearTimeout(pending.timeout); + pending.reject(new Error('Connection replaced by new agent')); + } + for (const [requestId, pending] of existing.pendingStreamRequests) { + console.log(`[Hawser WS] Ending stream request ${requestId} due to connection replacement`); + pending.onEnd?.('Connection replaced by new agent'); + } + existing.pendingRequests.clear(); + existing.pendingStreamRequests.clear(); + + existing.ws.close(1000, 'Replaced by new connection'); + wsToEnvId.delete(existing.ws); + } + + // Store connection in shared map (accessible by hawser.ts via globalThis) + const connection: EdgeConnection = { + ws, + environmentId, + agentId: msg.agentId, + agentName: msg.agentName, + agentVersion: msg.version || 'unknown', + dockerVersion: msg.dockerVersion || 'unknown', + hostname: msg.hostname || 'unknown', + capabilities: msg.capabilities || [], + connectedAt: new Date(), + lastHeartbeat: new Date(), + pendingRequests: new Map(), + pendingStreamRequests: new Map() + }; + + edgeConnections.set(environmentId, connection); + wsToEnvId.set(ws, environmentId); + + // Send welcome + ws.send(JSON.stringify({ + type: 'welcome', + environmentId, + message: `Welcome ${msg.agentName}! Connected to Dockhand dev server.` + })); + + // Start server-side ping interval to keep connection alive through Traefik/proxies + // Traefik has ~10s idle timeout, so we ping every 5 seconds + connection.pingInterval = setInterval(() => { + try { + ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); + } catch (e) { + // Connection likely closed, clear interval + if (connection.pingInterval) { + clearInterval(connection.pingInterval); + connection.pingInterval = undefined; + } + } + }, 5000); + + console.log(`[Hawser WS] Agent ${msg.agentName} connected for environment ${environmentId}`); + } else if (msg.type === 'ping') { + // Agent sent ping - respond with pong to keep connection alive + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + conn.lastHeartbeat = new Date(); + } + } + ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); + } else if (msg.type === 'pong') { + // Heartbeat response - update last seen + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + conn.lastHeartbeat = new Date(); + } + } + } else if (msg.type === 'response') { + // Response to a request we sent + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + const pending = conn.pendingRequests.get(msg.requestId); + if (pending) { + clearTimeout(pending.timeout); + conn.pendingRequests.delete(msg.requestId); + + // Body is now a string (either plain text/JSON or base64-encoded binary) + // isBinary flag indicates if base64 decoding is needed + pending.resolve({ + statusCode: msg.statusCode, + headers: msg.headers || {}, + body: msg.body || '', + isBinary: msg.isBinary || false + }); + } + } + } + } else if (msg.type === 'stream') { + // Streaming data from agent + const envId = wsToEnvId.get(ws); + if (!envId) { + console.warn(`[Hawser WS] Stream data from unknown WebSocket, requestId=${msg.requestId}`); + return; + } + const conn = edgeConnections.get(envId); + if (!conn) { + console.warn(`[Hawser WS] Stream data for unknown environment ${envId}, requestId=${msg.requestId}`); + return; + } + const pending = conn.pendingStreamRequests?.get(msg.requestId); + if (!pending) { + console.warn(`[Hawser WS] Stream data for unknown request ${msg.requestId} on env ${envId}`); + return; + } + pending.onData(msg.data, msg.stream); + } else if (msg.type === 'stream_end') { + // Stream ended + const envId = wsToEnvId.get(ws); + if (!envId) { + console.warn(`[Hawser WS] Stream end from unknown WebSocket, requestId=${msg.requestId}`); + return; + } + const conn = edgeConnections.get(envId); + if (!conn) { + console.warn(`[Hawser WS] Stream end for unknown environment ${envId}, requestId=${msg.requestId}`); + return; + } + const pending = conn.pendingStreamRequests.get(msg.requestId); + if (!pending) { + console.warn(`[Hawser WS] Stream end for unknown request ${msg.requestId} on env ${envId}`); + return; + } + conn.pendingStreamRequests.delete(msg.requestId); + pending.onEnd(msg.reason); + } else if (msg.type === 'metrics') { + // Metrics from agent - save to database for dashboard graphs + const envId = wsToEnvId.get(ws); + if (envId && msg.metrics) { + if (globalThis.__hawserHandleMetrics) { + globalThis.__hawserHandleMetrics(envId, msg.metrics).catch((err) => { + console.error(`[Hawser WS] Error saving metrics:`, err); + }); + } + } + } else if (msg.type === 'exec_ready') { + // Exec session is ready + const session = edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) { + console.log(`[Hawser WS] Exec ready: ${msg.execId}`); + // Frontend doesn't need explicit ready message, it's already waiting for output + } + } else if (msg.type === 'exec_output') { + // Terminal output from exec session + const session = edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) { + // Decode base64 data + const data = Buffer.from(msg.data, 'base64').toString('utf-8'); + session.ws.send(JSON.stringify({ type: 'output', data })); + } + } else if (msg.type === 'exec_end') { + // Exec session ended + const session = edgeExecSessions.get(msg.execId); + if (session) { + console.log(`[Hawser WS] Exec ended: ${msg.execId} (reason: ${msg.reason})`); + if (session.ws?.readyState === 1) { + session.ws.send(JSON.stringify({ type: 'exit' })); + session.ws.close(); + } + edgeExecSessions.delete(msg.execId); + } + } else if (msg.type === 'container_event') { + // Container event from edge agent + const envId = wsToEnvId.get(ws); + if (envId && msg.event) { + // Call the global handler registered by hawser.ts + if (globalThis.__hawserHandleContainerEvent) { + globalThis.__hawserHandleContainerEvent(envId, msg.event).catch((err) => { + console.error('[Hawser WS] Error handling container event:', err); + }); + } + } + } else if (msg.type === 'error' && msg.requestId) { + // Error might be for an exec session + const session = edgeExecSessions.get(msg.requestId); + if (session?.ws?.readyState === 1) { + console.error(`[Hawser WS] Exec error: ${msg.error}`); + session.ws.send(JSON.stringify({ type: 'error', message: msg.error })); + session.ws.close(); + edgeExecSessions.delete(msg.requestId); + } + } +} + +export default defineConfig({ + plugins: [bunExternals(), tailwindcss(), sveltekit(), webSocketPlugin()], + define: { + __BUILD_DATE__: JSON.stringify(new Date().toISOString()), + __BUILD_COMMIT__: JSON.stringify(getGitCommit()), + __BUILD_BRANCH__: JSON.stringify(getGitBranch()), + __APP_VERSION__: JSON.stringify(getGitTag()) + }, + optimizeDeps: { + include: ['lucide-svelte', '@xterm/xterm', '@xterm/addon-fit'] + }, + build: { + target: 'esnext', + minify: 'esbuild', + sourcemap: false, + rollupOptions: { + external: [/^bun:/] + } + }, + ssr: { + external: [/^bun:/] + } +}); From f4a57ecfd31e70ceafb4f053fe12cce771f228fe Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 29 Dec 2025 08:40:56 +0100 Subject: [PATCH 05/30] proper src structure, dockerfile, entrypoint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff564a3..183c5df 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Dockhand + Dockhand

From ba05d16d791ad9a3545512553fb5404b0f72ac2f Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 08:55:55 +0100 Subject: [PATCH 06/30] cleanup .DS_Store --- lib/.DS_Store | Bin 8196 -> 0 bytes lib/server/.DS_Store | Bin 8196 -> 0 bytes lib/server/db/.DS_Store | Bin 6148 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/.DS_Store delete mode 100644 lib/server/.DS_Store delete mode 100644 lib/server/db/.DS_Store diff --git a/lib/.DS_Store b/lib/.DS_Store deleted file mode 100644 index cd8d4957070919f36d185ccfaeff80292438ec34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMTWl0n7(U5v8j87WxJZUuYU_gm4ny83ADw<%74@8anf{DJUJowL?St46_Fd>@8Imw*={I_$? z|Ihc&>De;I(4N=VGFHbJ6Y24(Qcc|*ny>etDNP6^6+}V)tdPptmSZ30{1p305M?0B zK$L+f15pN|4E*;QpflTV;$6;tu8sOA15pP4ml^QC4{>^YnhfbAr@^O#y5I^xw4B62 zqp{)-h{i*j4Cy4Nq@f1xN|d`Iykda6lRO^UB||#NDR*ZGFCPe3MtDO(usY2j56l^o zoJM_=fhYr0GvMFd%bCtJ<}t10{$9ykX0RM9_X358x!%Srq+o>e7v=>fyNItO-xAQy!tg8_N0$m!?tsZ zhllVJ!Dg0(dz$#$VzS&q2al;NRgCHDFeXu&Q7z9@`}zk4_sMBxPJuSxJ>r>;?d~;l zF4447<5G}YvW~qgXZZTQtYhVeZ8xpV&T3XRZ)u*P%Ce6>Sl)P)~F=$e=V06;dZf!|`kj~GVzi8On zcz;Y)?`GPc&zQ&ZrgvPa7sX(W_+nMwuhX7(IJ&=zu3Qvp9$J0bVpZMeYdUfU^?K;A zVx=ngxD>pig@&uw$a2q7f`Q`tTOikUO|sf+X&J*(bcwIi(kiQc1)hIpTPWOAE35rk zXLy)GbVFHNyQ&`am))+pUiXM$)0VD;Yd=D|L3c>Y8Qr4~{c_kT3()sKr7oMFJ0^ z87*iGp#3?+5GdPQ9@e*Fat2l=@@HQ^sBHqFKxPnjd z8NR@Ee24Gx18(3C{E3@Fg)m>J6BYUQEy8+XgRoQR7WxHEI3hU07!N=w zmfHc&7klA2jY`Wc{DcVS?_6%NM>lWTx^4QD+qGG|z~XbZMT5Pky`}o z-%B9C{cY6};6A`b31+YIN|YBBtF*Kvd#PZIG10GErHWfGk(Y{8FiHhym&+?eDix)Y zvx(Shk&06h0#;rdTPINoDrKBa#F`~h0cY0B8zm}6rJS>>oRlPm1x&0X)+JIwDdN3C z`Zac){m6b{Hwo#pFc)=Ljt0W_!`O_Sgm53u2#ap272Y0WacZyoT2a?{DHQLi@Y8g!k|vKEh>u65{?FLjBM9wFJpKiWue;zm+1n z1?=0g5cR6BY_V$O5HQ+wxBVt|{IJQnCB0$Spfn==HL4+JYCxS_ybo#v1E z>I?}^!#0dS7=g(V@X|+M8p~!!oZ(yV?~Yqv{}qIa%4yT9s-&vws{P49dnD;*+=A1V z$?x^J9@{pv<$fl+$IwU8N^QGsxf#RK^S*(h>7=r+&#*Lmq|Gi^n&aCh`G6#eQd)_O zjWx71#AA((E#vXnSW81a^&e^+AD6^=b!#^4P9HIcEc+yXd<0tnn>oRqQ)ORFr`XPY zsYrU?B0OKd4tx=%8P)PkwJ+5_uvbnibBgrP-NUY7Th1Om?+{HZH4Zt~m9wp#dEFcD z%h_gO$a2!k>|Dmo70isQx0+_sIIfed#>wSv)7)u0hBr-r%GF0*Z*0;~YQVAcZo5BO z%AonVmQrx^-8xNb6!i|8+Nqu37^HKv<}X_Mz^aC(jfu{#?YpkjD6{9xt(D~gilXHj zj~RNtcf`=#!TyYs(=E-g4)qlc$H=VFNtG?g{54z`UQ zWO+8mPP6COS$3X%%r3Dn*|+Q`cAfnWU^*nsKs9Qy5K9rqgJ?oCTF{1W?80s&u@8eV za2O6oaU5effhTbiPvJDq;90zcSMVy%;tjlw^SFR_@IEf%Q+$Rma1Gz#d;Eaw_yd39 zhA>T-FGPd|!V)1atP)lWO+vGrDW1#FjyNEA)N5-2xcmk&g<7e}B`Jw+$K< zpaN8Y3Qz$m@NosQ#14iZKbZ$o0V?q83fT9dzzu6+6X>4~4Bi3&dkDK>?!5%CSO8cP zn?OWh8dP9VHCqe~I^resYGM-@bkS@+G;h}IP}Fb7`Nh*kYamA|Kn0!^=*Dtn^?we3 z)Bit{xS|47;I9 Date: Mon, 29 Dec 2025 09:03:11 +0100 Subject: [PATCH 07/30] drizzle config --- drizzle-pg/0000_initial_schema.sql | 401 +++ drizzle-pg/0001_add_stack_env_vars.sql | 14 + .../0002_add_pending_container_updates.sql | 12 + drizzle-pg/meta/0000_snapshot.json | 2709 +++++++++++++++ drizzle-pg/meta/0001_snapshot.json | 2803 +++++++++++++++ drizzle-pg/meta/0002_snapshot.json | 2883 ++++++++++++++++ drizzle-pg/meta/_journal.json | 27 + drizzle/0000_initial_schema.sql | 401 +++ drizzle/0001_add_stack_env_vars.sql | 14 + .../0002_add_pending_container_updates.sql | 12 + drizzle/meta/0000_snapshot.json | 2824 ++++++++++++++++ drizzle/meta/0001_snapshot.json | 2924 ++++++++++++++++ drizzle/meta/0002_snapshot.json | 3008 +++++++++++++++++ drizzle/meta/_journal.json | 27 + 14 files changed, 18059 insertions(+) create mode 100644 drizzle-pg/0000_initial_schema.sql create mode 100644 drizzle-pg/0001_add_stack_env_vars.sql create mode 100644 drizzle-pg/0002_add_pending_container_updates.sql create mode 100644 drizzle-pg/meta/0000_snapshot.json create mode 100644 drizzle-pg/meta/0001_snapshot.json create mode 100644 drizzle-pg/meta/0002_snapshot.json create mode 100644 drizzle-pg/meta/_journal.json create mode 100644 drizzle/0000_initial_schema.sql create mode 100644 drizzle/0001_add_stack_env_vars.sql create mode 100644 drizzle/0002_add_pending_container_updates.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/_journal.json diff --git a/drizzle-pg/0000_initial_schema.sql b/drizzle-pg/0000_initial_schema.sql new file mode 100644 index 0000000..15c71a7 --- /dev/null +++ b/drizzle-pg/0000_initial_schema.sql @@ -0,0 +1,401 @@ +CREATE TABLE "audit_logs" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer, + "username" text NOT NULL, + "action" text NOT NULL, + "entity_type" text NOT NULL, + "entity_id" text, + "entity_name" text, + "environment_id" integer, + "description" text, + "details" text, + "ip_address" text, + "user_agent" text, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "auth_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "auth_enabled" boolean DEFAULT false, + "default_provider" text DEFAULT 'local', + "session_timeout" integer DEFAULT 86400, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "auto_update_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "container_name" text NOT NULL, + "enabled" boolean DEFAULT false, + "schedule_type" text DEFAULT 'daily', + "cron_expression" text, + "vulnerability_criteria" text DEFAULT 'never', + "last_checked" timestamp, + "last_updated" timestamp, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "auto_update_settings_environment_id_container_name_unique" UNIQUE("environment_id","container_name") +); +--> statement-breakpoint +CREATE TABLE "config_sets" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "env_vars" text, + "labels" text, + "ports" text, + "volumes" text, + "network_mode" text DEFAULT 'bridge', + "restart_policy" text DEFAULT 'no', + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "config_sets_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "container_events" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "container_id" text NOT NULL, + "container_name" text, + "image" text, + "action" text NOT NULL, + "actor_attributes" text, + "timestamp" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "environment_notifications" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer NOT NULL, + "notification_id" integer NOT NULL, + "enabled" boolean DEFAULT true, + "event_types" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "environment_notifications_environment_id_notification_id_unique" UNIQUE("environment_id","notification_id") +); +--> statement-breakpoint +CREATE TABLE "environments" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "host" text, + "port" integer DEFAULT 2375, + "protocol" text DEFAULT 'http', + "tls_ca" text, + "tls_cert" text, + "tls_key" text, + "tls_skip_verify" boolean DEFAULT false, + "icon" text DEFAULT 'globe', + "collect_activity" boolean DEFAULT true, + "collect_metrics" boolean DEFAULT true, + "highlight_changes" boolean DEFAULT true, + "labels" text, + "connection_type" text DEFAULT 'socket', + "socket_path" text DEFAULT '/var/run/docker.sock', + "hawser_token" text, + "hawser_last_seen" timestamp, + "hawser_agent_id" text, + "hawser_agent_name" text, + "hawser_version" text, + "hawser_capabilities" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "environments_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "git_credentials" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "auth_type" text DEFAULT 'none' NOT NULL, + "username" text, + "password" text, + "ssh_private_key" text, + "ssh_passphrase" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "git_credentials_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "git_repositories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "branch" text DEFAULT 'main', + "credential_id" integer, + "compose_path" text DEFAULT 'docker-compose.yml', + "environment_id" integer, + "auto_update" boolean DEFAULT false, + "auto_update_schedule" text DEFAULT 'daily', + "auto_update_cron" text DEFAULT '0 3 * * *', + "webhook_enabled" boolean DEFAULT false, + "webhook_secret" text, + "last_sync" timestamp, + "last_commit" text, + "sync_status" text DEFAULT 'pending', + "sync_error" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "git_repositories_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "git_stacks" ( + "id" serial PRIMARY KEY NOT NULL, + "stack_name" text NOT NULL, + "environment_id" integer, + "repository_id" integer NOT NULL, + "compose_path" text DEFAULT 'docker-compose.yml', + "auto_update" boolean DEFAULT false, + "auto_update_schedule" text DEFAULT 'daily', + "auto_update_cron" text DEFAULT '0 3 * * *', + "webhook_enabled" boolean DEFAULT false, + "webhook_secret" text, + "last_sync" timestamp, + "last_commit" text, + "sync_status" text DEFAULT 'pending', + "sync_error" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "git_stacks_stack_name_environment_id_unique" UNIQUE("stack_name","environment_id") +); +--> statement-breakpoint +CREATE TABLE "hawser_tokens" ( + "id" serial PRIMARY KEY NOT NULL, + "token" text NOT NULL, + "token_prefix" text NOT NULL, + "name" text NOT NULL, + "environment_id" integer, + "is_active" boolean DEFAULT true, + "last_used" timestamp, + "created_at" timestamp DEFAULT now(), + "expires_at" timestamp, + CONSTRAINT "hawser_tokens_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "host_metrics" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "cpu_percent" double precision NOT NULL, + "memory_percent" double precision NOT NULL, + "memory_used" bigint, + "memory_total" bigint, + "timestamp" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "ldap_config" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT false, + "server_url" text NOT NULL, + "bind_dn" text, + "bind_password" text, + "base_dn" text NOT NULL, + "user_filter" text DEFAULT '(uid={{username}})', + "username_attribute" text DEFAULT 'uid', + "email_attribute" text DEFAULT 'mail', + "display_name_attribute" text DEFAULT 'cn', + "group_base_dn" text, + "group_filter" text, + "admin_group" text, + "role_mappings" text, + "tls_enabled" boolean DEFAULT false, + "tls_ca" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "notification_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "type" text NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT true, + "config" text NOT NULL, + "event_types" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "oidc_config" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT false, + "issuer_url" text NOT NULL, + "client_id" text NOT NULL, + "client_secret" text NOT NULL, + "redirect_uri" text NOT NULL, + "scopes" text DEFAULT 'openid profile email', + "username_claim" text DEFAULT 'preferred_username', + "email_claim" text DEFAULT 'email', + "display_name_claim" text DEFAULT 'name', + "admin_claim" text, + "admin_value" text, + "role_mappings_claim" text DEFAULT 'groups', + "role_mappings" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "registries" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "username" text, + "password" text, + "is_default" boolean DEFAULT false, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "registries_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "roles" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "is_system" boolean DEFAULT false, + "permissions" text NOT NULL, + "environment_ids" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "roles_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "schedule_executions" ( + "id" serial PRIMARY KEY NOT NULL, + "schedule_type" text NOT NULL, + "schedule_id" integer NOT NULL, + "environment_id" integer, + "entity_name" text NOT NULL, + "triggered_by" text NOT NULL, + "triggered_at" timestamp NOT NULL, + "started_at" timestamp, + "completed_at" timestamp, + "duration" integer, + "status" text NOT NULL, + "error_message" text, + "details" text, + "logs" text, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "provider" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "settings" ( + "key" text PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "stack_events" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "stack_name" text NOT NULL, + "event_type" text NOT NULL, + "timestamp" timestamp DEFAULT now(), + "metadata" text +); +--> statement-breakpoint +CREATE TABLE "stack_sources" ( + "id" serial PRIMARY KEY NOT NULL, + "stack_name" text NOT NULL, + "environment_id" integer, + "source_type" text DEFAULT 'internal' NOT NULL, + "git_repository_id" integer, + "git_stack_id" integer, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "stack_sources_stack_name_environment_id_unique" UNIQUE("stack_name","environment_id") +); +--> statement-breakpoint +CREATE TABLE "user_preferences" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer, + "environment_id" integer, + "key" text NOT NULL, + "value" text NOT NULL, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "user_preferences_user_id_environment_id_key_unique" UNIQUE("user_id","environment_id","key") +); +--> statement-breakpoint +CREATE TABLE "user_roles" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "role_id" integer NOT NULL, + "environment_id" integer, + "created_at" timestamp DEFAULT now(), + CONSTRAINT "user_roles_user_id_role_id_environment_id_unique" UNIQUE("user_id","role_id","environment_id") +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" serial PRIMARY KEY NOT NULL, + "username" text NOT NULL, + "email" text, + "password_hash" text NOT NULL, + "display_name" text, + "avatar" text, + "auth_provider" text DEFAULT 'local', + "mfa_enabled" boolean DEFAULT false, + "mfa_secret" text, + "is_active" boolean DEFAULT true, + "last_login" timestamp, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "users_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "vulnerability_scans" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "image_id" text NOT NULL, + "image_name" text NOT NULL, + "scanner" text NOT NULL, + "scanned_at" timestamp NOT NULL, + "scan_duration" integer, + "critical_count" integer DEFAULT 0, + "high_count" integer DEFAULT 0, + "medium_count" integer DEFAULT 0, + "low_count" integer DEFAULT 0, + "negligible_count" integer DEFAULT 0, + "unknown_count" integer DEFAULT 0, + "vulnerabilities" text, + "error" text, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auto_update_settings" ADD CONSTRAINT "auto_update_settings_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "container_events" ADD CONSTRAINT "container_events_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "environment_notifications" ADD CONSTRAINT "environment_notifications_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "environment_notifications" ADD CONSTRAINT "environment_notifications_notification_id_notification_settings_id_fk" FOREIGN KEY ("notification_id") REFERENCES "public"."notification_settings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_repositories" ADD CONSTRAINT "git_repositories_credential_id_git_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."git_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD CONSTRAINT "git_stacks_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD CONSTRAINT "git_stacks_repository_id_git_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."git_repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hawser_tokens" ADD CONSTRAINT "hawser_tokens_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_metrics" ADD CONSTRAINT "host_metrics_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "schedule_executions" ADD CONSTRAINT "schedule_executions_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_events" ADD CONSTRAINT "stack_events_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD CONSTRAINT "stack_sources_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD CONSTRAINT "stack_sources_git_repository_id_git_repositories_id_fk" FOREIGN KEY ("git_repository_id") REFERENCES "public"."git_repositories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD CONSTRAINT "stack_sources_git_stack_id_git_stacks_id_fk" FOREIGN KEY ("git_stack_id") REFERENCES "public"."git_stacks"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vulnerability_scans" ADD CONSTRAINT "vulnerability_scans_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "container_events_env_timestamp_idx" ON "container_events" USING btree ("environment_id","timestamp");--> statement-breakpoint +CREATE INDEX "host_metrics_env_timestamp_idx" ON "host_metrics" USING btree ("environment_id","timestamp");--> statement-breakpoint +CREATE INDEX "schedule_executions_type_id_idx" ON "schedule_executions" USING btree ("schedule_type","schedule_id");--> statement-breakpoint +CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "sessions_expires_at_idx" ON "sessions" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "vulnerability_scans_env_image_idx" ON "vulnerability_scans" USING btree ("environment_id","image_id"); \ No newline at end of file diff --git a/drizzle-pg/0001_add_stack_env_vars.sql b/drizzle-pg/0001_add_stack_env_vars.sql new file mode 100644 index 0000000..8ee2010 --- /dev/null +++ b/drizzle-pg/0001_add_stack_env_vars.sql @@ -0,0 +1,14 @@ +CREATE TABLE "stack_environment_variables" ( + "id" serial PRIMARY KEY NOT NULL, + "stack_name" text NOT NULL, + "environment_id" integer, + "key" text NOT NULL, + "value" text NOT NULL, + "is_secret" boolean DEFAULT false, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "stack_environment_variables_stack_name_environment_id_key_unique" UNIQUE("stack_name","environment_id","key") +); +--> statement-breakpoint +ALTER TABLE "git_stacks" ADD COLUMN "env_file_path" text;--> statement-breakpoint +ALTER TABLE "stack_environment_variables" ADD CONSTRAINT "stack_environment_variables_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle-pg/0002_add_pending_container_updates.sql b/drizzle-pg/0002_add_pending_container_updates.sql new file mode 100644 index 0000000..ac712d1 --- /dev/null +++ b/drizzle-pg/0002_add_pending_container_updates.sql @@ -0,0 +1,12 @@ +CREATE TABLE "pending_container_updates" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer NOT NULL, + "container_id" text NOT NULL, + "container_name" text NOT NULL, + "current_image" text NOT NULL, + "checked_at" timestamp DEFAULT now(), + "created_at" timestamp DEFAULT now(), + CONSTRAINT "pending_container_updates_environment_id_container_id_unique" UNIQUE("environment_id","container_id") +); +--> statement-breakpoint +ALTER TABLE "pending_container_updates" ADD CONSTRAINT "pending_container_updates_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle-pg/meta/0000_snapshot.json b/drizzle-pg/meta/0000_snapshot.json new file mode 100644 index 0000000..c2004b5 --- /dev/null +++ b/drizzle-pg/meta/0000_snapshot.json @@ -0,0 +1,2709 @@ +{ + "id": "50905243-3288-41de-8cef-87b4e546d7cd", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/0001_snapshot.json b/drizzle-pg/meta/0001_snapshot.json new file mode 100644 index 0000000..c972fe5 --- /dev/null +++ b/drizzle-pg/meta/0001_snapshot.json @@ -0,0 +1,2803 @@ +{ + "id": "31d336d0-689e-4403-b49e-308e13df0014", + "prevId": "50905243-3288-41de-8cef-87b4e546d7cd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/0002_snapshot.json b/drizzle-pg/meta/0002_snapshot.json new file mode 100644 index 0000000..209f367 --- /dev/null +++ b/drizzle-pg/meta/0002_snapshot.json @@ -0,0 +1,2883 @@ +{ + "id": "eef8322a-0ccc-418c-b0f6-f51972a1850e", + "prevId": "31d336d0-689e-4403-b49e-308e13df0014", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_container_updates": { + "name": "pending_container_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json new file mode 100644 index 0000000..b439adc --- /dev/null +++ b/drizzle-pg/meta/_journal.json @@ -0,0 +1,27 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1765804022462, + "tag": "0000_initial_schema", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1766378770502, + "tag": "0001_add_stack_env_vars", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1766763867484, + "tag": "0002_add_pending_container_updates", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/0000_initial_schema.sql b/drizzle/0000_initial_schema.sql new file mode 100644 index 0000000..b04383a --- /dev/null +++ b/drizzle/0000_initial_schema.sql @@ -0,0 +1,401 @@ +CREATE TABLE `audit_logs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer, + `username` text NOT NULL, + `action` text NOT NULL, + `entity_type` text NOT NULL, + `entity_id` text, + `entity_name` text, + `environment_id` integer, + `description` text, + `details` text, + `ip_address` text, + `user_agent` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE INDEX `audit_logs_user_id_idx` ON `audit_logs` (`user_id`);--> statement-breakpoint +CREATE INDEX `audit_logs_created_at_idx` ON `audit_logs` (`created_at`);--> statement-breakpoint +CREATE TABLE `auth_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `auth_enabled` integer DEFAULT false, + `default_provider` text DEFAULT 'local', + `session_timeout` integer DEFAULT 86400, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `auto_update_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `container_name` text NOT NULL, + `enabled` integer DEFAULT false, + `schedule_type` text DEFAULT 'daily', + `cron_expression` text, + `vulnerability_criteria` text DEFAULT 'never', + `last_checked` text, + `last_updated` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `auto_update_settings_environment_id_container_name_unique` ON `auto_update_settings` (`environment_id`,`container_name`);--> statement-breakpoint +CREATE TABLE `config_sets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `description` text, + `env_vars` text, + `labels` text, + `ports` text, + `volumes` text, + `network_mode` text DEFAULT 'bridge', + `restart_policy` text DEFAULT 'no', + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `config_sets_name_unique` ON `config_sets` (`name`);--> statement-breakpoint +CREATE TABLE `container_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `container_id` text NOT NULL, + `container_name` text, + `image` text, + `action` text NOT NULL, + `actor_attributes` text, + `timestamp` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `container_events_env_timestamp_idx` ON `container_events` (`environment_id`,`timestamp`);--> statement-breakpoint +CREATE TABLE `environment_notifications` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer NOT NULL, + `notification_id` integer NOT NULL, + `enabled` integer DEFAULT true, + `event_types` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`notification_id`) REFERENCES `notification_settings`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `environment_notifications_environment_id_notification_id_unique` ON `environment_notifications` (`environment_id`,`notification_id`);--> statement-breakpoint +CREATE TABLE `environments` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `host` text, + `port` integer DEFAULT 2375, + `protocol` text DEFAULT 'http', + `tls_ca` text, + `tls_cert` text, + `tls_key` text, + `tls_skip_verify` integer DEFAULT false, + `icon` text DEFAULT 'globe', + `collect_activity` integer DEFAULT true, + `collect_metrics` integer DEFAULT true, + `highlight_changes` integer DEFAULT true, + `labels` text, + `connection_type` text DEFAULT 'socket', + `socket_path` text DEFAULT '/var/run/docker.sock', + `hawser_token` text, + `hawser_last_seen` text, + `hawser_agent_id` text, + `hawser_agent_name` text, + `hawser_version` text, + `hawser_capabilities` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `environments_name_unique` ON `environments` (`name`);--> statement-breakpoint +CREATE TABLE `git_credentials` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `auth_type` text DEFAULT 'none' NOT NULL, + `username` text, + `password` text, + `ssh_private_key` text, + `ssh_passphrase` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `git_credentials_name_unique` ON `git_credentials` (`name`);--> statement-breakpoint +CREATE TABLE `git_repositories` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `url` text NOT NULL, + `branch` text DEFAULT 'main', + `credential_id` integer, + `compose_path` text DEFAULT 'docker-compose.yml', + `environment_id` integer, + `auto_update` integer DEFAULT false, + `auto_update_schedule` text DEFAULT 'daily', + `auto_update_cron` text DEFAULT '0 3 * * *', + `webhook_enabled` integer DEFAULT false, + `webhook_secret` text, + `last_sync` text, + `last_commit` text, + `sync_status` text DEFAULT 'pending', + `sync_error` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`credential_id`) REFERENCES `git_credentials`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `git_repositories_name_unique` ON `git_repositories` (`name`);--> statement-breakpoint +CREATE TABLE `git_stacks` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `stack_name` text NOT NULL, + `environment_id` integer, + `repository_id` integer NOT NULL, + `compose_path` text DEFAULT 'docker-compose.yml', + `auto_update` integer DEFAULT false, + `auto_update_schedule` text DEFAULT 'daily', + `auto_update_cron` text DEFAULT '0 3 * * *', + `webhook_enabled` integer DEFAULT false, + `webhook_secret` text, + `last_sync` text, + `last_commit` text, + `sync_status` text DEFAULT 'pending', + `sync_error` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`repository_id`) REFERENCES `git_repositories`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `git_stacks_stack_name_environment_id_unique` ON `git_stacks` (`stack_name`,`environment_id`);--> statement-breakpoint +CREATE TABLE `hawser_tokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `token` text NOT NULL, + `token_prefix` text NOT NULL, + `name` text NOT NULL, + `environment_id` integer, + `is_active` integer DEFAULT true, + `last_used` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `expires_at` text, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `hawser_tokens_token_unique` ON `hawser_tokens` (`token`);--> statement-breakpoint +CREATE TABLE `host_metrics` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `cpu_percent` real NOT NULL, + `memory_percent` real NOT NULL, + `memory_used` integer, + `memory_total` integer, + `timestamp` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `host_metrics_env_timestamp_idx` ON `host_metrics` (`environment_id`,`timestamp`);--> statement-breakpoint +CREATE TABLE `ldap_config` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT false, + `server_url` text NOT NULL, + `bind_dn` text, + `bind_password` text, + `base_dn` text NOT NULL, + `user_filter` text DEFAULT '(uid={{username}})', + `username_attribute` text DEFAULT 'uid', + `email_attribute` text DEFAULT 'mail', + `display_name_attribute` text DEFAULT 'cn', + `group_base_dn` text, + `group_filter` text, + `admin_group` text, + `role_mappings` text, + `tls_enabled` integer DEFAULT false, + `tls_ca` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `notification_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `type` text NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT true, + `config` text NOT NULL, + `event_types` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `oidc_config` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT false, + `issuer_url` text NOT NULL, + `client_id` text NOT NULL, + `client_secret` text NOT NULL, + `redirect_uri` text NOT NULL, + `scopes` text DEFAULT 'openid profile email', + `username_claim` text DEFAULT 'preferred_username', + `email_claim` text DEFAULT 'email', + `display_name_claim` text DEFAULT 'name', + `admin_claim` text, + `admin_value` text, + `role_mappings_claim` text DEFAULT 'groups', + `role_mappings` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `registries` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `url` text NOT NULL, + `username` text, + `password` text, + `is_default` integer DEFAULT false, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `registries_name_unique` ON `registries` (`name`);--> statement-breakpoint +CREATE TABLE `roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `description` text, + `is_system` integer DEFAULT false, + `permissions` text NOT NULL, + `environment_ids` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint +CREATE TABLE `schedule_executions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `schedule_type` text NOT NULL, + `schedule_id` integer NOT NULL, + `environment_id` integer, + `entity_name` text NOT NULL, + `triggered_by` text NOT NULL, + `triggered_at` text NOT NULL, + `started_at` text, + `completed_at` text, + `duration` integer, + `status` text NOT NULL, + `error_message` text, + `details` text, + `logs` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `schedule_executions_type_id_idx` ON `schedule_executions` (`schedule_type`,`schedule_id`);--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `provider` text NOT NULL, + `expires_at` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `sessions_user_id_idx` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `sessions_expires_at_idx` ON `sessions` (`expires_at`);--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `stack_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `stack_name` text NOT NULL, + `event_type` text NOT NULL, + `timestamp` text DEFAULT CURRENT_TIMESTAMP, + `metadata` text, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `stack_sources` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `stack_name` text NOT NULL, + `environment_id` integer, + `source_type` text DEFAULT 'internal' NOT NULL, + `git_repository_id` integer, + `git_stack_id` integer, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`git_repository_id`) REFERENCES `git_repositories`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`git_stack_id`) REFERENCES `git_stacks`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `stack_sources_stack_name_environment_id_unique` ON `stack_sources` (`stack_name`,`environment_id`);--> statement-breakpoint +CREATE TABLE `user_preferences` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer, + `environment_id` integer, + `key` text NOT NULL, + `value` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_preferences_user_id_environment_id_key_unique` ON `user_preferences` (`user_id`,`environment_id`,`key`);--> statement-breakpoint +CREATE TABLE `user_roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `role_id` integer NOT NULL, + `environment_id` integer, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_roles_user_id_role_id_environment_id_unique` ON `user_roles` (`user_id`,`role_id`,`environment_id`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `username` text NOT NULL, + `email` text, + `password_hash` text NOT NULL, + `display_name` text, + `avatar` text, + `auth_provider` text DEFAULT 'local', + `mfa_enabled` integer DEFAULT false, + `mfa_secret` text, + `is_active` integer DEFAULT true, + `last_login` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE TABLE `vulnerability_scans` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `image_id` text NOT NULL, + `image_name` text NOT NULL, + `scanner` text NOT NULL, + `scanned_at` text NOT NULL, + `scan_duration` integer, + `critical_count` integer DEFAULT 0, + `high_count` integer DEFAULT 0, + `medium_count` integer DEFAULT 0, + `low_count` integer DEFAULT 0, + `negligible_count` integer DEFAULT 0, + `unknown_count` integer DEFAULT 0, + `vulnerabilities` text, + `error` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `vulnerability_scans_env_image_idx` ON `vulnerability_scans` (`environment_id`,`image_id`); \ No newline at end of file diff --git a/drizzle/0001_add_stack_env_vars.sql b/drizzle/0001_add_stack_env_vars.sql new file mode 100644 index 0000000..aa52b21 --- /dev/null +++ b/drizzle/0001_add_stack_env_vars.sql @@ -0,0 +1,14 @@ +CREATE TABLE `stack_environment_variables` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `stack_name` text NOT NULL, + `environment_id` integer, + `key` text NOT NULL, + `value` text NOT NULL, + `is_secret` integer DEFAULT false, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `stack_environment_variables_stack_name_environment_id_key_unique` ON `stack_environment_variables` (`stack_name`,`environment_id`,`key`);--> statement-breakpoint +ALTER TABLE `git_stacks` ADD `env_file_path` text; \ No newline at end of file diff --git a/drizzle/0002_add_pending_container_updates.sql b/drizzle/0002_add_pending_container_updates.sql new file mode 100644 index 0000000..f3c87a6 --- /dev/null +++ b/drizzle/0002_add_pending_container_updates.sql @@ -0,0 +1,12 @@ +CREATE TABLE `pending_container_updates` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer NOT NULL, + `container_id` text NOT NULL, + `container_name` text NOT NULL, + `current_image` text NOT NULL, + `checked_at` text DEFAULT CURRENT_TIMESTAMP, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `pending_container_updates_environment_id_container_id_unique` ON `pending_container_updates` (`environment_id`,`container_id`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..0aaf6ba --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,2824 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d7d12244-ddb1-4246-844c-56f6c903ea29", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..4a5d606 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,2924 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9dd11a39-a911-4c3f-9c2f-6920b14c2d96", + "prevId": "d7d12244-ddb1-4246-844c-56f6c903ea29", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..f99d580 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,3008 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "31bce98b-04c0-4e21-8cb0-49a67c345d87", + "prevId": "9dd11a39-a911-4c3f-9c2f-6920b14c2d96", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_container_updates": { + "name": "pending_container_updates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "columns": [ + "environment_id", + "container_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..868151f --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,27 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1765804016391, + "tag": "0000_initial_schema", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1766378754939, + "tag": "0001_add_stack_env_vars", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1766763860091, + "tag": "0002_add_pending_container_updates", + "breakpoints": true + } + ] +} \ No newline at end of file From 522154cd685bb3c543aba3fee2cd5799d7841964 Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 29 Dec 2025 09:08:29 +0100 Subject: [PATCH 08/30] ignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f32e31a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.DS_Store From 81fcc28d0b72c96c22f229af0848e46cfb0daa30 Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 09:27:27 +0100 Subject: [PATCH 09/30] Update LICENSE.txt --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 86472ee..148c985 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -123,6 +123,6 @@ under an Open Source License, as stated in this License. For licensing inquiries, commercial licensing, or enterprise features: - Website: https://dockhand.io + Website: https://dockhand.pro ----------------------------------------------------------------------------- From 53ca99ac77a79bcf62adba7643517222bf9efcfc Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 13:37:26 +0100 Subject: [PATCH 10/30] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 183c5df..e39b02f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,14 @@ Dockhand is licensed under the [Business Source License 1.1](LICENSE.txt) (BSL 1 See [LICENSE.txt](LICENSE.txt) for full terms. + + + Buy Me A Coffee + + + ## Links - **Website**: [https://dockhand.pro](https://dockhand.pro) From fcb36c46468c1867c1846a78c5bd47177dba96de Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 13:39:36 +0100 Subject: [PATCH 11/30] bmac --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..45f1422 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# .github/FUNDING.yml +buy_me_a_coffee: + - dockhand From 695acd922ecf99f801c3d57ad9f3efff521829ac Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Mon, 29 Dec 2025 13:46:13 +0100 Subject: [PATCH 12/30] bmac --- .github/FUNDING.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 45f1422..1d473b7 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ -# .github/FUNDING.yml buy_me_a_coffee: - - dockhand + displayName: "Buy Me a Coffee" + account: dockhand \ No newline at end of file From c60db2930cbaeb7be0cb56d61e5c76043e9cce63 Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 29 Dec 2025 15:29:28 +0100 Subject: [PATCH 13/30] compose example --- docker-compose-postgresql.yaml | 25 +++++++++++++++++++++++++ docker-compose.yaml | 13 +++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 docker-compose-postgresql.yaml create mode 100644 docker-compose.yaml diff --git a/docker-compose-postgresql.yaml b/docker-compose-postgresql.yaml new file mode 100644 index 0000000..a1fa941 --- /dev/null +++ b/docker-compose-postgresql.yaml @@ -0,0 +1,25 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: dockhand + POSTGRES_PASSWORD: changeme + POSTGRES_DB: dockhand + volumes: + - postgres_data:/var/lib/postgresql/data + + dockhand: + image: fnsys/dockhand:latest + ports: + - 3000:3000 + environment: + DATABASE_URL: postgres://dockhand:changeme@postgres:5432/dockhand + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - dockhand_data:/app/data + depends_on: + - postgres + +volumes: + postgres_data: + dockhand_data: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b83f7c4 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +services: + dockhand: + image: fnsys/dockhand:latest + container_name: dockhand + restart: unless-stopped + ports: + - 3000:3000 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - dockhand_data:/app/data + +volumes: + dockhand_data: \ No newline at end of file From cd6544aedb994430a578396b9ab603815c8a601a Mon Sep 17 00:00:00 2001 From: jarek Date: Thu, 1 Jan 2026 16:00:34 +0100 Subject: [PATCH 14/30] 1.0.5 --- Dockerfile | 2 +- docker-entrypoint.sh | 54 + src/images/logo.webp | Bin 9926 -> 0 bytes src/lib/components/CodeEditor.svelte | 117 +- src/lib/components/PullTab.svelte | 7 +- src/lib/components/StackEnvVarsEditor.svelte | 2 +- src/lib/components/StackEnvVarsPanel.svelte | 398 +++++-- src/lib/data/changelog.json | 20 + src/lib/server/auth.ts | 129 +- src/lib/server/db.ts | 2 + src/lib/server/db/schema/index.ts | 2 +- src/lib/server/db/schema/pg-schema.ts | 2 +- src/lib/server/docker.ts | 34 +- src/lib/server/git.ts | 22 +- src/lib/server/notifications.ts | 3 + src/lib/server/stacks.ts | 190 ++- .../server/subprocesses/event-subprocess.ts | 19 +- src/routes/api/environments/+server.ts | 1 + src/routes/api/environments/[id]/+server.ts | 1 + src/routes/api/stacks/+server.ts | 17 +- src/routes/api/stacks/[name]/env/+server.ts | 166 ++- src/routes/api/users/+server.ts | 10 +- src/routes/api/users/[id]/mfa/+server.ts | 22 +- src/routes/containers/+page.svelte | 16 +- .../containers/CreateContainerModal.svelte | 1040 ++--------------- .../containers/EditContainerModal.svelte | 957 ++++++--------- src/routes/login/+page.svelte | 7 +- src/routes/logs/+page.svelte | 3 +- src/routes/profile/+page.svelte | 32 +- src/routes/profile/MfaSetupModal.svelte | 177 ++- .../settings/auth/users/UserModal.svelte | 2 +- .../notifications/NotificationModal.svelte | 16 +- src/routes/stacks/+page.svelte | 120 +- src/routes/stacks/GitStackModal.svelte | 41 +- src/routes/stacks/StackModal.svelte | 242 +++- 35 files changed, 1889 insertions(+), 1984 deletions(-) delete mode 100644 src/images/logo.webp diff --git a/Dockerfile b/Dockerfile index 6f8aa0d..9a08239 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,7 +59,7 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh # Copy emergency scripts (only the emergency subfolder, not license generation scripts) COPY scripts/emergency/ ./scripts/ -RUN chmod +x ./scripts/*.sh 2>/dev/null || true +RUN chmod +x ./scripts/*.sh ./scripts/**/*.sh 2>/dev/null || true # Create directories with proper ownership RUN mkdir -p /home/dockhand/.dockhand/stacks /app/data \ diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ecbc9b2..3f00ec4 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -12,6 +12,60 @@ if [ "$(id -u)" = "0" ]; then RUNNING_AS_ROOT=true fi +# === Non-root mode (user: directive in compose) === +# If container started as non-root, skip all user management and run directly +if [ "$RUNNING_AS_ROOT" = "false" ]; then + echo "Running as user $(id -u):$(id -g) (set via container user directive)" + + # Ensure data directories exist (user must have write access to DATA_DIR via volume mount) + DATA_DIR="${DATA_DIR:-/app/data}" + if [ ! -d "$DATA_DIR/db" ]; then + echo "Creating database directory at $DATA_DIR/db" + mkdir -p "$DATA_DIR/db" 2>/dev/null || { + echo "ERROR: Cannot create $DATA_DIR/db directory" + echo "Ensure the data volume is mounted with correct permissions for user $(id -u):$(id -g)" + echo "" + echo "Example docker-compose.yml:" + echo " volumes:" + echo " - ./data:/app/data # This directory must be writable by user $(id -u)" + exit 1 + } + fi + if [ ! -d "$DATA_DIR/stacks" ]; then + mkdir -p "$DATA_DIR/stacks" 2>/dev/null || true + fi + + # Check Docker socket access if mounted + SOCKET_PATH="/var/run/docker.sock" + if [ -S "$SOCKET_PATH" ]; then + if test -r "$SOCKET_PATH" 2>/dev/null; then + echo "Docker socket accessible at $SOCKET_PATH" + # Detect hostname from Docker if not set + if [ -z "$DOCKHAND_HOSTNAME" ]; then + DETECTED_HOSTNAME=$(curl -s --unix-socket "$SOCKET_PATH" http://localhost/info 2>/dev/null | sed -n 's/.*"Name":"\([^"]*\)".*/\1/p') + if [ -n "$DETECTED_HOSTNAME" ]; then + export DOCKHAND_HOSTNAME="$DETECTED_HOSTNAME" + echo "Detected Docker host hostname: $DOCKHAND_HOSTNAME" + fi + fi + else + SOCKET_GID=$(stat -c '%g' "$SOCKET_PATH" 2>/dev/null || echo "unknown") + echo "WARNING: Docker socket not readable by user $(id -u)" + echo "Add --group-add $SOCKET_GID to your docker run command" + fi + else + echo "No Docker socket found at $SOCKET_PATH" + echo "Configure Docker environments via the web UI (Settings > Environments)" + fi + + # Run directly as current user (no su-exec needed) + if [ "$1" = "" ]; then + exec bun run ./build/index.js + else + exec "$@" + fi +fi + # === User Setup === # Root mode: PUID=0 requested OR already running as root with default PUID/PGID if [ "$PUID" = "0" ]; then diff --git a/src/images/logo.webp b/src/images/logo.webp deleted file mode 100644 index 421abdfeef7973d5afb18896fb1c0e4f5ca42ad8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9926 zcmV;%COO$sNk&G#CIA3eMM6+kP&il$0000G0000w0RS%n06|PpNMHv500FcKh=>?k zux*=`q?Ih&Rc+h0ZQHhOGdtV1ZQFL1ZJRG&zgQplu6_17H>vG1y(@8<7kQHQWP0SO;SqQP!vHQnh=idj|v8(F^W17ZBgh$%= zEPed<@!!XPZ=cDRvU9U%1*LI0}Pc9EQ3BTj33i!+2w0974K+pq{`u z#5Dww?SOFz?ghd-feC%UlrCUW3ox$%n7Mt--8-i590|6K9D7Ed{UX(Fk!_>MxJzW+ zArfy7$v1{2HidO|gXQ*tWfzYM){Kw;TVjT%Kw~^=7it@gdDJ1)9<+K;hfpg9CzFlB z;1~=|gAv#=Fdp;hoC--Vx_Z$w0a{TQrouvy$s=KM` zJvE^w=VmpdZ%ED8>f5{~^=(bSmNRF`hP-c7ceMI}vb1%`jcWgZ5n_How}s-d`l;f_ zfSrSQeBOO(`@Ef1qiqYtGooMv#cgs1syh(8tKt%wRj7AEY|}l|XQNQ3Qtpb!1y_qS zZHc7trusAvzEl@xdQz_(RyA1dmpMWWu1e@&>Sy40@-p@@PG5`3b=Xy1?=(g5t+aMf zv#aA=t)9;8q{hJUQFTq`7WF97*?gw@t!)JFfE6eo0qQ1;p;bQjekXWjQmy9;t{7)$ z^=;-9^}5GN>gmjj>ZF7QQ%+eP#kxxE4B!#LZG0M6Umzy;P6^+rUE-{({>faT9`@K* zy_NYu?URtM?23X75R>Wv?4%xY!li<*B=wp)IL=aPROSYCtH-wL^UT+3+XB3gf^A0m zFaXp}6jNI})&3}WP)d)e6XL9>hGwo*_jv54KFEBgb}0Z4E!e$krwkqtJkSYiEff=a zB=od8Ce9}6x6IkA){GMiyL;9BBDi-tO=_z+530K|tEqA)hcDIfne)^~ z3H5wW@vSPq18eOgJYG^ePO@rtax$A=FO@Rxo@aBs|Q?1 zeU<~xQJlI$Q9h4ywHr2UPU}YMJz{kGiTbp>He*$3H{0^Vsn^NdU-8dgrk$EqxvvLa zR{X79F_x)B8R&-_lRA!iWPOB9hfKDUfXjDe{-Yx@GFOe}Tq@YYH4@VEN&#YXG#_ z>k}HwYx!W^`HE*3;-5(O`C-#?8dYmtI-bTMZNpxF@x>Qk{p{~D&7W2&Y+W&sH_mdb0W-K z9e(ZpRQ#qswc7D&W7r$j%Mq|;CAG(^k{GrQ0&5@f`?((ix1H)XadSk#so#DLvg9`%YJiZ13U0VI9IHY?@$50ML@MfyAYz*9!$f(~@%`Vq z^`Op(y8iV)Ef7N<-?KF$b~^mI0hPRgPoK~k!7f;KisA<~34N??2&)<7yEa*pTXa759q(zJ!gNSdn5l@^$`9={g3}Y ztN;6N?!UkPOFyyS^S{b||NRX9qJP8u|M>v_|G-V;-|QcezX&`c`?vNV+W+jn==oCf zhqON()T;CI`DfVA&VO(H(ENt`$NbOvuMhYEepCE|`{&IsD1L?J1?r#B|K5Lu{EzsK z`0x0Bh5do{0RBDwFa5WcCy0mHzu$hbd;xzL{;B@o`=|I1+MkVY^*_Y^PWyWN)BIQb z-}0Z8Uts^o|D*rQ{_Flr-pkjA?vJpa>gW6K&-4AkSm}Mc!WJD+^8rRSvk~CBsn?an zSuK4S^TQ6K8swNV3}O&~_^tU<8LFB)b|Wwb14On(p*Tc=-^!@3Qx#@^9i2YcX7x`FUM{h-#NLy>uDX702?9Co7l@I!4c=N z!g>@b;w4N*M5FdxjPcPFOjHH8Kne`IoRT3%)zPgy#L`5r{tPrGh?VB+x}6mM`@l>h zPO1JkCKhL2i>+2H!B(cYBwV3Fc-RL`joBr%yH~U_@&SUSm|vm7NUo+U_B;4PC6gf9 z(9p`Q&BGG<1Yz$uS)>e_+9uNeCC`^0S7=7AgGcOrBG`%ApqWC}WSb|T3`WGN4%A70 zoIAvdz>ZdDJJS4hlo8!mP@xrs;i}np(Oplc%K4z*fZSq6ml=bou2;jXwD~1n<`%wn zZ9z+o*acyg;8Ue#; z)}~9sH2|9e0=0EE3L=Aw(pmUFEKpv+gG1NHm|<1>g)2k)mDS48RrsHT+h2(Q3)t96 zvtiJEB_zE$VzcNE#D)%Khs{KDEY(!WmC+blU(1zt+^^ya4R@Yg>K+5YDW}Ad&_%y7 z-kxGVcChr_KZBeFaTA-g!E72G`<*;juG(6&n~SB2<$$Trucp%!aN6IBVUMsL#ikQrnt3GJn-&q6F|!0@3^zSI$$Ck8n4@Jq!?z?Na3iLE2JjY;ap!Zsp+}dnSFrf%S5Q7N`Y_!4)>N~*hl<#op zy8r0nYwJTCY#b!MyXaU~c&^v7ejcC>{?B<6Y(D-;6i1t;9UOYQ+6$VwgN3S>n{$N| zsAc2?Yf!IwRRatmM!YDDr)XyY3h$rvM?Sd?XaVNyhO}x~6a}Ka^;~AftY>$>!oe>4 zJDEz1oNsEpYCv&)ugQm|Af=PSsTuq%mK7^K+{;c|qk%1N$SV@WpiSFhT)OA_R%v0T z;&*A#5L5IcId^PUa!;OQr^5i45f=2T+(rK5(hjd%8JaQAK{05x8Nxp_62Kksf9d_? z<J5>*s)oK1IG`qr6<|sCwbV%kB_i5NR=6G8;W27uw8ji3?0_I;4MBsJiN?hV37)5IU%?ig(Z)jVKwQ5I1f4xEIaK10La{i5YBx6kq0 z@X?)J{v|jb$?OM!Q<;&&naC}KcrH5`N)1ek%#8N3gcpDn$LEhRJJ9em|K#6!(IV@+ z#uEMrFUv&9%YD{saJ3>hF(mNllA8cMqD*z*h4gAX-uu%*hk+B_Wp=f_xEs+)IJ}VM zQh?qK%PdzkYKZ^%hbozVwEq($nV$nyK7U$&hQnX)mrgvOG-0ShgUv*gsw!v5Dyh(m zgq0Sj12K36eiLNs0YngoW?r#`7{hCa)V~zDA5?t@x;|6)I+)g_N$F7mRrVa;$N-i< zgX|Gi46AZgp$=(PR;97Vig5NaPc0^2E&efnR~%TV1E3b-o_XYd@r(0#(eUW_%z%hn z)9vkghJZC42PLziPalV&fPAJ7_GG;icCiZ=ae4*He>oA4GkPIX<|Exwv1}6X zL}GAseEwpR-Ji&hHv3^>)L|IP55W2jKF7g1?Koa?RJ>K6;%pb#g!sDse2AFSpdG*S z`pbyx4<|W-X{h0D;s2hyyXUe-yFB(#TdGha*D0MYrESvFTQGlE@AQCv|J(0F!7qyT zf2yYf;kcf%O3moBQAm6Y8w~redN?z^8$!LuCo3Xto2ly}h?TlAUtIHccMJYaVFQ-& zwTJ4p07N`pD_jqyC*q^W#}QpYdBKq%kM-S@@@^PvwtVoJ8&Pmzh6wf#k|aQ6b}DKI z7E7@?-1J-{qL-L)IM2XW7W+~00fNFjr!jsofBkOH{|0aW_#(o8HNpWOVYuaQ4e1sg zBPuj|O$O;yc(!SwaZw&{BtCqJYVU`tBE>yJP;)+{vCB2jQ}@1U6)!`7iEEi>{p-Da zu5Kuu%_ztPuw?h_+|-gtO5tOIyH85%{@lSTW@s1j3YFK(@4a@0{C$$(3}3kvwd|Ly zZe}q{iyB73F?UmC16IW7werb+HTV=fIo5S$tZVYry^Y6ud*FOU_s%5h6aBVDnPv4R zspj-+YATBtf?AF+(Ws)|DnQ0j;per?|Qw65+C;KCx7!H_BdwhAM<9xBY(3ZKhQc| zGY{35MuBMDmQGY>Efeh|=G)oeDT8VAelW~+)6P7`+ijtAS0;iEd847CrL?Mn$kQGB zOX<87*H@shPk>z04pf3vHkYe(6Y*ZsR-kG*LIQNd!SeW?6(%7#3Byl{?eYGzPZ?dI z6TkTuSo*56b<&;Hp42kKodBm_=oBM4F#mAb1(pTyxP1H52TJhdX>;!~ok+Z-2Zpgr z1hL)d4!O;ziS#=AqlU`D%IbKYntIZuW z{8~>xy|?6NGb__S`X+yGtz=$_qw)%En}-h8E7tT>K?1nVQt{O*i27c9@CzLGWh#I9 z5ojJEyf&JF7|{QI(+7Ozr2W!mWiZ^>SY1DiY`NH_uBd=s*1c}TW^9NjVM|S*j2+U& z)}@en6*@{^K9Rg!n&{yUaY8fcE~E^JQr+9VL6z{rUm!WC)I5 zcutpplv%?w4gTzRp1>~W>$@J*gR7e~=#50QkHu@utgP*qh}aTYZ)?1R3WM2W(bLUJ zuhydUWKy%R;?Ex|dBhjM_*#FieF>K(F z?3Fxyqv>TIy~+R$QhyH|Q6cUOO`uRw0xQH!O#*shkuG`G;UeAD5+7v5cLd#h z>e&v#kcV%O2AI&i#oEjt9rDyy`Gs$tKZ#-O*PyMG!-(1sH`tG34n;7qA6Bh&OL@#S z6s$%jJ`HCa111twpi58_^rxN42OB0TP*)`y)QvJrQB(wec!yTa+=J0>#-?_&g{|V% zf@E5%nG1E;+T&f#JmDl8v06}tpJY{4iMP=+kc(WAy=UzFoaNf1xyvbO=gt4WpC+he z@BM@p8G|Hg_$ANa(($lx}${Bss+mJ7~W3D7-2#$h}nmq0`6j|C(hN(dcE1wxUQoPD@DcD@Q;Hv7x%h-vvi2lg7}}I9Leg&<9ktJ+Sex|d z#sHc?1zW37n7b-lIGT+9XMJ< ztx6?$2M1GviNiJY!<`aB{*ig*%f4)SlV>rJuN-=km>zqR({0=t2tqhxo^ti?@-9U% zGhHVGM0K(q8zaaoqdn6x*U75U&(^WWAtoGBhcTbx3@h0hLPAxS&dW^6 z+z$ly?DEuYzcrwpG3c2K69{|6QL75>UUXYgfpV6r1a@#~+7t?Ok^-6C_QGmKY~^A!+A`=TaK zcQWr&7>$O%C{<|U0iSX$s`_KZLudO03@(*53rVp}0)GC}4{FM}#OF+veR6_dr_B1K z7b^FUreMis*acpuE!vVC=f@c5q-05>snB1qA^-^-ecK)&o6}chY0FBpZMu1K_nz<| zd<;SlmgM`Z%LPv9w|Uqlc~7@cTT4`AMm&=^^oW^hW~7XB+BxL1N(~_Lh;H?U{~RDNl(O1ZQHGozYDF(vCUDEK}YQ||W=`F)i?fx(u3XgVS>%NmBy(_G)x z&HXdaR(w;T=i>-i0-Ti(J9!lkF09ey*Iviee9&d1#wJm%RAzNkZtp-p3R1)k&xO^M z&^?2q$?`E>;$X6cJpcOhJ55E5_j|K6kFk~T23OAJ73UIhrPMY^RlrDs0`7dsQUbO* zvO-oouF8j!R02r+at+W_K+M{&u`F{1Pg|Wb>?A2V+~t%R5E)5?urudqRFYe%)+5?@ z+$6eNfF+_#|Csmjw*gXnv`a^r^SEl9P83W{j)st#4M*uz6qtN7C5h7 z@byORGPVRz*k6-mh6(;^zml|#j}^*@eOC8c7esHai48AXa1Lb~HwFeET+`KdWT;~n zRT>}xiX7~>S9j+0!M`=QiJxt$UbRUMJBq2G@B0lCIpW5VVl~s2NEu0s*JF*aJI_e6 zgl`G8SQWrCEx;&E6_<=0P$PKz#POmhF%+NObS|;yxy0ZHjpyqO3lD-0?q`Pb-F$RE ztUbB>BmS$dajnG^`7Q6aYfp57=0dMaGBA_ zI?B|-i-a>^7)tJN{Ui8!i)gt6#|XGA_mZ!YR8HYl9WH`}aWw`g_5UzbcEvCZoy zEDAZEtiEfY_NttpWLz_D>2ZvNTcXJmi4fy$ZY{Bnsr{a1w9|iBH%9<^nCb$?G(3I+ z;_aPvy%}ws!()dn!WYulRl0)F>nVfKNxz;Yh3I%nf!7ub_gLjt-#)!c`ih)#FRe9u z89x2JyguY2=amnk=;>BOx#&V~TyRB>CZ@edl(TS<& zvBj!kQOIu7iffl6nCg95Yf0**$pJcR4l-P@tPY0Z~IaLtv zc8i?`ZjyJa-OBjnyTPpNm}zd<%7X$jUGBiHk#qkhONC~K<4tLdlJUqpZ)@G|eUceK zxA9ZN^wsgGJR*|^yx3G+OVUt&oxntJz^+f|u4LXK>^V9xpRfqE4!my3spEcr`0S>&h zUm%4SeE-uFEQV&QDkyvJ-mqCRQjw7R%-_{|C>R{iWzWx39>}bO(2xdt4$5JpUzp$^ zY4^Q|%Y1h(Vtex&oD%&}R%OQ4YEDpjHVppqKHV;{Tv_#qN5l}P4&Qee?ZJ0oAEP#) z++Vx??A^|nr~Ykv8(}SDDvbUj_#7`jk^=knPd$oRuzGtyMq+Qlo}H+z~H_Qe-fi?lu-@gu6j#m%p>e45KMgA`+PV}wZ(vY0?cV&@W?l7&|ImE+8Y_7Kqr zuD~25t4ibCXcc~dS$(`K)BFN#5Cb_crvDP(oJGSg8MUo8feH89cp=3$>ZjigE75V& z7Qy5Na!gc9L;N>kT8BS%&chg#zhe1c)wYL2TVG2w5yD$m?w_$S{%T;-6xe{k3ElAH zi4WZ2`5G~eX;ep52M=V6&WQqTw9Zs_XvvqB3*PEff6vPAeAh}so)c;PREu(bOuWT; zqyix%%LuiIM*`l9oI zhq)SQo!rAEW9uwDF!vzj?YY0|U52%4 z;8VFEe|56FTMCxvCzqD`)n^3Y&9P=L*)7$vSfs1g;+hUZ^U4E;X}Jta<%r#C`P zgXGNvu2%|xjKocs1wZj&(^#Ovv_C)&c#x2vaf4qsP|ES}iT%$97k^~ZDA2b3kwijt`l3Q><%pQ;p zfXG#moL#;>5`uS@xD+aqm>>?X5e|N0FL?2z-8}BNqV-##7Ju0BG4fjT<(DrGC>80) zw;(PV;lt>t;XPm;^+=rp_ywf+kSha36-(sypc43{D%$wj=nlN7$1`|E;^HVQj%hY; zF4RdfTCzG($tk%5#CO=j2$Kq4&C9sLw|?}G7w~PA zjnZW|H|8vm7h)A-lB*Bu7t03hOYXK++G>6yijOu<&+ps)?#ih&h9-^oKmdQ?b-Yh|OWP@i%~4#^iP{7Jj*~?w#pO4$SxH zvxw}J*_RUDJX_p!;raU_Z4#LQ`ElyR;OtZSDOm2Dk08x%Dj1-R#B`!cl+cwWJ>9 z$$%rv<+v2Xi{n-!9D6i+)A4ou8|l`hyrlub@xd@F!-GiAFAF|?RE^YaG^J zeEcpY^7OE25%@jJR)-$IZrzuwZ$3K6!LT70IwkBY=u`%Je6q7m?% z#8jnP_otuq_w!YE&zlqH)I;OW?8CemQw+6WR}_z5*Rr@O&x>bR&dr(0^vTD zT*;UZZ{A7k6bZ|a)`g;AL?v{sCkB27EoE<(J`3Qyl|ko;?h&pq4K&G>+A>apaCY?s z|0d(#+NcBWsRbs+(Xuo~LKNr}6*0E!>knn-w98al1T5S|zAtY3Jk?h*k}wsp2E-ok zzODRH)uHwod385N96loZ%92`B`B?b!csmk;hxq~m3_tj5SGmAvp^Nav zHTt0$@CT=j$JQ*n$T+NE>~%oMe&RsF>g)xzD}!G+cu>J(5FHnR*5Ne46XJ9l-k&zq zMl193e_Xcf2K0=C4QZ&=C0sv$N?H|;t0dwy2ZN9w^}yVfOO!fVYYrhPIo)+PdaiZ2 zw*`WlOgJi!axg|wpXWGV=Q`B%Lx$NOdSFs-4L9KaB>#!T3eC6_%`PEK{4H?Uq1Z3j zV;OS%MpXOaor(IEl-X%UcLwxG%uAMO_)stYW#96@?Z?tt>%;z1V&$M`={12-_*6~k z4{Wf!5VRX!8|Tj-w+*DI0G-vBhd&^FusitrrFcQ$jnAvT_R1jBp%uWH)oF(lwI-@e z3yG%%Z$o2OfN(j#`_&1UP8rB{sj>Yr?yBom-LG7&#G_0XD}B9kRoWpbjjqi|pev_U zU)lQN?%~VMoekEmwcJ;#s`OUxEbT9MlZB$(mV^Le#ndxklK0}b^Os{koWPSxX@RJ2 zRg0k_@k#@DP~Q!g&7qF+$+)6aJpNn7E{u}qFm$TU~q#Y2KI6j~Jdck#?+Pn>n7WjjU&_#Xcm{DPhqea^Z zgYOU+J&m{;C@Sl!o>WdB z_#LW$!+V#e7181WL~5|MANn-LC{`4$oL_V^M*r3rRF6*P@3T57iKkpy93m zo1bto>@CCyKKS0nrK3SwYP#kNb`fS=Wik>No5?4^Ce&^a&!9|+Sry!Z&%ho}ZxV-d zEhxuTr=MQEg3oN8o(I_H6GKO-M$Cih`dG^p0rvZAa+eH?XQu`xWx*k26;!Ej-QQtq zT#%rYtN{;m5bqF_fM)u2T>6-4eBW@2{3QN&n)OmV#l^C2ZQp}ABIjEy%byieH4p9s z-{5>v>@q+2o`WGa?|bx_`nnpAIq|;b>n0z?8r<3@`d{~%8^XHAYFPU%RW*nB9z{dw zEcO!lxxFG~fd_>6{(s!9=EI+H_NItCIu(&>#`nZa0u3d-l2w5ONk_tbvcLcU00000 E0BGZP2mk;8 diff --git a/src/lib/components/CodeEditor.svelte b/src/lib/components/CodeEditor.svelte index f49406f..4cac179 100644 --- a/src/lib/components/CodeEditor.svelte +++ b/src/lib/components/CodeEditor.svelte @@ -3,11 +3,51 @@ import { EditorState, StateField, StateEffect, RangeSet } from '@codemirror/state'; import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, gutter, GutterMarker, Decoration, WidgetType, type DecorationSet } from '@codemirror/view'; import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; - import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching } from '@codemirror/language'; + import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, StreamLanguage, type StreamParser } from '@codemirror/language'; import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete'; import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'; + // Simple dotenv/env file language parser + const dotenvParser: StreamParser<{ inValue: boolean }> = { + startState() { + return { inValue: false }; + }, + token(stream, state) { + // Start of line + if (stream.sol()) { + state.inValue = false; + // Skip leading whitespace + stream.eatSpace(); + // Comment line + if (stream.peek() === '#') { + stream.skipToEnd(); + return 'comment'; + } + } + // If in value part, consume the rest + if (state.inValue) { + stream.skipToEnd(); + return 'string'; + } + // Variable name before = + if (stream.match(/^[a-zA-Z_][a-zA-Z0-9_]*/)) { + if (stream.peek() === '=') { + return 'variableName.definition'; + } + return 'variableName'; + } + // Equals sign - switch to value mode + if (stream.eat('=')) { + state.inValue = true; + return 'operator'; + } + // Skip anything else + stream.next(); + return null; + } + }; + // Docker Compose keywords for autocomplete const COMPOSE_TOP_LEVEL = ['services', 'networks', 'volumes', 'configs', 'secrets', 'name', 'version']; @@ -453,6 +493,9 @@ case 'sh': // No dedicated shell/dockerfile support, use basic highlighting return []; + case 'dotenv': + case 'env': + return StreamLanguage.define(dotenvParser); default: return []; } @@ -542,6 +585,13 @@ // Track if we're initialized (prevents multiple createEditor calls) let initialized = false; + // Debounce timer for marker updates (prevents flicker during fast typing) + let markerUpdateTimer: ReturnType | null = null; + const MARKER_UPDATE_DEBOUNCE_MS = 300; + + // Track last applied markers to avoid redundant updates + let lastAppliedMarkersJson = ''; + function createEditor() { if (!container || view || initialized) return; initialized = true; @@ -551,12 +601,14 @@ : [dockhandLight, syntaxHighlighting(defaultHighlightStyle)]; // Build autocompletion config - add Docker Compose completions for YAML + // Note: activateOnTyping can interfere with key repeat, so we disable it + // Users can still trigger autocomplete manually with Ctrl+Space const autocompletionConfig = language === 'yaml' ? autocompletion({ override: [composeCompletions, composeValueCompletions], - activateOnTyping: true + activateOnTyping: false }) - : autocompletion(); + : autocompletion({ activateOnTyping: false }); const extensions = [ lineNumbers(), @@ -594,18 +646,25 @@ extensions }); - // Custom transaction handler - this is SYNCHRONOUS and more reliable than updateListener + // Custom transaction handler - applies transactions synchronously but defers callback // Based on the Svelte Playground pattern: https://svelte.dev/playground/91649ba3e0ce4122b3b34f3a95a00104 const dispatchTransactions = (trs: readonly import('@codemirror/state').Transaction[]) => { if (!view) return; - // Apply all transactions + // Apply all transactions synchronously (required by CodeMirror) view.update(trs); // Check if any transaction changed the document const lastChangingTr = trs.findLast(tr => tr.docChanged); if (lastChangingTr && onchangeRef) { - onchangeRef(lastChangingTr.newDoc.toString()); + // Defer callback to next microtask to avoid blocking input handling + // This allows key repeat to work properly + const newContent = lastChangingTr.newDoc.toString(); + queueMicrotask(() => { + if (onchangeRef) { + onchangeRef(newContent); + } + }); } }; @@ -615,7 +674,6 @@ dispatchTransactions }); - // Push initial markers if provided if (variableMarkers.length > 0) { view.dispatch({ @@ -625,11 +683,16 @@ } function destroyEditor() { + if (markerUpdateTimer) { + clearTimeout(markerUpdateTimer); + markerUpdateTimer = null; + } if (view) { view.destroy(); view = null; } initialized = false; + lastAppliedMarkersJson = ''; } // Get current editor content @@ -656,11 +719,35 @@ } // Update variable markers - this is the key method for parent to call - export function updateVariableMarkers(markers: VariableMarker[]) { - if (view) { - view.dispatch({ - effects: updateMarkersEffect.of(markers) - }); + // Debounced to prevent flicker during fast typing + export function updateVariableMarkers(markers: VariableMarker[], immediate = false) { + if (!view) return; + + // Check if markers actually changed (compare by content, not reference) + const newJson = JSON.stringify(markers); + if (newJson === lastAppliedMarkersJson) { + return; // No change, skip update + } + + // Clear any pending update + if (markerUpdateTimer) { + clearTimeout(markerUpdateTimer); + markerUpdateTimer = null; + } + + const applyUpdate = () => { + if (view) { + lastAppliedMarkersJson = newJson; + view.dispatch({ + effects: updateMarkersEffect.of(markers) + }); + } + }; + + if (immediate) { + applyUpdate(); + } else { + markerUpdateTimer = setTimeout(applyUpdate, MARKER_UPDATE_DEBOUNCE_MS); } } @@ -693,12 +780,11 @@ }); // Update markers when prop changes (backup mechanism, parent should also call updateVariableMarkers) + // Uses the debounced update to prevent flicker during fast typing $effect(() => { const markers = variableMarkers; if (view && markers) { - view.dispatch({ - effects: updateMarkersEffect.of(markers) - }); + updateVariableMarkers(markers); } }); @@ -706,7 +792,6 @@

e.stopPropagation()} >
+ + +
+
+ + Dockhand + + ← Back to home +
+ +

${title}

+ +
+
${escapeHtml(content)}
+
+ + +
+ +`; +} + +// Read the source files +const licenseContent = readFileSync(join(ROOT_DIR, 'LICENSE.txt'), 'utf-8'); +const privacyContent = readFileSync(join(ROOT_DIR, 'PRIVACY.txt'), 'utf-8'); + +// Generate HTML pages +const licenseHtml = generateHtmlPage('License Terms and Conditions', licenseContent); +const privacyHtml = generateHtmlPage('Privacy Policy', privacyContent); + +// Write to webpage directory +writeFileSync(join(WEBPAGE_DIR, 'license.html'), licenseHtml); +writeFileSync(join(WEBPAGE_DIR, 'privacy.html'), privacyHtml); + +console.log('Generated legal pages:'); +console.log(' - webpage/license.html'); +console.log(' - webpage/privacy.html'); diff --git a/scripts/patch-build.ts b/scripts/patch-build.ts new file mode 100644 index 0000000..e10ecbc --- /dev/null +++ b/scripts/patch-build.ts @@ -0,0 +1,575 @@ +/** + * Post-build script to fix svelte-adapter-bun WebSocket issue + * The adapter calls server.websocket() which doesn't exist in SvelteKit. + * + * IMPORTANT: Terminal WebSocket logic is shared with vite.config.ts + * Core functions like resolveDockerTarget are defined in: + * src/lib/server/ws-terminal-shared.ts + * + * When updating WebSocket terminal handling, update the shared module + * and this file will use the same logic at build time. + */ + +import { join } from 'node:path'; + +const BUILD_DIR = join(import.meta.dir, '../build'); + +async function patchHandler() { + const handlerPath = join(BUILD_DIR, 'handler.js'); + const handlerFile = Bun.file(handlerPath); + + if (!await handlerFile.exists()) { + console.error('handler.js not found'); + process.exit(1); + } + + let content = await handlerFile.text(); + + // Replace broken server.websocket() call + content = content.replace( + 'const websocket = server.websocket();', + 'const websocket = null;' + ); + + // Add WebSocket upgrade detection before ssr handler + const ssrIndex = content.indexOf('var ssr = async (request, bunServer) => {'); + if (ssrIndex > -1) { + const upgradeCode = ` +var handleUpgrade = (request, bunServer) => { + const url = new URL(request.url); + const isUpgrade = request.headers.get('connection')?.toLowerCase().includes('upgrade') && + request.headers.get('upgrade')?.toLowerCase() === 'websocket'; + if (!isUpgrade) return null; + + // Handle terminal exec WebSocket + if (url.pathname.includes('/api/containers/') && url.pathname.includes('/exec')) { + const pathParts = url.pathname.split('/'); + const containerIdIndex = pathParts.indexOf('containers') + 1; + const containerId = pathParts[containerIdIndex]; + const shell = url.searchParams.get('shell') || '/bin/sh'; + const user = url.searchParams.get('user') || 'root'; + const envId = url.searchParams.get('envId') ? parseInt(url.searchParams.get('envId'), 10) : undefined; + if (bunServer.upgrade(request, { data: { type: 'terminal', containerId, shell, user, envId } })) { + return new Response(null, { status: 101 }); + } + } + + // Handle Hawser Edge WebSocket + if (url.pathname === '/api/hawser/connect') { + if (bunServer.upgrade(request, { data: { type: 'hawser' } })) { + return new Response(null, { status: 101 }); + } + } + + return null; +}; +`; + content = content.slice(0, ssrIndex) + upgradeCode + content.slice(ssrIndex); + } + + // Modify handler to check for upgrade first + content = content.replace( + 'return ssr(request, server2);', + 'const upgradeResponse = handleUpgrade(request, server2); if (upgradeResponse) return upgradeResponse; return ssr(request, server2);' + ); + + await Bun.write(handlerPath, content); + console.log('✓ Patched handler.js'); +} + +async function patchIndex() { + const indexPath = join(BUILD_DIR, 'index.js'); + const indexFile = Bun.file(indexPath); + + if (!await indexFile.exists()) { + console.error('index.js not found'); + process.exit(1); + } + + let content = await indexFile.text(); + + const wsHandler = ` +import { existsSync as _existsSync } from 'fs'; +import { homedir as _homedir } from 'os'; +import { Database as _Database } from 'bun:sqlite'; +import { SQL as _SQL } from 'bun'; +import { join as _join } from 'path'; + +// Database connection (supports both SQLite and PostgreSQL) +let _db = null; +let _isPostgres = false; +function _getDb() { + if (!_db) { + const dbUrl = process.env.DATABASE_URL; + if (dbUrl && (dbUrl.startsWith('postgres://') || dbUrl.startsWith('postgresql://'))) { + _db = new _SQL(dbUrl); + _isPostgres = true; + } else { + const _dbPath = _join(process.cwd(), 'data', 'db', 'dockhand.db'); + if (_existsSync(_dbPath)) { + _db = new _Database(_dbPath); + } + } + } + return _db; +} + +async function _getEnvironment(id) { + const db = _getDb(); + if (!db) return null; + let row; + if (_isPostgres) { + const result = await db.unsafe('SELECT * FROM environments WHERE id = $1', [id]); + row = result[0]; + } else { + row = db.prepare('SELECT * FROM environments WHERE id = ?').get(id); + } + return row ? { ...row, is_local: Boolean(row.is_local), connection_type: row.connection_type, hawser_token: row.hawser_token } : null; +} + +function detectDockerSocket() { + if (process.env.DOCKER_SOCKET && _existsSync(process.env.DOCKER_SOCKET)) return process.env.DOCKER_SOCKET; + if (process.env.DOCKER_HOST?.startsWith('unix://')) { + const p = process.env.DOCKER_HOST.replace('unix://', ''); + if (_existsSync(p)) return p; + } + for (const s of ['/var/run/docker.sock', _homedir() + '/.docker/run/docker.sock', _homedir() + '/.orbstack/run/docker.sock', '/run/docker.sock']) { + if (_existsSync(s)) return s; + } + return '/var/run/docker.sock'; +} +const dockerSocketPath = detectDockerSocket(); +console.log('Detected Docker socket at:', dockerSocketPath); + +const dockerStreams = new Map(); +let _wsConnCounter = 0; + +async function _getDockerTarget(envId) { + if (!envId) return { type: 'unix', socket: dockerSocketPath }; + const env = await _getEnvironment(envId); + if (!env) return { type: 'unix', socket: dockerSocketPath }; + // Check for socket connection type (local Unix socket) + if (env.is_local || env.connection_type === 'socket' || !env.connection_type) { + return { type: 'unix', socket: env.socket_path || dockerSocketPath }; + } + if (env.connection_type === 'hawser-edge') return { type: 'hawser-edge', environmentId: envId }; + return { type: 'tcp', host: env.host, port: env.port || 2375, hawserToken: env.connection_type === 'hawser-standard' ? env.hawser_token : undefined }; +} + +async function createExec(containerId, cmd, user, target) { + const headers = { 'Content-Type': 'application/json' }; + const fetchOpts = { + method: 'POST', + headers, + body: JSON.stringify({ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: cmd, User: user }) + }; + let url; + if (target.type === 'unix') { + url = 'http://localhost/containers/' + containerId + '/exec'; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/containers/' + containerId + '/exec'; + if (target.hawserToken) headers['X-Hawser-Token'] = target.hawserToken; + } + const res = await fetch(url, fetchOpts); + if (!res.ok) throw new Error('Failed to create exec: ' + (await res.text())); + return res.json(); +} + +async function resizeExec(execId, cols, rows, target) { + try { + const fetchOpts = { method: 'POST' }; + let url; + if (target.type === 'unix') { + url = 'http://localhost/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + if (target.hawserToken) fetchOpts.headers = { 'X-Hawser-Token': target.hawserToken }; + } + await fetch(url, fetchOpts); + } catch {} +} + +// ============ Hawser Edge Support ============ +// Global edge connections map (shared with hawser.ts via globalThis) +if (!globalThis.__hawserEdgeConnections) globalThis.__hawserEdgeConnections = new Map(); +const _edgeConnections = globalThis.__hawserEdgeConnections; + +// Map WebSocket to environmentId for quick lookup +const _wsToEnvId = new Map(); + +// Edge exec sessions (execId -> frontend WebSocket) +const _edgeExecSessions = new Map(); + +// Validate Hawser token against database +async function _validateHawserToken(token) { + const db = _getDb(); + if (!db) return { valid: false }; + let tokens; + if (_isPostgres) { + tokens = await db.unsafe('SELECT * FROM hawser_tokens WHERE is_active = true'); + } else { + tokens = db.prepare('SELECT * FROM hawser_tokens WHERE is_active = 1').all(); + } + for (const t of tokens) { + try { + const isValid = await Bun.password.verify(token, t.token); + if (isValid) { + if (_isPostgres) { + await db.unsafe('UPDATE hawser_tokens SET last_used = NOW() WHERE id = $1', [t.id]); + } else { + db.prepare('UPDATE hawser_tokens SET last_used = datetime(\\'now\\') WHERE id = ?').run(t.id); + } + return { valid: true, environmentId: t.environment_id, tokenId: t.id }; + } + } catch {} + } + return { valid: false }; +} + +// Update environment status in database +async function _updateEnvStatus(envId, conn) { + const db = _getDb(); + if (!db) return; + try { + if (conn) { + if (_isPostgres) { + await db.unsafe('UPDATE environments SET hawser_last_seen = NOW(), hawser_agent_id = $1, hawser_agent_name = $2, hawser_version = $3, hawser_capabilities = $4 WHERE id = $5', + [conn.agentId, conn.agentName, conn.agentVersion, JSON.stringify(conn.capabilities || []), envId]); + } else { + db.prepare('UPDATE environments SET hawser_last_seen = datetime(\\'now\\'), hawser_agent_id = ?, hawser_agent_name = ?, hawser_version = ?, hawser_capabilities = ? WHERE id = ?') + .run(conn.agentId, conn.agentName, conn.agentVersion, JSON.stringify(conn.capabilities || []), envId); + } + } else { + if (_isPostgres) { + await db.unsafe('UPDATE environments SET hawser_last_seen = NOW() WHERE id = $1', [envId]); + } else { + db.prepare('UPDATE environments SET hawser_last_seen = datetime(\\'now\\') WHERE id = ?').run(envId); + } + } + } catch {} +} + +// Handle Hawser Edge protocol messages +async function _handleHawserMessage(ws, msg) { + if (msg.type === 'hello') { + console.log('[Hawser] Hello from agent:', msg.agentName, '(' + msg.agentId + ')'); + const validation = await _validateHawserToken(msg.token); + if (!validation.valid) { + console.log('[Hawser] Invalid token'); + ws.send(JSON.stringify({ type: 'error', error: 'Invalid token' })); + ws.close(); + return; + } + const envId = validation.environmentId; + const existing = _edgeConnections.get(envId); + if (existing) { + const pendingCount = existing.pendingRequests.size; + const streamCount = existing.pendingStreamRequests.size; + console.log('[Hawser] Replacing existing connection for env', envId, '- rejecting', pendingCount, 'pending requests and', streamCount, 'stream requests'); + // Reject all pending requests before closing + for (const [requestId, pending] of existing.pendingRequests) { + clearTimeout(pending.timeout); + pending.reject(new Error('Connection replaced by new agent')); + } + for (const [requestId, pending] of existing.pendingStreamRequests) { + pending.onEnd?.('Connection replaced by new agent'); + } + existing.pendingRequests.clear(); + existing.pendingStreamRequests.clear(); + existing.ws.close(1000, 'Replaced'); + _wsToEnvId.delete(existing.ws); + } + const conn = { + ws, environmentId: envId, agentId: msg.agentId, agentName: msg.agentName, + agentVersion: msg.version || 'unknown', dockerVersion: msg.dockerVersion || 'unknown', + hostname: msg.hostname || 'unknown', capabilities: msg.capabilities || [], + connectedAt: new Date(), lastHeartbeat: new Date(), + pendingRequests: new Map(), pendingStreamRequests: new Map(), + pingInterval: null + }; + _edgeConnections.set(envId, conn); + _wsToEnvId.set(ws, envId); + await _updateEnvStatus(envId, conn); + ws.send(JSON.stringify({ type: 'welcome', environmentId: envId, message: 'Connected to Dockhand' })); + // Start server-side ping interval to keep connection alive through Traefik/proxies (5s) + conn.pingInterval = setInterval(() => { + try { ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); } + catch { if (conn.pingInterval) { clearInterval(conn.pingInterval); conn.pingInterval = null; } } + }, 5000); + console.log('[Hawser] Agent', msg.agentName, 'connected for env', envId); + } else if (msg.type === 'ping') { + const envId = _wsToEnvId.get(ws); + if (envId) { const c = _edgeConnections.get(envId); if (c) c.lastHeartbeat = new Date(); } + ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); + } else if (msg.type === 'pong') { + const envId = _wsToEnvId.get(ws); + if (envId) { const c = _edgeConnections.get(envId); if (c) c.lastHeartbeat = new Date(); } + } else if (msg.type === 'response') { + const envId = _wsToEnvId.get(ws); + if (!envId) { + console.warn('[Hawser] Response from unknown WebSocket, requestId=' + msg.requestId); + return; + } + const conn = _edgeConnections.get(envId); + if (conn) { + const pending = conn.pendingRequests.get(msg.requestId); + if (pending) { + clearTimeout(pending.timeout); + conn.pendingRequests.delete(msg.requestId); + pending.resolve({ statusCode: msg.statusCode, headers: msg.headers || {}, body: msg.body || '', isBinary: msg.isBinary || false }); + } else { + console.warn('[Hawser] Response for unknown request ' + msg.requestId + ' on env ' + envId); + } + } + } else if (msg.type === 'stream') { + const envId = _wsToEnvId.get(ws); + if (!envId) { + console.warn('[Hawser] Stream data from unknown WebSocket, requestId=' + msg.requestId); + return; + } + const conn = _edgeConnections.get(envId); + if (conn?.pendingStreamRequests) { + const pending = conn.pendingStreamRequests.get(msg.requestId); + if (pending) { + pending.onData(msg.data, msg.stream); + } else { + console.warn('[Hawser] Stream data for unknown request ' + msg.requestId + ' on env ' + envId); + } + } + } else if (msg.type === 'stream_end') { + const envId = _wsToEnvId.get(ws); + if (!envId) { + console.warn('[Hawser] Stream end from unknown WebSocket, requestId=' + msg.requestId); + return; + } + const conn = _edgeConnections.get(envId); + if (conn?.pendingStreamRequests) { + const pending = conn.pendingStreamRequests.get(msg.requestId); + if (pending) { + conn.pendingStreamRequests.delete(msg.requestId); + pending.onEnd(msg.reason); + } else { + console.warn('[Hawser] Stream end for unknown request ' + msg.requestId + ' on env ' + envId); + } + } + } else if (msg.type === 'exec_ready') { + const session = _edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) console.log('[Hawser] Exec ready:', msg.execId); + } else if (msg.type === 'exec_output') { + const session = _edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) { + const data = Buffer.from(msg.data, 'base64').toString('utf-8'); + session.ws.send(JSON.stringify({ type: 'output', data })); + } + } else if (msg.type === 'exec_end') { + const session = _edgeExecSessions.get(msg.execId); + if (session) { + console.log('[Hawser] Exec ended:', msg.execId); + if (session.ws?.readyState === 1) { session.ws.send(JSON.stringify({ type: 'exit' })); session.ws.close(); } + _edgeExecSessions.delete(msg.execId); + } + } else if (msg.type === 'container_event') { + const envId = _wsToEnvId.get(ws); + if (envId && msg.event) { + // Call the global handler registered by hawser.ts + if (globalThis.__hawserHandleContainerEvent) { + globalThis.__hawserHandleContainerEvent(envId, msg.event).catch((err) => { + console.error('[Hawser] Error handling container event:', err); + }); + } + } + } else if (msg.type === 'metrics') { + // Metrics from agent - save to database for dashboard graphs + const envId = _wsToEnvId.get(ws); + if (envId && msg.metrics) { + if (globalThis.__hawserHandleMetrics) { + globalThis.__hawserHandleMetrics(envId, msg.metrics).catch((err) => { + console.error('[Hawser] Error saving metrics:', err); + }); + } + } + } +} + +// Expose send function for hawser.ts module +globalThis.__hawserSendMessage = (envId, message) => { + const conn = _edgeConnections.get(envId); + if (!conn?.ws) return false; + try { conn.ws.send(message); return true; } catch { return false; } +}; + +// ============ Combined WebSocket Handler ============ +const combinedWebsocket = { + async open(ws) { + const connType = ws.data?.type; + + // Hawser Edge connection - wait for hello message + if (connType === 'hawser') { + console.log('[Hawser] New connection pending authentication'); + return; + } + + // Terminal connection + const connId = 'ws-' + (++_wsConnCounter); + ws.data = ws.data || {}; + ws.data.connId = connId; + const { containerId, shell, user, envId } = ws.data; + if (!containerId) { ws.send(JSON.stringify({ type: 'error', message: 'No container ID' })); ws.close(); return; } + const target = await _getDockerTarget(envId); + console.log('[WS] Open:', connId, containerId, 'target:', target.type); + + // Handle Hawser Edge terminal + if (target.type === 'hawser-edge') { + const conn = _edgeConnections.get(target.environmentId); + if (!conn) { ws.send(JSON.stringify({ type: 'error', message: 'Edge agent not connected' })); ws.close(); return; } + const execId = crypto.randomUUID(); + _edgeExecSessions.set(execId, { ws, execId, environmentId: target.environmentId }); + ws.data.edgeExecId = execId; + conn.ws.send(JSON.stringify({ type: 'exec_start', execId, containerId, cmd: shell || '/bin/sh', user: user || 'root', cols: 120, rows: 30 })); + return; + } + + try { + const exec = await createExec(containerId, [shell || '/bin/sh'], user || 'root', target); + const execId = exec.Id; + let dockerStream; + let headersStripped = false; + let isChunked = false; + const socketHandler = { + data(socket, data) { + if (ws.readyState === 1) { + let text = new TextDecoder().decode(data); + if (!headersStripped) { + if (text.toLowerCase().includes('transfer-encoding: chunked')) isChunked = true; + const i = text.indexOf('\\r\\n\\r\\n'); + if (i > -1) { text = text.slice(i + 4); headersStripped = true; } + else if (text.startsWith('HTTP/')) return; + } + if (isChunked && text) text = text.replace(/^[0-9a-fA-F]+\\r\\n/gm, '').replace(/\\r\\n$/g, ''); + if (text) ws.send(JSON.stringify({ type: 'output', data: text })); + } + }, + close() { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'exit' })); ws.close(); } }, + error() {}, + open(socket) { + const body = JSON.stringify({ Detach: false, Tty: true }); + const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : ''; + socket.write('POST /exec/' + execId + '/start HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: ' + body.length + '\\r\\n\\r\\n' + body); + } + }; + if (target.type === 'unix') { + dockerStream = await Bun.connect({ unix: target.socket, socket: socketHandler }); + } else { + dockerStream = await Bun.connect({ hostname: target.host, port: target.port, socket: socketHandler }); + } + dockerStreams.set(connId, { stream: dockerStream, execId, target }); + } catch (e) { ws.send(JSON.stringify({ type: 'error', message: e.message })); ws.close(); } + }, + async message(ws, message) { + const connType = ws.data?.type; + + // Hawser Edge message + if (connType === 'hawser') { + try { + let msgStr = typeof message === 'string' ? message : message instanceof ArrayBuffer ? new TextDecoder().decode(message) : Buffer.isBuffer(message) ? message.toString('utf-8') : new TextDecoder().decode(new Uint8Array(message)); + const msg = JSON.parse(msgStr); + await _handleHawserMessage(ws, msg); + } catch (e) { + console.error('[Hawser] Error:', e.message); + ws.send(JSON.stringify({ type: 'error', error: e.message })); + } + return; + } + + // Edge exec session input + const edgeExecId = ws.data?.edgeExecId; + if (edgeExecId) { + const session = _edgeExecSessions.get(edgeExecId); + if (session) { + const conn = _edgeConnections.get(session.environmentId); + if (conn) { + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input') conn.ws.send(JSON.stringify({ type: 'exec_input', execId: edgeExecId, data: Buffer.from(msg.data).toString('base64') })); + else if (msg.type === 'resize') conn.ws.send(JSON.stringify({ type: 'exec_resize', execId: edgeExecId, cols: msg.cols, rows: msg.rows })); + } catch {} + } + } + return; + } + + // Terminal message + const connId = ws.data?.connId; + if (!connId) return; + const d = dockerStreams.get(connId); + if (!d) return; + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input' && d.stream) d.stream.write(msg.data); + else if (msg.type === 'resize' && d.execId) resizeExec(d.execId, msg.cols, msg.rows, d.target); + } catch { if (d.stream) d.stream.write(message); } + }, + close(ws) { + const connType = ws.data?.type; + + // Hawser Edge disconnection + if (connType === 'hawser') { + const envId = _wsToEnvId.get(ws); + if (envId) { + const conn = _edgeConnections.get(envId); + if (conn) { + console.log('[Hawser] Agent disconnected:', conn.agentId); + // Clear server-side ping interval + if (conn.pingInterval) { clearInterval(conn.pingInterval); conn.pingInterval = null; } + for (const [, p] of conn.pendingRequests) { clearTimeout(p.timeout); p.reject(new Error('Connection closed')); } + for (const [, p] of conn.pendingStreamRequests) { p.onEnd('Connection closed'); } + _edgeConnections.delete(envId); + _updateEnvStatus(envId, null); + } + _wsToEnvId.delete(ws); + } + return; + } + + // Edge exec session close + const edgeExecId = ws.data?.edgeExecId; + if (edgeExecId) { + const session = _edgeExecSessions.get(edgeExecId); + if (session) { + const conn = _edgeConnections.get(session.environmentId); + if (conn) conn.ws.send(JSON.stringify({ type: 'exec_end', execId: edgeExecId, reason: 'user_closed' })); + _edgeExecSessions.delete(edgeExecId); + } + return; + } + + // Terminal close + const connId = ws.data?.connId; + if (!connId) return; + const d = dockerStreams.get(connId); + if (d?.stream) d.stream.end(); + dockerStreams.delete(connId); + } +}; +`; + + const insertPoint = content.indexOf('var path = env('); + if (insertPoint > -1) { + content = content.slice(0, insertPoint) + wsHandler + content.slice(insertPoint); + } + + content = content.replace( + 'var { fetch: handlerFetch, websocket } = getHandler();', + 'var { fetch: handlerFetch, websocket: _ } = getHandler(); var websocket = combinedWebsocket;' + ); + + await Bun.write(indexPath, content); + console.log('✓ Patched index.js'); +} + +console.log('Patching build...'); +await patchHandler(); +await patchIndex(); +console.log('✓ Done'); From 410d542c580c0e6d0e9c65677642effad98bbfa6 Mon Sep 17 00:00:00 2001 From: jarek Date: Sat, 3 Jan 2026 14:56:20 +0100 Subject: [PATCH 24/30] 1.0.6 --- Dockerfile | 14 +- src/lib/components/StackEnvVarsPanel.svelte | 132 +++++++++++-------- src/lib/components/data-grid/DataGrid.svelte | 10 +- src/lib/data/changelog.json | 10 ++ src/routes/activity/+page.svelte | 12 +- src/routes/images/+page.svelte | 21 ++- src/routes/stacks/+page.svelte | 12 +- src/routes/stacks/GitStackModal.svelte | 117 ++++++++++++++-- src/routes/stacks/StackModal.svelte | 69 +++------- 9 files changed, 264 insertions(+), 133 deletions(-) diff --git a/Dockerfile b/Dockerfile index edd2e4a..902b76f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,9 @@ # - Full transparency (no dependency on pre-built Chainguard images) # - Reproducible builds from open-source Wolfi packages # - Minimal attack surface with only required packages +# +# Bun is copied from the official oven/bun image (app-builder stage) to ensure +# compatibility with all x86_64 CPUs (including those without AVX2 like Celeron). # ============================================================================= # ----------------------------------------------------------------------------- @@ -15,13 +18,14 @@ # to build our custom Wolfi OS from scratch using open-source packages. FROM alpine:3.21 AS os-builder +ARG TARGETARCH + WORKDIR /work # Install apko tool (latest stable release) # apko is the tool Chainguard uses to build their images - we use it directly ARG APKO_VERSION=0.30.34 -ARG TARGETARCH -RUN apk add --no-cache curl \ +RUN apk add --no-cache curl unzip \ && ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") \ && curl -sL "https://github.com/chainguard-dev/apko/releases/download/v${APKO_VERSION}/apko_${APKO_VERSION}_linux_${ARCH}.tar.gz" \ | tar -xz --strip-components=1 -C /usr/local/bin \ @@ -29,6 +33,7 @@ RUN apk add --no-cache curl \ # Generate apko.yaml for current target architecture only # We build single-arch to avoid multi-arch layer confusion in extraction +# Note: Bun is NOT included here - it's copied from app-builder stage for CPU compatibility RUN APKO_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") \ && printf '%s\n' \ "contents:" \ @@ -41,7 +46,6 @@ RUN APKO_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") " - ca-certificates" \ " - busybox" \ " - tzdata" \ - " - bun" \ " - docker-cli" \ " - docker-compose" \ " - sqlite" \ @@ -99,6 +103,10 @@ FROM scratch # Install our custom-built Wolfi OS (now we have /bin/sh!) COPY --from=os-builder /work/rootfs/ / +# Copy Bun from official image - ensures compatibility with all x86_64 CPUs (no AVX2 requirement) +# Wolfi's bun package requires AVX2 which breaks on Celeron/Atom CPUs +COPY --from=app-builder /usr/local/bin/bun /usr/bin/bun + WORKDIR /app # Set up environment variables diff --git a/src/lib/components/StackEnvVarsPanel.svelte b/src/lib/components/StackEnvVarsPanel.svelte index 715d063..30391b5 100644 --- a/src/lib/components/StackEnvVarsPanel.svelte +++ b/src/lib/components/StackEnvVarsPanel.svelte @@ -287,46 +287,68 @@
-
-
- Environment variables - {#if infoText} - - - - - - -

{infoText}

-
-
-
- {/if} - -
- - + +
+
+ Environment variables + {#if infoText} + + + + + + +

{infoText}

+
+
+
+ {/if} + +
+ + +
+ + {#if validation} +
+ {#if validation.missing.length > 0} + + {validation.missing.length} missing + + {/if} + {#if validation.required.length > 0} + + {validation.required.length - validation.missing.length} defined + + {/if} + {#if validation.optional.length > 0} + + {validation.optional.length} optional + + {/if}
+ {/if}
+ {#if !readonly} -
+
{#if viewMode === 'form'} {/if} -
- confirmClearOpen = o} - > - {#snippet children({ open })} - - {/snippet} - -
+ confirmClearOpen = o} + > + {#snippet children({ open })} + + {/snippet} +
(); let rowStateCacheDataRef: T[] | null = null; + let rowStateCacheExpandedRef: Set | null = null; + let rowStateCacheSelectedRef: Set | null = null; - // Clear row state cache when data reference changes + // Clear row state cache when data or selection/expansion state changes $effect(() => { - if (data !== rowStateCacheDataRef) { + if (data !== rowStateCacheDataRef || + expandedKeys !== rowStateCacheExpandedRef || + selectedKeys !== rowStateCacheSelectedRef) { rowStateCache = new WeakMap(); rowStateCacheDataRef = data; + rowStateCacheExpandedRef = expandedKeys; + rowStateCacheSelectedRef = selectedKeys; } }); diff --git a/src/lib/data/changelog.json b/src/lib/data/changelog.json index 7954abb..94fbdf7 100644 --- a/src/lib/data/changelog.json +++ b/src/lib/data/changelog.json @@ -1,4 +1,14 @@ [ + { + "version": "1.0.6", + "date": "2026-01-03", + "changes": [ + { "type": "fix", "text": "Legacy CPU support (Celeron, Atom) - Bun binary now copied from official image instead of Wolfi package" }, + { "type": "fix", "text": "Stack modal layouts improved with resizable split panels" }, + { "type": "fix", "text": "Missing column headers in images overview" } + ], + "imageTag": "fnsys/dockhand:v1.0.6" + }, { "version": "1.0.5", "date": "2026-01-01", diff --git a/src/routes/activity/+page.svelte b/src/routes/activity/+page.svelte index 008d4e2..c859190 100644 --- a/src/routes/activity/+page.svelte +++ b/src/routes/activity/+page.svelte @@ -307,7 +307,9 @@ total = data.total; if (append) { - events = [...events, ...data.events]; + // Use push() for O(k) append instead of spread for O(n) copy + events.push(...data.events); + events = events; // Trigger Svelte reactivity hasMore = events.length < total; // Update eventIds Set with new events for (const evt of data.events) { @@ -517,12 +519,16 @@ // Add to beginning of events (prepend new events) - use Set for fast duplicate check if (!eventIds.has(newEvent.id)) { eventIds.add(newEvent.id); - events = [newEvent, ...events]; + // Use unshift() for in-place mutation instead of spread for O(n) copy + events.unshift(newEvent); + events = events; // Trigger Svelte reactivity total = total + 1; // Add container to list if not already there if (newEvent.containerName && !containers.includes(newEvent.containerName)) { - containers = [...containers, newEvent.containerName].sort(); + containers.push(newEvent.containerName); + containers.sort(); + containers = containers; // Trigger Svelte reactivity } } } catch { diff --git a/src/routes/images/+page.svelte b/src/routes/images/+page.svelte index fd428c6..279a33c 100644 --- a/src/routes/images/+page.svelte +++ b/src/routes/images/+page.svelte @@ -7,7 +7,7 @@ import { Label } from '$lib/components/ui/label'; import * as Dialog from '$lib/components/ui/dialog'; import * as Select from '$lib/components/ui/select'; - import { Trash2, Upload, RefreshCw, Play, Search, Layers, Server, ShieldCheck, CheckSquare, Square, Tag, Check, XCircle, Icon, AlertTriangle, X, Images, Copy, Download, ChevronRight, ChevronDown, Loader2 } from 'lucide-svelte'; + import { Trash2, Upload, RefreshCw, Play, Search, Layers, Server, ShieldCheck, CheckSquare, Square, Tag, Check, XCircle, Icon, AlertTriangle, X, Images, Copy, Download, ChevronRight, ChevronDown, Loader2, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-svelte'; import { broom, whale } from '@lucide/lab'; import ConfirmPopover from '$lib/components/ConfirmPopover.svelte'; import BatchOperationModal from '$lib/components/BatchOperationModal.svelte'; @@ -720,6 +720,25 @@ {/if} + {:else if column.sortable} + + {:else if column.id !== 'expand' && column.id !== 'actions'} + {column.label} {/if} {/snippet} {#snippet cell(column, group, rowState)} diff --git a/src/routes/stacks/+page.svelte b/src/routes/stacks/+page.svelte index fe95bbe..5f32637 100644 --- a/src/routes/stacks/+page.svelte +++ b/src/routes/stacks/+page.svelte @@ -1270,10 +1270,7 @@ sortDirection = state.direction; }} onRowClick={(stack, e) => { - const hasContainers = stack.containers && stack.containers.length > 0; - if (hasContainers) { - toggleExpand(stack.name); - } + toggleExpand(stack.name); }} rowClass={(stack) => { const isExp = expandedStacks.has(stack.name); @@ -1904,6 +1901,13 @@ {/each}
+ {:else} +
+
+ + No containers +
+
{/if} {/snippet} diff --git a/src/routes/stacks/GitStackModal.svelte b/src/routes/stacks/GitStackModal.svelte index af6fa47..3d53581 100644 --- a/src/routes/stacks/GitStackModal.svelte +++ b/src/routes/stacks/GitStackModal.svelte @@ -1,17 +1,25 @@ { if (isOpen) focusFirstInput(); }}> - - - - - {gitStack ? 'Edit git stack' : 'Deploy from Git'} - - - {gitStack ? 'Update git stack settings' : 'Deploy a compose stack from a Git repository'} - + + +
+
+
+ +
+
+ + {gitStack ? 'Edit git stack' : 'Deploy from Git'} + + + {gitStack ? 'Update git stack settings' : 'Deploy a compose stack from a Git repository'} + +
+
+ + + +
-
+
-
+
+
{#if !gitStack}
@@ -774,10 +849,24 @@ {#if formError}

{formError}

{/if} +
+
+ + + -
+
- + {#if gitStack}
-
- - Environment variables - - - - - -
-

These variables will be written to a .env file in the stack directory.

-
-
-
- - {#if envValidation} -
- {#if envValidation.missing.length > 0} - - {envValidation.missing.length} missing - - {/if} - {#if envValidation.required.length > 0} - - {envValidation.required.length - envValidation.missing.length} required - - {/if} - {#if envValidation.optional.length > 0} - - {envValidation.optional.length} optional - - {/if} - {#if envValidation.unused.length > 0} - - {envValidation.unused.length} unused - - {/if} -
- {/if} -
-
- { markDirty(); debouncedValidate(); }} - theme={editorTheme} - /> -
+ { markDirty(); debouncedValidate(); }} + theme={editorTheme} + infoText="These variables will be written to a .env file in the stack directory." + />
{:else if activeTab === 'graph'} From b269b8d50db47cdd28b76e57c446cba6f143227f Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Sun, 11 Jan 2026 07:16:18 +0100 Subject: [PATCH 25/30] 1.0.7 --- Dockerfile | 27 +- drizzle-pg/meta/_journal.json | 7 + drizzle/meta/_journal.json | 7 + package.json | 68 +- src/hooks.server.ts | 7 +- src/lib/components/CodeEditor.svelte | 9 +- src/lib/components/StackEnvVarsEditor.svelte | 2 +- src/lib/components/StackEnvVarsPanel.svelte | 41 +- src/lib/components/TimezoneSelector.svelte | 2 +- src/lib/components/data-grid/DataGrid.svelte | 38 +- src/lib/components/icon-picker.svelte | 2 +- .../ui/date-picker/date-picker.svelte | 2 +- .../ui/dialog/dialog-content.svelte | 2 +- .../ui/dialog/dialog-overlay.svelte | 2 +- .../dropdown-menu-content.svelte | 2 +- .../dropdown-menu-sub-content.svelte | 2 +- .../ui/popover/popover-content.svelte | 2 +- .../ui/select/select-content.svelte | 2 +- .../ui/tooltip/tooltip-content.svelte | 2 +- src/lib/config/grid-columns.ts | 6 +- src/lib/data/changelog.json | 19 + src/lib/data/dependencies.json | 322 +---- src/lib/server/auth.ts | 56 +- src/lib/server/db.ts | 174 ++- src/lib/server/db/drizzle.ts | 8 +- src/lib/server/db/schema/index.ts | 2 + src/lib/server/db/schema/pg-schema.ts | 2 + src/lib/server/docker.ts | 121 +- src/lib/server/hawser.ts | 18 +- src/lib/server/metrics-collector.ts | 271 ----- src/lib/server/stacks.ts | 715 +++++++++-- src/lib/server/subprocess-manager.ts | 39 +- .../server/subprocesses/event-subprocess.ts | 204 +++- .../server/subprocesses/metrics-subprocess.ts | 37 +- src/lib/stores/grid-preferences.ts | 17 +- src/lib/stores/settings.ts | 62 +- src/lib/types.ts | 5 +- src/routes/+page.svelte | 2 +- src/routes/activity/+page.svelte | 21 +- .../containers/[id]/logs/stream/+server.ts | 38 +- .../api/containers/[id]/stats/+server.ts | 36 +- src/routes/api/containers/stats/+server.ts | 38 +- src/routes/api/dashboard/stats/+server.ts | 2 +- .../api/dashboard/stats/stream/+server.ts | 91 +- src/routes/api/environments/+server.ts | 18 +- src/routes/api/environments/[id]/+server.ts | 7 +- src/routes/api/environments/test/+server.ts | 82 +- src/routes/api/events/+server.ts | 11 +- src/routes/api/git/stacks/+server.ts | 4 +- src/routes/api/registry/tags/+server.ts | 45 +- src/routes/api/settings/general/+server.ts | 102 +- src/routes/api/stacks/+server.ts | 44 +- src/routes/api/stacks/[name]/+server.ts | 5 +- .../api/stacks/[name]/compose/+server.ts | 41 +- src/routes/api/stacks/[name]/down/+server.ts | 5 +- src/routes/api/stacks/[name]/env/+server.ts | 39 +- .../api/stacks/[name]/env/raw/+server.ts | 69 +- .../api/stacks/[name]/restart/+server.ts | 5 +- src/routes/api/stacks/[name]/start/+server.ts | 5 +- src/routes/api/stacks/[name]/stop/+server.ts | 5 +- src/routes/api/stacks/sources/+server.ts | 3 +- src/routes/api/system/+server.ts | 1 + src/routes/containers/+page.svelte | 12 +- src/routes/containers/BatchUpdateModal.svelte | 2 +- .../containers/ContainerInspectModal.svelte | 128 +- .../containers/ContainerSettingsTab.svelte | 20 +- .../containers/CreateContainerModal.svelte | 8 +- .../containers/EditContainerModal.svelte | 10 +- src/routes/containers/FileBrowserModal.svelte | 2 +- src/routes/containers/FileBrowserPanel.svelte | 39 +- src/routes/dashboard/EnvironmentTile.svelte | 123 +- src/routes/dashboard/dashboard-header.svelte | 25 +- src/routes/dashboard/index.ts | 1 + src/routes/images/+page.svelte | 107 +- .../images/ImagePullProgressPopover.svelte | 2 +- src/routes/networks/CreateNetworkModal.svelte | 2 +- .../networks/NetworkInspectModal.svelte | 2 +- src/routes/registry/+page.svelte | 139 ++- src/routes/settings/about/AboutTab.svelte | 14 +- .../environments/EnvironmentModal.svelte | 23 +- src/routes/settings/general/GeneralTab.svelte | 130 +- src/routes/stacks/+page.svelte | 124 +- .../stacks/GitDeployProgressPopover.svelte | 2 +- src/routes/stacks/StackModal.svelte | 1083 ++++++++++++++--- src/routes/volumes/VolumeBrowserModal.svelte | 2 +- src/routes/volumes/VolumeInspectModal.svelte | 2 +- 86 files changed, 3642 insertions(+), 1383 deletions(-) delete mode 100644 src/lib/server/metrics-collector.ts diff --git a/Dockerfile b/Dockerfile index 902b76f..1e2e0c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,9 @@ # - Reproducible builds from open-source Wolfi packages # - Minimal attack surface with only required packages # -# Bun is copied from the official oven/bun image (app-builder stage) to ensure -# compatibility with all x86_64 CPUs (including those without AVX2 like Celeron). +# Bun is copied from the official oven/bun image (app-builder stage). +# For CPUs without AVX support (Celeron, Atom, pre-Haswell), build with: +# docker build --build-arg BUN_VARIANT=baseline -t dockhand:baseline . # ============================================================================= # ----------------------------------------------------------------------------- @@ -75,10 +76,15 @@ RUN apko build apko.yaml dockhand-base:latest output.tar \ # Alpine's musl libc causes rayon/tokio thread pool panics during svelte-adapter-bun build FROM oven/bun:1.3.5-debian AS app-builder +# Build argument for Bun variant (regular or baseline) +# baseline is for CPUs without AVX support (Celeron, Atom, pre-Haswell) +ARG BUN_VARIANT=regular +ARG TARGETARCH + WORKDIR /app # Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends jq git && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends jq git curl unzip ca-certificates && rm -rf /var/lib/apt/lists/* # Copy package files and install ALL dependencies (needed for build) COPY package.json bun.lock* bunfig.toml ./ @@ -95,6 +101,19 @@ RUN NODE_OPTIONS="--max-old-space-size=8192 --max-semi-space-size=128" bun run b RUN rm -rf node_modules && bun install --production --frozen-lockfile \ && rm -rf node_modules/@types node_modules/bun-types +# Download baseline Bun binary if BUN_VARIANT=baseline (for CPUs without AVX) +# Only applies to amd64 - ARM64 doesn't have AVX concept +ARG BUN_VERSION=1.3.5 +RUN if [ "$BUN_VARIANT" = "baseline" ] && [ "$TARGETARCH" = "amd64" ]; then \ + echo "Downloading Bun baseline binary for CPUs without AVX support..." && \ + curl -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64-baseline.zip" -o /tmp/bun.zip && \ + unzip -o /tmp/bun.zip -d /tmp && \ + cp /tmp/bun-linux-x64-baseline/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + rm -rf /tmp/bun.zip /tmp/bun-linux-x64-baseline && \ + echo "Bun baseline binary installed successfully"; \ + fi + # ----------------------------------------------------------------------------- # Stage 3: Final Image (Scratch + Custom Wolfi OS) # ----------------------------------------------------------------------------- @@ -105,6 +124,8 @@ COPY --from=os-builder /work/rootfs/ / # Copy Bun from official image - ensures compatibility with all x86_64 CPUs (no AVX2 requirement) # Wolfi's bun package requires AVX2 which breaks on Celeron/Atom CPUs +# For baseline builds (BUN_VARIANT=baseline), this contains the baseline binary (no AVX requirement) +# For regular builds, this contains the standard oven/bun binary COPY --from=app-builder /usr/local/bin/bun /usr/bin/bun WORKDIR /app diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json index b439adc..590bb1f 100644 --- a/drizzle-pg/meta/_journal.json +++ b/drizzle-pg/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1766763867484, "tag": "0002_add_pending_container_updates", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1767687362730, + "tag": "0003_add_stack_paths", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 868151f..f4192f3 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1766763860091, "tag": "0002_add_pending_container_updates", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1767689000000, + "tag": "0003_add_stack_paths", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 24cc896..033063d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dockhand", "private": true, - "version": "1.0.4", + "version": "1.0.3", "type": "module", "scripts": { "dev": "bunx --bun vite dev", @@ -39,7 +39,7 @@ }, "dependencies": { "@codemirror/autocomplete": "6.20.0", - "@codemirror/commands": "6.10.0", + "@codemirror/commands": "6.10.1", "@codemirror/lang-css": "6.3.1", "@codemirror/lang-html": "6.4.11", "@codemirror/lang-javascript": "6.2.4", @@ -48,63 +48,71 @@ "@codemirror/lang-python": "6.2.1", "@codemirror/lang-sql": "6.10.0", "@codemirror/lang-xml": "6.1.0", - "@codemirror/language": "6.11.3", + "@codemirror/lang-yaml": "6.1.2", + "@codemirror/language": "6.12.1", "@codemirror/search": "6.5.11", + "@codemirror/state": "6.5.3", + "@codemirror/theme-one-dark": "6.1.3", + "@codemirror/view": "6.39.9", "@lezer/highlight": "1.2.3", "@lucide/lab": "^0.1.2", + "codemirror": "6.0.2", "croner": "9.1.0", "cronstrue": "3.9.0", - "drizzle-orm": "0.45.0", + "drizzle-orm": "0.45.1", + "hash-wasm": "4.12.0", "js-yaml": "^4.1.1", - "ldapts": "^8.0.9", - "nodemailer": "^7.0.11", + "ldapts": "^8.1.3", + "nodemailer": "^7.0.12", "otpauth": "^9.4.1", - "postgres": "3.4.7", + "postgres": "3.4.8", "qrcode": "^1.5.4", - "svelte-dnd-action": "0.9.68", + "svelte-dnd-action": "0.9.69", "svelte-sonner": "1.0.7" }, "devDependencies": { - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/state": "^6.5.2", - "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.38.8", - "@internationalized/date": "^3.10.0", + "@internationalized/date": "^3.10.1", "@layerstack/tailwind": "^1.0.1", - "@lucide/svelte": "^0.544.0", + "@lucide/svelte": "^0.562.0", "@playwright/test": "1.57.0", - "@sveltejs/kit": "^2.48.5", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/vite": "^4.1.17", - "@types/bun": "^1.2.5", + "@sveltejs/kit": "^2.49.3", + "@sveltejs/vite-plugin-svelte": "^6.2.3", + "@tailwindcss/vite": "^4.1.18", + "@types/bun": "^1.3.5", "@types/js-yaml": "^4.0.9", "@types/nodemailer": "^7.0.4", "@types/qrcode": "^1.5.6", - "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0", - "@xterm/xterm": "^5.5.0", - "autoprefixer": "^10.4.22", - "bits-ui": "^2.14.4", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", + "autoprefixer": "^10.4.23", + "bits-ui": "^2.15.4", "clsx": "^2.1.1", - "codemirror": "^6.0.2", "cytoscape": "^3.33.1", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "drizzle-kit": "0.31.8", - "layerchart": "^1.0.12", - "lucide-svelte": "^0.555.0", + "layerchart": "^1.0.13", + "lucide-svelte": "^0.562.0", "mode-watcher": "^1.1.0", "postcss": "^8.5.6", - "svelte": "^5.43.8", + "svelte": "^5.46.1", "svelte-adapter-bun": "1.0.1", - "svelte-check": "^4.3.4", + "svelte-check": "^4.3.5", "svelte-easy-crop": "^5.0.0", "svelte-virtual-scroll-list": "^1.3.0", "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.1.17", + "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vite": "^7.2.2" + "vite": "^7.3.1" + }, + "overrides": { + "@codemirror/state": "6.5.3", + "@codemirror/view": "6.39.9", + "@codemirror/language": "6.12.1", + "@lezer/common": "1.5.0", + "@lezer/highlight": "1.2.3" } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 10be1d1..c5bb24f 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,6 +4,7 @@ import { startScheduler } from '$lib/server/scheduler'; import { isAuthEnabled, validateSession } from '$lib/server/auth'; import { setServerStartTime } from '$lib/server/uptime'; import { checkLicenseExpiry, getHostname } from '$lib/server/license'; +import { initCryptoFallback } from '$lib/server/crypto-fallback'; import type { HandleServerError, Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; @@ -20,6 +21,9 @@ let initialized = false; if (!initialized) { try { + // Initialize crypto fallback first (detects old kernels and logs status) + initCryptoFallback(); + setServerStartTime(); // Track when server started initDatabase(); // Log hostname for license validation (set by entrypoint in Docker, or os.hostname() outside) @@ -68,7 +72,8 @@ const PUBLIC_PATHS = [ '/api/auth/oidc', '/api/license', '/api/changelog', - '/api/dependencies' + '/api/dependencies', + '/api/health' ]; // Check if path is public diff --git a/src/lib/components/CodeEditor.svelte b/src/lib/components/CodeEditor.svelte index 25b0c68..0e76cb9 100644 --- a/src/lib/components/CodeEditor.svelte +++ b/src/lib/components/CodeEditor.svelte @@ -225,6 +225,9 @@ // Mutable ref for callback - allows updating without recreating editor let onchangeRef: ((value: string) => void) | undefined = onchange; + // Flag to suppress onchange during programmatic value sync + let isSyncingExternalValue = false; + // Keep callback ref updated when prop changes $effect(() => { onchangeRef = onchange; @@ -660,8 +663,9 @@ view.update(trs); // Check if any transaction changed the document + // Skip onchange during programmatic value sync (only fire for user edits) const lastChangingTr = trs.findLast(tr => tr.docChanged); - if (lastChangingTr && onchangeRef) { + if (lastChangingTr && onchangeRef && !isSyncingExternalValue) { // Defer callback to next microtask to avoid blocking input handling // This allows key repeat to work properly const newContent = lastChangingTr.newDoc.toString(); @@ -801,9 +805,12 @@ // Only update if the external value differs from editor content // This prevents feedback loops from editor changes if (externalValue !== currentContent) { + // Suppress onchange during programmatic sync - only user edits should trigger it + isSyncingExternalValue = true; view.dispatch({ changes: { from: 0, to: currentContent.length, insert: externalValue } }); + isSyncingExternalValue = false; } } }); diff --git a/src/lib/components/StackEnvVarsEditor.svelte b/src/lib/components/StackEnvVarsEditor.svelte index 54a7b10..e0e4ff3 100644 --- a/src/lib/components/StackEnvVarsEditor.svelte +++ b/src/lib/components/StackEnvVarsEditor.svelte @@ -104,7 +104,7 @@
- {#each variables as variable, index (index)} + {#each variables as variable, index (`${index}-${variable.key}`)} {@const source = getSource(variable.key)} {@const isVarRequired = isRequired(variable.key)} {@const isVarOptional = isOptional(variable.key)} diff --git a/src/lib/components/StackEnvVarsPanel.svelte b/src/lib/components/StackEnvVarsPanel.svelte index 30391b5..df98404 100644 --- a/src/lib/components/StackEnvVarsPanel.svelte +++ b/src/lib/components/StackEnvVarsPanel.svelte @@ -46,38 +46,35 @@ let confirmClearOpen = $state(false); let contentAreaRef: HTMLDivElement; let parseWarnings = $state([]); - let hasMergedOnLoad = $state(false); // Count of secrets (for display in hint) const secretCount = $derived(variables.filter(v => v.isSecret && v.key.trim()).length); /** - * Merge variables and rawContent on initial load. - * Called by parent after setting both variables and rawContent. - * This ensures both are in sync regardless of which view mode is active. + * Sync variables with rawContent after initial load. + * Pass the loaded data directly to avoid timing issues with bindable props. + * Merges: secrets from loadedVars (DB) + non-secrets from loadedRaw (file). */ - export function mergeOnLoad() { - if (hasMergedOnLoad) return; - hasMergedOnLoad = true; + export function syncAfterLoad(loadedVars: EnvVar[], loadedRaw: string) { + if (!loadedRaw.trim()) { + // No raw content - just use the loaded variables as-is + variables = loadedVars; + rawContent = ''; + return; + } - // If rawContent exists, parse it and merge with variables (which may have secrets from DB) - if (rawContent.trim()) { - const { vars: rawVars } = parseRawContent(rawContent); - const rawVarsByKey = new Map(rawVars.map(v => [v.key, v])); + const { vars: rawVars } = parseRawContent(loadedRaw); - // Secrets come from variables (DB), non-secrets come from rawContent (file) - // But if a var exists in variables but not in rawContent, keep it (could be new) - const secrets = variables.filter(v => v.isSecret); - const nonSecretsFromRaw = rawVars; + // Secrets come from loadedVars (DB), non-secrets come from loadedRaw (file) + const secrets = loadedVars.filter(v => v.isSecret); - // Also keep non-secrets from variables that aren't in raw (new vars added before first save) - const rawKeys = new Set(rawVars.map(v => v.key)); - const newNonSecrets = variables.filter(v => !v.isSecret && v.key.trim() && !rawKeys.has(v.key)); + // Also keep non-secrets from loadedVars that aren't in raw (new vars added before first save) + const rawKeys = new Set(rawVars.map(v => v.key)); + const newNonSecrets = loadedVars.filter(v => !v.isSecret && v.key.trim() && !rawKeys.has(v.key)); - variables = [...nonSecretsFromRaw, ...newNonSecrets, ...secrets]; - } - // If no rawContent, variables is already correct (from DB), just need to generate raw - // for when user switches to text view (done in handleViewModeChange) + // Set both at once to avoid any intermediate states + variables = [...rawVars, ...newNonSecrets, ...secrets]; + rawContent = loadedRaw; } /** diff --git a/src/lib/components/TimezoneSelector.svelte b/src/lib/components/TimezoneSelector.svelte index bb28fe4..0866fdd 100644 --- a/src/lib/components/TimezoneSelector.svelte +++ b/src/lib/components/TimezoneSelector.svelte @@ -111,7 +111,7 @@ {/snippet} - + diff --git a/src/lib/components/data-grid/DataGrid.svelte b/src/lib/components/data-grid/DataGrid.svelte index 2a3ddf4..6b9cf3b 100644 --- a/src/lib/components/data-grid/DataGrid.svelte +++ b/src/lib/components/data-grid/DataGrid.svelte @@ -496,34 +496,32 @@ }); // Row state cache to prevent creating new objects on every scroll + // Use $derived to track dependencies synchronously (unlike $effect which is async) let rowStateCache = new WeakMap(); - let rowStateCacheDataRef: T[] | null = null; - let rowStateCacheExpandedRef: Set | null = null; - let rowStateCacheSelectedRef: Set | null = null; - // Clear row state cache when data or selection/expansion state changes - $effect(() => { - if (data !== rowStateCacheDataRef || - expandedKeys !== rowStateCacheExpandedRef || - selectedKeys !== rowStateCacheSelectedRef) { - rowStateCache = new WeakMap(); - rowStateCacheDataRef = data; - rowStateCacheExpandedRef = expandedKeys; - rowStateCacheSelectedRef = selectedKeys; - } - }); + // Track cache invalidation keys - when these change, cache is stale + let cachedSelectedKeysRef: Set | null = null; + let cachedExpandedKeysRef: Set | null = null; + let cachedHighlightedKeyRef: unknown = undefined; // Helper to get row state (memoized via WeakMap) + // Cache is invalidated synchronously when selection/expansion changes function getRowState(item: T, index: number): DataGridRowState { const actualIndex = virtualScroll ? startIndex + index : index; + // Check if cache needs to be cleared (synchronous check) + if (selectedKeys !== cachedSelectedKeysRef || + expandedKeys !== cachedExpandedKeysRef || + highlightedKey !== cachedHighlightedKeyRef) { + rowStateCache = new WeakMap(); + cachedSelectedKeysRef = selectedKeys; + cachedExpandedKeysRef = expandedKeys; + cachedHighlightedKeyRef = highlightedKey; + } + // Try to get cached state const cached = rowStateCache.get(item as object); if (cached && cached.index === actualIndex) { - // Update mutable fields that may have changed - cached.isSelected = isSelected(item[keyField]); - cached.isHighlighted = highlightedKey === item[keyField]; - cached.isExpanded = isExpanded(item[keyField]); return cached; } @@ -761,7 +759,7 @@ e.stopPropagation(); toggleSelection(item[keyField]); }} - class="flex items-center justify-center transition-colors cursor-pointer {rowState.isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-40 hover:!opacity-100'}" + class="flex items-center justify-center w-full h-full min-h-[24px] transition-colors cursor-pointer {rowState.isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-40 hover:!opacity-100'}" > {#if rowState.isSelected} @@ -870,7 +868,7 @@ - +
{placeholder} {/if} - + =3.17): Uses Bun's native password API (faster) + * On old kernels (<3.17): Uses hash-wasm (WASM-based, no getrandom dependency) + * * Argon2id is the recommended variant - resistant to both side-channel and GPU attacks */ export async function hashPassword(password: string): Promise { + // On old kernels, Bun.password.hash() crashes because it internally uses getrandom() + // Use hash-wasm as a fallback which is pure WASM and doesn't depend on the syscall + if (usingFallback()) { + const salt = secureRandomBytes(ARGON2_SALT_LENGTH); + return argon2id({ + password, + salt, + iterations: ARGON2_TIME_COST, + parallelism: ARGON2_PARALLELISM, + memorySize: ARGON2_MEMORY_COST, + hashLength: ARGON2_HASH_LENGTH, + outputType: 'encoded' // Returns PHC format: $argon2id$v=19$m=65536,t=3,p=1$... + }); + } + + // Modern kernels: use Bun's native implementation (faster) return Bun.password.hash(password, { algorithm: 'argon2id', - memoryCost: 65536, // 64 MB - timeCost: 3 // 3 iterations + memoryCost: ARGON2_MEMORY_COST, + timeCost: ARGON2_TIME_COST }); } /** * Verify a password against a hash * Uses constant-time comparison internally + * + * Both Bun.password and hash-wasm use the same PHC format, so hashes are compatible */ export async function verifyPassword(password: string, hash: string): Promise { try { + // On old kernels, use hash-wasm for verification + if (usingFallback()) { + return await argon2Verify({ password, hash }); + } + + // Modern kernels: use Bun's native implementation return await Bun.password.verify(password, hash); } catch { return false; @@ -130,7 +166,7 @@ export async function verifyPassword(password: string, hash: string): Promise { + const results = await db.select().from(environments).where(eq(environments.name, name)); + return results[0]; +} + export async function createEnvironment(env: Omit): Promise { const result = await db.insert(environments).values({ name: env.name, @@ -2487,6 +2492,8 @@ export interface StackSourceData { sourceType: StackSourceType; gitRepositoryId: number | null; gitStackId: number | null; + composePath: string | null; + envPath: string | null; createdAt: string; updatedAt: string; } @@ -2527,9 +2534,10 @@ export async function getStackSource(stackName: string, environmentId?: number | export async function getStackSources(environmentId?: number | null): Promise { let results; - if (environmentId !== undefined) { + if (environmentId !== undefined && environmentId !== null) { + // Only get stacks for the specific environment results = await db.select().from(stackSources) - .where(or(eq(stackSources.environmentId, environmentId), isNull(stackSources.environmentId))) + .where(eq(stackSources.environmentId, environmentId)) .orderBy(asc(stackSources.stackName)); } else { results = await db.select().from(stackSources).orderBy(asc(stackSources.stackName)); @@ -2563,6 +2571,8 @@ export async function upsertStackSource(data: { sourceType: StackSourceType; gitRepositoryId?: number | null; gitStackId?: number | null; + composePath?: string | null; + envPath?: string | null; }): Promise { const existing = await getStackSource(data.stackName, data.environmentId); @@ -2572,6 +2582,8 @@ export async function upsertStackSource(data: { sourceType: data.sourceType, gitRepositoryId: data.gitRepositoryId || null, gitStackId: data.gitStackId || null, + composePath: data.composePath ?? null, + envPath: data.envPath ?? null, updatedAt: new Date().toISOString() }) .where(eq(stackSources.id, existing.id)); @@ -2582,12 +2594,33 @@ export async function upsertStackSource(data: { environmentId: data.environmentId ?? null, sourceType: data.sourceType, gitRepositoryId: data.gitRepositoryId || null, - gitStackId: data.gitStackId || null + gitStackId: data.gitStackId || null, + composePath: data.composePath ?? null, + envPath: data.envPath ?? null }); return getStackSource(data.stackName, data.environmentId) as Promise; } } +export async function updateStackSource( + stackName: string, + environmentId: number | null, + updates: { composePath?: string | null; envPath?: string | null } +): Promise { + const existing = await getStackSource(stackName, environmentId); + if (!existing) return false; + + await db.update(stackSources) + .set({ + composePath: updates.composePath !== undefined ? updates.composePath : existing.composePath, + envPath: updates.envPath !== undefined ? updates.envPath : existing.envPath, + updatedAt: new Date().toISOString() + }) + .where(eq(stackSources.id, existing.id)); + + return true; +} + export async function deleteStackSource(stackName: string, environmentId?: number | null): Promise { // Delete matching record (either with specific envId or NULL) await db.delete(stackSources) @@ -3083,10 +3116,8 @@ export interface ContainerEventResult { } export async function logContainerEvent(data: ContainerEventCreateData): Promise { - // Timestamp is always a string with nanosecond precision (stored as text in both SQLite and PostgreSQL) - // For PostgreSQL, we convert to Date since the schema uses native timestamp type - const timestamp = isPostgres ? new Date(data.timestamp) : data.timestamp; - + // Timestamp is already an ISO-8601 string from event-subprocess + // Both SQLite and PostgreSQL schemas use mode: 'string' so we pass it directly const result = await db.insert(containerEvents).values({ environmentId: data.environmentId ?? null, containerId: data.containerId, @@ -3094,7 +3125,7 @@ export async function logContainerEvent(data: ContainerEventCreateData): Promise image: data.image ?? null, action: data.action, actorAttributes: data.actorAttributes ? JSON.stringify(data.actorAttributes) : null, - timestamp + timestamp: data.timestamp }).returning(); return getContainerEvent(result[0].id) as Promise; @@ -3896,6 +3927,73 @@ export async function setEventCleanupEnabled(enabled: boolean): Promise { } } +// ============================================================================= +// EXTERNAL STACK PATHS +// ============================================================================= + +const EXTERNAL_STACK_PATHS_KEY = 'external_stack_paths'; + +export async function getExternalStackPaths(): Promise { + const result = await db.select().from(settings).where(eq(settings.key, EXTERNAL_STACK_PATHS_KEY)); + if (result[0]) { + try { + const parsed = JSON.parse(result[0].value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + return []; +} + +export async function setExternalStackPaths(paths: string[]): Promise { + const jsonValue = JSON.stringify(paths); + const existing = await db.select().from(settings).where(eq(settings.key, EXTERNAL_STACK_PATHS_KEY)); + if (existing.length > 0) { + await db.update(settings) + .set({ value: jsonValue, updatedAt: new Date().toISOString() }) + .where(eq(settings.key, EXTERNAL_STACK_PATHS_KEY)); + } else { + await db.insert(settings).values({ + key: EXTERNAL_STACK_PATHS_KEY, + value: jsonValue + }); + } +} + +// ============================================================================= +// PRIMARY STACK LOCATION +// ============================================================================= + +const PRIMARY_STACK_LOCATION_KEY = 'primary_stack_location'; + +export async function getPrimaryStackLocation(): Promise { + const result = await db.select().from(settings).where(eq(settings.key, PRIMARY_STACK_LOCATION_KEY)); + if (result[0]?.value) { + return result[0].value; + } + return null; +} + +export async function setPrimaryStackLocation(path: string | null): Promise { + const existing = await db.select().from(settings).where(eq(settings.key, PRIMARY_STACK_LOCATION_KEY)); + if (path === null) { + // Delete the setting if path is null + if (existing.length > 0) { + await db.delete(settings).where(eq(settings.key, PRIMARY_STACK_LOCATION_KEY)); + } + } else if (existing.length > 0) { + await db.update(settings) + .set({ value: path, updatedAt: new Date().toISOString() }) + .where(eq(settings.key, PRIMARY_STACK_LOCATION_KEY)); + } else { + await db.insert(settings).values({ + key: PRIMARY_STACK_LOCATION_KEY, + value: path + }); + } +} + // ============================================================================= // ENVIRONMENT UPDATE CHECK SETTINGS // ============================================================================= @@ -3988,6 +4086,66 @@ export async function setDefaultTimezone(timezone: string): Promise { await setSetting('default_timezone', timezone); } +// ============================================================================= +// BACKGROUND MONITORING SETTINGS +// ============================================================================= + +/** + * Get event collection mode ('stream' or 'poll'). + * Defaults to 'stream' for real-time event streaming. + */ +export async function getEventCollectionMode(): Promise<'stream' | 'poll'> { + const value = await getSetting('event_collection_mode'); + return value || 'stream'; +} + +/** + * Set event collection mode. + */ +export async function setEventCollectionMode(mode: 'stream' | 'poll'): Promise { + await setSetting('event_collection_mode', mode); +} + +/** + * Get event poll interval in milliseconds. + * Defaults to 60000ms (60 seconds). + */ +export async function getEventPollInterval(): Promise { + const value = await getSetting('event_poll_interval'); + return value || 60000; +} + +/** + * Set event poll interval in milliseconds. + * Valid range: 30000ms (30s) to 300000ms (5min). + */ +export async function setEventPollInterval(interval: number): Promise { + if (interval < 30000 || interval > 300000) { + throw new Error('Event poll interval must be between 30s and 300s'); + } + await setSetting('event_poll_interval', interval); +} + +/** + * Get metrics collection interval in milliseconds. + * Defaults to 30000ms (30 seconds) - changed from hardcoded 10s. + */ +export async function getMetricsCollectionInterval(): Promise { + const value = await getSetting('metrics_collection_interval'); + return value || 30000; +} + +/** + * Set metrics collection interval in milliseconds. + * Valid range: 10000ms (10s) to 300000ms (5min). + */ +export async function setMetricsCollectionInterval(interval: number): Promise { + if (interval < 10000 || interval > 300000) { + throw new Error('Metrics collection interval must be between 10s and 300s'); + } + await setSetting('metrics_collection_interval', interval); +} + // ============================================================================= // STACK ENVIRONMENT VARIABLES OPERATIONS // ============================================================================= diff --git a/src/lib/server/db/drizzle.ts b/src/lib/server/db/drizzle.ts index a1e08d3..8eb7eeb 100644 --- a/src/lib/server/db/drizzle.ts +++ b/src/lib/server/db/drizzle.ts @@ -604,21 +604,21 @@ async function initializeDatabase() { logHeader('DATABASE INITIALIZATION'); if (isPostgres) { - // PostgreSQL via Bun.sql + // PostgreSQL via postgres-js (more stable than bun:sql for concurrent queries) validatePostgresUrl(config.databaseUrl!); logInfo(`Database: PostgreSQL`); logInfo(`Connection: ${maskPassword(config.databaseUrl!)}`); - const { drizzle } = await import('drizzle-orm/bun-sql'); - const { SQL } = await import('bun'); + const { drizzle } = await import('drizzle-orm/postgres-js'); + const postgres = (await import('postgres')).default; // Import PostgreSQL schema schema = await import('./schema/pg-schema.js'); if (verbose) logStep('Connecting to PostgreSQL...'); try { - rawClient = new SQL(config.databaseUrl!); + rawClient = postgres(config.databaseUrl!); db = drizzle({ client: rawClient, schema }); logSuccess('PostgreSQL connection established'); } catch (error) { diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index aeab9e6..274531e 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -332,6 +332,8 @@ export const stackSources = sqliteTable('stack_sources', { sourceType: text('source_type').notNull().default('internal'), gitRepositoryId: integer('git_repository_id').references(() => gitRepositories.id, { onDelete: 'set null' }), gitStackId: integer('git_stack_id').references(() => gitStacks.id, { onDelete: 'set null' }), + composePath: text('compose_path'), // Custom path to compose file (for stacks with non-default location) + envPath: text('env_path'), // Custom path to .env file (for stacks with non-default location) createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`), updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`) }, (table) => ({ diff --git a/src/lib/server/db/schema/pg-schema.ts b/src/lib/server/db/schema/pg-schema.ts index 6b37bf6..2aad1ca 100644 --- a/src/lib/server/db/schema/pg-schema.ts +++ b/src/lib/server/db/schema/pg-schema.ts @@ -335,6 +335,8 @@ export const stackSources = pgTable('stack_sources', { sourceType: text('source_type').notNull().default('internal'), gitRepositoryId: integer('git_repository_id').references(() => gitRepositories.id, { onDelete: 'set null' }), gitStackId: integer('git_stack_id').references(() => gitStacks.id, { onDelete: 'set null' }), + composePath: text('compose_path'), // Custom path to compose file (for stacks with non-default location) + envPath: text('env_path'), // Custom path to .env file (for stacks with non-default location) createdAt: timestamp('created_at', { mode: 'string' }).defaultNow(), updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow() }, (table) => ({ diff --git a/src/lib/server/docker.ts b/src/lib/server/docker.ts index 0b76f45..050953e 100644 --- a/src/lib/server/docker.ts +++ b/src/lib/server/docker.ts @@ -469,13 +469,16 @@ export async function dockerFetch( streaming ? 300000 : 30000 // 5 min for streaming, 30s for normal requests ); const elapsed = Date.now() - startTime; - if (elapsed > 5000) { + // Only warn for slow requests, but skip /stats which is expected to be slow (5-10s) + if (elapsed > 5000 && !path.includes('/stats')) { console.warn(`[Docker] Edge env ${config.environmentId}: ${method} ${path} took ${elapsed}ms`); } return edgeResponseToResponse(edgeResponse); - } catch (error) { + } catch (error: any) { const elapsed = Date.now() - startTime; - console.error(`[Docker] Edge env ${config.environmentId}: ${method} ${path} failed after ${elapsed}ms:`, error); + // Log error message only, not full stack trace + const msg = error?.message || String(error); + console.error(`[Docker] Edge env ${config.environmentId}: ${method} ${path} failed after ${elapsed}ms: ${msg}`); throw DockerConnectionError.fromError(error); } } @@ -491,13 +494,16 @@ export async function dockerFetch( ...bunOptions }); const elapsed = Date.now() - startTime; - if (elapsed > 5000) { + // Only warn for slow requests, but skip /stats which is expected to be slow (5-10s) + if (elapsed > 5000 && !path.includes('/stats')) { console.warn(`[Docker] Socket: ${method} ${path} took ${elapsed}ms`); } return response; - } catch (error) { + } catch (error: any) { const elapsed = Date.now() - startTime; - console.error(`[Docker] Socket: ${method} ${path} failed after ${elapsed}ms:`, error); + // Log error message only, not full stack trace + const msg = error?.message || String(error); + console.error(`[Docker] Socket: ${method} ${path} failed after ${elapsed}ms: ${msg}`); throw DockerConnectionError.fromError(error); } } else { @@ -516,21 +522,30 @@ export async function dockerFetch( } // For HTTPS with TLS certificates, we need to configure TLS - // IMPORTANT: Bun requires certificates as Buffer objects, not strings + // Pass certificate strings directly to Bun's fetch - no temp files needed if (config.type === 'https') { const tlsOptions: Record = {}; - // CA certificate - must be array of Buffers for Bun + // DISABLE TLS SESSION CACHING: Bun reuses TLS sessions across different hosts, + // which causes client certificate mismatches in mTLS scenarios. By setting + // sessionTimeout to 0, we force a fresh TLS handshake for every connection. + tlsOptions.sessionTimeout = 0; + + // Set explicit servername for SNI - helps isolate TLS contexts per host + tlsOptions.servername = config.host; + + // Load CA certificate (just this environment's CA, not composite) + // The sessionTimeout=0 should prevent session reuse across hosts if (config.ca) { - tlsOptions.ca = [Buffer.from(config.ca)]; + tlsOptions.ca = [config.ca]; } - // Client certificate and key for mTLS - must be Buffers + // Client cert and key for mTLS authentication if (config.cert) { - tlsOptions.cert = Buffer.from(config.cert); + tlsOptions.cert = [config.cert]; } if (config.key) { - tlsOptions.key = Buffer.from(config.key); + tlsOptions.key = config.key; } // Skip verification (self-signed without CA) @@ -541,8 +556,27 @@ export async function dockerFetch( } if (Object.keys(tlsOptions).length > 0) { - // @ts-ignore - Bun supports tls options with Buffer certs + // @ts-ignore - Bun supports tls options with string certs finalOptions.tls = tlsOptions; + // Force new connection for each request to prevent Bun from reusing + // a TLS session with wrong client certificates (pool key doesn't include certs) + // @ts-ignore - Bun supports keepalive option + finalOptions.keepalive = false; + } + + // Explicitly close connection to prevent TLS session reuse issues + // But only for non-streaming requests (logs, events, exec need keep-alive) + if (!streaming) { + finalOptions.headers = { + ...finalOptions.headers, + 'Connection': 'close' + }; + } + + // Optional verbose TLS debugging + if (process.env.DEBUG_TLS) { + // @ts-ignore - Bun-specific verbose option + finalOptions.verbose = true; } } @@ -550,13 +584,16 @@ export async function dockerFetch( try { const response = await fetch(url, { ...finalOptions, ...bunOptions }); const elapsed = Date.now() - startTime; - if (elapsed > 5000) { + // Only warn for slow requests, but skip /stats which is expected to be slow (5-10s) + if (elapsed > 5000 && !path.includes('/stats')) { console.warn(`[Docker] ${config.connectionType || 'direct'} ${config.host}: ${method} ${path} took ${elapsed}ms`); } return response; - } catch (error) { + } catch (error: any) { const elapsed = Date.now() - startTime; - console.error(`[Docker] ${config.connectionType || 'direct'} ${config.host}: ${method} ${path} failed after ${elapsed}ms:`, error); + // Log error message only, not full stack trace + const msg = error?.message || String(error); + console.error(`[Docker] ${config.connectionType || 'direct'} ${config.host}: ${method} ${path} failed after ${elapsed}ms: ${msg}`); throw DockerConnectionError.fromError(error); } } @@ -994,12 +1031,31 @@ export async function updateContainer(id: string, options: CreateContainerOption // Image operations export async function listImages(envId?: number | null): Promise { - const images = await dockerJsonRequest('/images/json', {}, envId); + // Fetch images and containers in parallel + const [images, containers] = await Promise.all([ + dockerJsonRequest('/images/json', {}, envId), + dockerJsonRequest('/containers/json?all=true', {}, envId).catch(() => [] as any[]) + ]); + + // Build a map of imageId -> container count + // Docker may return -1 for Containers field on some hosts, so we compute it ourselves + const imageContainerCount = new Map(); + for (const container of containers) { + const imageId = container.ImageID || container.Image; + if (imageId) { + imageContainerCount.set(imageId, (imageContainerCount.get(imageId) || 0) + 1); + } + } + return images.map((image) => ({ id: image.Id, + repoTags: image.RepoTags || [], tags: image.RepoTags || [], size: image.Size, - created: image.Created + virtualSize: image.VirtualSize || image.Size, + created: image.Created, + labels: image.Labels || {}, + containers: imageContainerCount.get(image.Id) || 0 })); } @@ -1943,7 +1999,10 @@ export async function pruneContainers(envId?: number | null) { } export async function pruneImages(dangling = true, envId?: number | null) { - const filters = dangling ? '{"dangling":["true"]}' : '{}'; + // dangling=true: only remove untagged images (default Docker behavior) + // dangling=false: remove ALL unused images including tagged ones + // Docker API quirk: to remove all unused, we pass dangling=false filter + const filters = dangling ? '{"dangling":["true"]}' : '{"dangling":["false"]}'; return dockerJsonRequest(`/images/prune?filters=${encodeURIComponent(filters)}`, { method: 'POST' }, envId); } @@ -2042,18 +2101,30 @@ export async function execInContainer( } // Get Docker events as a stream (for SSE) +// For streaming mode: call with just filters +// For polling mode: call with since and until to get a finite window of events export async function getDockerEvents( filters: Record, - envId?: number | null + envId?: number | null, + options?: { since?: string; until?: string } ): Promise | null> { const filterJson = JSON.stringify(filters); + // Build query string with optional since/until for polling mode + let queryString = `filters=${encodeURIComponent(filterJson)}`; + if (options?.since) { + queryString += `&since=${encodeURIComponent(options.since)}`; + } + if (options?.until) { + queryString += `&until=${encodeURIComponent(options.until)}`; + } + try { // Note: We use streaming: true to disable Bun's idle timeout for this long-lived connection. // The Docker events API keeps the connection open indefinitely, sending events as they occur. // Without streaming: true, Bun would terminate the connection after ~5 seconds of inactivity. const response = await dockerFetch( - `/events?filters=${encodeURIComponent(filterJson)}`, + `/events?${queryString}`, { streaming: true }, envId ); @@ -3114,8 +3185,12 @@ async function cleanupStaleVolumeHelpersForEnv(envId?: number | null): Promise { const timeoutHandle = setTimeout(() => { @@ -614,7 +612,7 @@ export function sendEdgeStreamRequest( return { requestId: '', cancel: () => {} }; } - const requestId = crypto.randomUUID(); + const requestId = secureRandomUUID(); // Initialize pendingStreamRequests if not present (can happen in dev mode due to HMR) if (!connection.pendingStreamRequests) { diff --git a/src/lib/server/metrics-collector.ts b/src/lib/server/metrics-collector.ts deleted file mode 100644 index dbccbff..0000000 --- a/src/lib/server/metrics-collector.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { saveHostMetric, getEnvironments, getEnvSetting } from './db'; -import { listContainers, getContainerStats, getDockerInfo, getDiskUsage } from './docker'; -import { sendEventNotification } from './notifications'; -import os from 'node:os'; - -const COLLECT_INTERVAL = 10000; // 10 seconds -const DISK_CHECK_INTERVAL = 300000; // 5 minutes -const DEFAULT_DISK_THRESHOLD = 80; // 80% threshold for disk warnings - -let collectorInterval: ReturnType | null = null; -let diskCheckInterval: ReturnType | null = null; - -// Track last disk warning sent per environment to avoid spamming -const lastDiskWarning: Map = new Map(); -const DISK_WARNING_COOLDOWN = 3600000; // 1 hour between warnings - -/** - * Collect metrics for a single environment - */ -async function collectEnvMetrics(env: { id: number; name: string; collectMetrics?: boolean }) { - try { - // Skip environments where metrics collection is disabled - if (env.collectMetrics === false) { - return; - } - - // Get running containers - const containers = await listContainers(false, env.id); // Only running - let totalCpuPercent = 0; - let totalMemUsed = 0; - - // Get stats for each running container - const statsPromises = containers.map(async (container) => { - try { - const stats = await getContainerStats(container.id, env.id) as any; - - // Calculate CPU percentage - const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuCount = stats.cpu_stats.online_cpus || os.cpus().length; - - let cpuPercent = 0; - if (systemDelta > 0 && cpuDelta > 0) { - cpuPercent = (cpuDelta / systemDelta) * cpuCount * 100; - } - - // Get container memory usage - const memUsage = stats.memory_stats?.usage || 0; - const memCache = stats.memory_stats?.stats?.cache || 0; - // Subtract cache from usage to get actual memory used by the container - const actualMemUsed = memUsage - memCache; - - return { cpu: cpuPercent, mem: actualMemUsed > 0 ? actualMemUsed : memUsage }; - } catch { - return { cpu: 0, mem: 0 }; - } - }); - - const statsResults = await Promise.all(statsPromises); - totalCpuPercent = statsResults.reduce((sum, v) => sum + v.cpu, 0); - totalMemUsed = statsResults.reduce((sum, v) => sum + v.mem, 0); - - // Get host total memory from Docker info (this is the remote host's memory) - const info = await getDockerInfo(env.id) as any; - const memTotal = info.MemTotal || os.totalmem(); - - // Calculate memory percentage based on container usage vs host total - const memPercent = memTotal > 0 ? (totalMemUsed / memTotal) * 100 : 0; - - // Normalize CPU by number of cores from the remote host - const cpuCount = info.NCPU || os.cpus().length; - const normalizedCpu = totalCpuPercent / cpuCount; - - // Save to database - await saveHostMetric( - normalizedCpu, - memPercent, - totalMemUsed, - memTotal, - env.id - ); - } catch (error) { - // Skip this environment if it fails (might be offline) - console.error(`Failed to collect metrics for ${env.name}:`, error); - } -} - -async function collectMetrics() { - try { - const environments = await getEnvironments(); - - // Filter enabled environments and collect metrics in parallel - const enabledEnvs = environments.filter(env => env.collectMetrics !== false); - - // Process all environments in parallel for better performance - await Promise.all(enabledEnvs.map(env => collectEnvMetrics(env))); - } catch (error) { - console.error('Metrics collection error:', error); - } -} - -/** - * Check disk space for a single environment - */ -async function checkEnvDiskSpace(env: { id: number; name: string; collectMetrics?: boolean }) { - try { - // Skip environments where metrics collection is disabled - if (env.collectMetrics === false) { - return; - } - - // Check if we're in cooldown for this environment - const lastWarningTime = lastDiskWarning.get(env.id); - if (lastWarningTime && Date.now() - lastWarningTime < DISK_WARNING_COOLDOWN) { - return; // Skip this environment, still in cooldown - } - - // Get Docker disk usage data - const diskData = await getDiskUsage(env.id) as any; - if (!diskData) return; - - // Calculate total Docker disk usage using reduce for cleaner code - let totalUsed = 0; - if (diskData.Images) { - totalUsed += diskData.Images.reduce((sum: number, img: any) => sum + (img.Size || 0), 0); - } - if (diskData.Containers) { - totalUsed += diskData.Containers.reduce((sum: number, c: any) => sum + (c.SizeRw || 0), 0); - } - if (diskData.Volumes) { - totalUsed += diskData.Volumes.reduce((sum: number, v: any) => sum + (v.UsageData?.Size || 0), 0); - } - if (diskData.BuildCache) { - totalUsed += diskData.BuildCache.reduce((sum: number, bc: any) => sum + (bc.Size || 0), 0); - } - - // Get Docker root filesystem info from Docker info - const info = await getDockerInfo(env.id) as any; - const driverStatus = info?.DriverStatus; - - // Try to find "Data Space Total" from driver status - let dataSpaceTotal = 0; - let diskPercentUsed = 0; - - if (driverStatus) { - for (const [key, value] of driverStatus) { - if (key === 'Data Space Total' && typeof value === 'string') { - dataSpaceTotal = parseSize(value); - break; - } - } - } - - // If we found total disk space, calculate percentage - if (dataSpaceTotal > 0) { - diskPercentUsed = (totalUsed / dataSpaceTotal) * 100; - } else { - // Fallback: just report absolute usage if we can't determine percentage - const GB = 1024 * 1024 * 1024; - if (totalUsed > 50 * GB) { - await sendEventNotification('disk_space_warning', { - title: 'High Docker disk usage', - message: `Environment "${env.name}" is using ${formatSize(totalUsed)} of Docker disk space`, - type: 'warning' - }, env.id); - lastDiskWarning.set(env.id, Date.now()); - } - return; - } - - // Check against threshold - const threshold = await getEnvSetting('disk_warning_threshold', env.id) || DEFAULT_DISK_THRESHOLD; - if (diskPercentUsed >= threshold) { - console.log(`[Metrics] Docker disk usage for ${env.name}: ${diskPercentUsed.toFixed(1)}% (threshold: ${threshold}%)`); - - await sendEventNotification('disk_space_warning', { - title: 'Disk space warning', - message: `Environment "${env.name}" Docker disk usage is at ${diskPercentUsed.toFixed(1)}% (${formatSize(totalUsed)} used)`, - type: 'warning' - }, env.id); - - lastDiskWarning.set(env.id, Date.now()); - } - } catch (error) { - // Skip this environment if it fails - console.error(`Failed to check disk space for ${env.name}:`, error); - } -} - -/** - * Check Docker disk usage and send warnings if above threshold - */ -async function checkDiskSpace() { - try { - const environments = await getEnvironments(); - - // Filter enabled environments and check disk space in parallel - const enabledEnvs = environments.filter(env => env.collectMetrics !== false); - - // Process all environments in parallel for better performance - await Promise.all(enabledEnvs.map(env => checkEnvDiskSpace(env))); - } catch (error) { - console.error('Disk space check error:', error); - } -} - -/** - * Parse size string like "107.4GB" to bytes - */ -function parseSize(sizeStr: string): number { - const units: Record = { - 'B': 1, - 'KB': 1024, - 'MB': 1024 * 1024, - 'GB': 1024 * 1024 * 1024, - 'TB': 1024 * 1024 * 1024 * 1024 - }; - - const match = sizeStr.match(/^([\d.]+)\s*([KMGT]?B)$/i); - if (!match) return 0; - - const value = parseFloat(match[1]); - const unit = match[2].toUpperCase(); - return value * (units[unit] || 1); -} - -/** - * Format bytes to human readable string - */ -function formatSize(bytes: number): string { - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let unitIndex = 0; - let size = bytes; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - return `${size.toFixed(1)} ${units[unitIndex]}`; -} - -export function startMetricsCollector() { - if (collectorInterval) return; // Already running - - console.log('Starting server-side metrics collector (every 10s)'); - - // Initial collection - collectMetrics(); - - // Schedule regular collection - collectorInterval = setInterval(collectMetrics, COLLECT_INTERVAL); - - // Start disk space checking (every 5 minutes) - console.log('Starting disk space monitoring (every 5 minutes)'); - checkDiskSpace(); // Initial check - diskCheckInterval = setInterval(checkDiskSpace, DISK_CHECK_INTERVAL); -} - -export function stopMetricsCollector() { - if (collectorInterval) { - clearInterval(collectorInterval); - collectorInterval = null; - } - if (diskCheckInterval) { - clearInterval(diskCheckInterval); - diskCheckInterval = null; - } - lastDiskWarning.clear(); - console.log('Metrics collector stopped'); -} diff --git a/src/lib/server/stacks.ts b/src/lib/server/stacks.ts index 35d1021..ad4dddb 100644 --- a/src/lib/server/stacks.ts +++ b/src/lib/server/stacks.ts @@ -5,8 +5,8 @@ * All lifecycle operations use docker compose commands. */ -import { existsSync, mkdirSync, rmSync, readdirSync, cpSync, statSync } from 'node:fs'; -import { join, resolve } from 'node:path'; +import { existsSync, mkdirSync, rmSync, readdirSync, cpSync, statSync, unlinkSync, renameSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve, dirname } from 'node:path'; import { getEnvironment, getStackEnvVarsAsRecord, @@ -88,22 +88,6 @@ export interface DeployStackOptions { // ERRORS // ============================================================================= -/** - * Error for operations on external stacks without compose files - */ -export class ExternalStackError extends Error { - public readonly stackName: string; - - constructor(stackName: string) { - super( - `Stack "${stackName}" was created outside of Dockhand. ` + - `To manage this stack, first import it by clicking the Import button in the stack menu.` - ); - this.name = 'ExternalStackError'; - this.stackName = stackName; - } -} - /** * Error when compose file is missing for a managed stack */ @@ -232,6 +216,65 @@ export function getStacksDir(): string { return _stacksDir; } +/** + * Get stack directory path for a specific environment. + * New stacks use: $DATA_DIR/stacks/// + * Legacy stacks (no env): $DATA_DIR/stacks// + * + * Automatically looks up environment name from database. + */ +export async function getStackDir(stackName: string, envId?: number | null): Promise { + const stacksDir = getStacksDir(); + if (envId) { + const env = await getEnvironment(envId); + if (env) { + return join(stacksDir, env.name, stackName); + } + } + // Legacy path for stacks without environment + return join(stacksDir, stackName); +} + +/** + * Find stack directory, checking paths in order: + * 1. New path (envName): $DATA_DIR/stacks/// + * 2. ID-based path (envId): $DATA_DIR/stacks/// + * 3. Legacy path: $DATA_DIR/stacks// + * + * Automatically looks up environment name from database. + * Always checks legacy path for backwards compatibility with pre-env stacks. + */ +export async function findStackDir(stackName: string, envId?: number | null): Promise { + const stacksDir = getStacksDir(); + + // Look up environment name if we have an ID + if (envId) { + const env = await getEnvironment(envId); + + // 1. Check new path (with envName) + if (env) { + const namePath = join(stacksDir, env.name, stackName); + if (existsSync(namePath)) { + return namePath; + } + } + + // 2. Check ID-based path + const idPath = join(stacksDir, String(envId), stackName); + if (existsSync(idPath)) { + return idPath; + } + } + + // 3. Always check legacy path (stacks created before env-scoping was added) + const legacyPath = join(stacksDir, stackName); + if (existsSync(legacyPath)) { + return legacyPath; + } + + return null; +} + /** * List stacks that have compose files stored locally */ @@ -256,31 +299,116 @@ export function listManagedStacks(): string[] { // ============================================================================= /** - * Get compose file content for a stack + * Result type for getStackComposeFile + */ +export interface GetComposeFileResult { + success: boolean; + content?: string; + stackDir?: string; + error?: string; + needsFileLocation?: boolean; + composePath?: string | null; + envPath?: string | null; + suggestedEnvPath?: string; +} + +/** + * Get compose file content for a stack. + * + * Unified logic for all stacks: + * - If composePath is set in DB → use custom path + * - If composePath is NULL → use default location (data/stacks/{env}/{name}/) + * - If no source record and no files found → return needsFileLocation: true */ export async function getStackComposeFile( - stackName: string -): Promise<{ success: boolean; content?: string; stackDir?: string; error?: string }> { - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); + stackName: string, + envId?: number | null +): Promise { + const source = await getStackSource(stackName, envId); - // Check all common compose file names (Docker Compose v1 and v2 naming conventions) - const composeFileNames = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']; + // Case 1: Stack not in database = untracked (discovered from Docker but not imported) + // User must select the compose file location - don't guess from default location + if (!source) { + return { + success: false, + needsFileLocation: true, + error: `Select the compose file location for stack "${stackName}"` + }; + } + + // Case 2: Stack has custom composePath set - use it + if (source.composePath) { + try { + if (!existsSync(source.composePath)) { + return { + success: false, + error: `Compose file no longer accessible at ${source.composePath}. The file may have been moved or deleted.`, + composePath: source.composePath, + envPath: source.envPath + }; + } + + const content = await Bun.file(source.composePath).text(); + const stackDir = dirname(source.composePath); + + // For custom paths, suggest .env next to compose if envPath not set + let suggestedEnvPath: string | undefined; + if (source.envPath === null) { + suggestedEnvPath = source.composePath.replace(/\/[^/]+$/, '/.env'); + } - for (const fileName of composeFileNames) { - const file = Bun.file(join(stackDir, fileName)); - if (await file.exists()) { return { success: true, - content: await file.text(), - stackDir + content, + stackDir, + composePath: source.composePath, + envPath: source.envPath, + suggestedEnvPath + }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return { + success: false, + error: `Failed to read compose file: ${message}`, + composePath: source.composePath, + envPath: source.envPath }; } } + // Case 3: Stack is in DB but no custom path - check default location + // This is for stacks created in Dockhand using the default data directory + const stackDir = await findStackDir(stackName, envId); + + if (stackDir) { + // Check all common compose file names + const composeFileNames = ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']; + + for (const fileName of composeFileNames) { + const actualComposePath = join(stackDir, fileName); + const file = Bun.file(actualComposePath); + if (await file.exists()) { + // Check for .env file in the same directory + const envFilePath = join(stackDir, '.env'); + const envExists = existsSync(envFilePath); + + return { + success: true, + content: await file.text(), + stackDir, + // Always return the actual resolved paths for display + composePath: actualComposePath, + envPath: envExists ? envFilePath : null + }; + } + } + } + + // Case 4: Stack is in DB but compose file not found - need user to specify location return { success: false, - error: `Compose file not found for stack "${stackName}". The stack may have been created outside of Dockhand.` + needsFileLocation: true, + error: `Select the compose file location for stack "${stackName}"` }; } @@ -289,22 +417,206 @@ export async function getStackComposeFile( * @param name - Stack name * @param content - Compose file content * @param create - If true, creates a new stack (fails if exists). If false, updates existing (fails if not exists). + * @param envId - Environment ID for path scoping */ export async function saveStackComposeFile( name: string, content: string, - create = false + create = false, + envId?: number | null, + options?: { + composePath?: string; // Custom compose file path + envPath?: string | null; // Custom env path (null = default, '' = none) + moveFromDir?: string; // Old directory to move all files from when path changes + oldComposePath?: string; // Old compose file path for renaming + oldEnvPath?: string; // Old env file path for renaming + } ): Promise<{ success: boolean; error?: string }> { - // Validate stack name - if (!/^[a-zA-Z0-9_-]+$/.test(name)) { + // Validate stack name - Docker Compose requires lowercase alphanumeric, hyphens, underscores + // Must also start with a letter or number + if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) { return { success: false, - error: 'Stack name can only contain letters, numbers, hyphens, and underscores' + error: 'Stack name must be lowercase, start with a letter or number, and contain only letters, numbers, hyphens, and underscores' }; } - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, name); + // Check if this stack has a custom compose path configured, or if one was provided + const source = await getStackSource(name, envId); + const composePath = options?.composePath || source?.composePath; + + // Handle compose file move/rename when path changes + if (options?.oldComposePath && options?.composePath && + options.oldComposePath !== options.composePath && + existsSync(options.oldComposePath)) { + const newDir = dirname(options.composePath); + + // Ensure target directory exists + if (!existsSync(newDir)) { + try { + mkdirSync(newDir, { recursive: true }); + } catch (err: any) { + console.warn(`[Stack] Failed to create directory ${newDir}: ${err.message}`); + } + } + + // Move/rename the compose file to new location + try { + renameSync(options.oldComposePath, options.composePath); + console.log(`[Stack] Moved compose file: ${options.oldComposePath} -> ${options.composePath}`); + } catch (renameErr: any) { + // If rename fails (e.g., cross-filesystem), try copy+delete + if (renameErr.code === 'EXDEV') { + try { + const data = readFileSync(options.oldComposePath); + writeFileSync(options.composePath, data); + unlinkSync(options.oldComposePath); + console.log(`[Stack] Copied compose file (cross-fs): ${options.oldComposePath} -> ${options.composePath}`); + } catch (err: any) { + console.warn(`[Stack] Failed to copy compose file: ${err.message}`); + } + } else { + console.warn(`[Stack] Failed to move compose file: ${renameErr.message}`); + } + } + } + + // Handle env file move/rename when path changes + if (options?.oldEnvPath && options?.envPath && + options.oldEnvPath !== options.envPath && + existsSync(options.oldEnvPath)) { + const newDir = dirname(options.envPath); + + // Ensure target directory exists + if (!existsSync(newDir)) { + try { + mkdirSync(newDir, { recursive: true }); + } catch (err: any) { + console.warn(`[Stack] Failed to create directory ${newDir}: ${err.message}`); + } + } + + // Move/rename the env file to new location + try { + renameSync(options.oldEnvPath, options.envPath); + console.log(`[Stack] Moved env file: ${options.oldEnvPath} -> ${options.envPath}`); + } catch (renameErr: any) { + // If rename fails (e.g., cross-filesystem), try copy+delete + if (renameErr.code === 'EXDEV') { + try { + const data = readFileSync(options.oldEnvPath); + writeFileSync(options.envPath, data); + unlinkSync(options.oldEnvPath); + console.log(`[Stack] Copied env file (cross-fs): ${options.oldEnvPath} -> ${options.envPath}`); + } catch (err: any) { + console.warn(`[Stack] Failed to copy env file: ${err.message}`); + } + } else { + console.warn(`[Stack] Failed to move env file: ${renameErr.message}`); + } + } + } + + // Move all files from old directory to new directory when path changes + // Get the new directory from composePath + const newDir = options?.composePath ? dirname(options.composePath) : null; + + if (options?.moveFromDir && newDir && options.moveFromDir !== newDir && existsSync(options.moveFromDir)) { + try { + // Ensure new directory exists + if (!existsSync(newDir)) { + mkdirSync(newDir, { recursive: true }); + } + + // Move all files from old directory to new directory + const files = readdirSync(options.moveFromDir); + for (const file of files) { + const oldFilePath = join(options.moveFromDir, file); + const newFilePath = join(newDir, file); + + try { + // Use rename for atomic move (same filesystem) or copy+delete for cross-filesystem + renameSync(oldFilePath, newFilePath); + console.log(`[Stack] Moved file: ${oldFilePath} -> ${newFilePath}`); + } catch (renameErr: any) { + // If rename fails (e.g., cross-filesystem), try copy+delete + if (renameErr.code === 'EXDEV') { + const stat = statSync(oldFilePath); + if (stat.isDirectory()) { + // For directories, use recursive copy + cpSync(oldFilePath, newFilePath, { recursive: true }); + rmSync(oldFilePath, { recursive: true, force: true }); + } else { + // For files, read and write + const data = readFileSync(oldFilePath); + writeFileSync(newFilePath, data); + unlinkSync(oldFilePath); + } + console.log(`[Stack] Copied file (cross-fs): ${oldFilePath} -> ${newFilePath}`); + } else { + throw renameErr; + } + } + } + + // Remove old directory if it's now empty + try { + const remaining = readdirSync(options.moveFromDir); + if (remaining.length === 0) { + rmSync(options.moveFromDir, { recursive: true, force: true }); + console.log(`[Stack] Removed empty old directory: ${options.moveFromDir}`); + } + } catch { + // Ignore errors when checking/removing old directory + } + } catch (err: any) { + console.warn(`[Stack] Failed to move files from ${options.moveFromDir} to ${newDir}: ${err.message}`); + // Continue with save even if move fails - new files will be written anyway + } + } + + // If a custom composePath is being set (new or update), save it to the database + if (options?.composePath || options?.envPath !== undefined) { + await upsertStackSource({ + stackName: name, + environmentId: envId ?? null, + sourceType: 'internal', + composePath: options?.composePath || source?.composePath || null, + envPath: options?.envPath !== undefined ? options.envPath : (source?.envPath ?? null) + }); + } + + if (composePath) { + // Write directly to the custom compose file path + // Ensure parent directory exists for custom paths + const parentDir = dirname(composePath); + if (!existsSync(parentDir)) { + try { + mkdirSync(parentDir, { recursive: true }); + } catch (err: any) { + return { success: false, error: `Failed to create directory for compose file: ${err.message}` }; + } + } + try { + await Bun.write(composePath, content); + return { success: true }; + } catch (err: any) { + return { success: false, error: `Failed to save compose file: ${err.message}` }; + } + } + + // For creates, use new path; for updates, find existing path first + let stackDir: string; + if (create) { + stackDir = await getStackDir(name, envId); + } else { + const existingDir = await findStackDir(name, envId); + if (!existingDir) { + return { success: false, error: `Stack "${name}" not found` }; + } + stackDir = existingDir; + } + const composeFile = join(stackDir, 'docker-compose.yml'); const exists = existsSync(stackDir); @@ -323,11 +635,6 @@ export async function saveStackComposeFile( } catch (err: any) { return { success: false, error: `Failed to create stack directory: ${err.message}` }; } - } else { - // Updating existing stack - must exist - if (!exists) { - return { success: false, error: `Stack "${name}" not found` }; - } } try { @@ -338,6 +645,68 @@ export async function saveStackComposeFile( } } +// ============================================================================= +// REGISTRY AUTHENTICATION +// ============================================================================= + +/** + * Login to all configured Docker registries before running compose commands. + * This ensures that `docker compose up` can pull images from private registries. + */ +async function loginToRegistries(dockerHost?: string, logPrefix = '[Stack]'): Promise { + const { getRegistries } = await import('./db.js'); + const registries = await getRegistries(); + + if (registries.length === 0) { + return; + } + + const spawnEnv: Record = { ...(process.env as Record) }; + if (dockerHost) { + spawnEnv.DOCKER_HOST = dockerHost; + } + + for (const reg of registries) { + if (!reg.username || !reg.password) { + continue; // Skip registries without credentials + } + + try { + // Extract registry host from URL + const url = new URL(reg.url); + const registryHost = url.host; + + console.log(`${logPrefix} Logging into registry: ${registryHost}`); + + const proc = Bun.spawn( + ['docker', 'login', '-u', reg.username, '--password-stdin', registryHost], + { + env: spawnEnv, + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe' + } + ); + + // Write password to stdin + const writer = proc.stdin.getWriter(); + await writer.write(new TextEncoder().encode(reg.password)); + await writer.close(); + + const exitCode = await proc.exited; + + if (exitCode === 0) { + console.log(`${logPrefix} Successfully logged into ${registryHost}`); + } else { + const stderr = await new Response(proc.stderr).text(); + console.error(`${logPrefix} Failed to login to ${registryHost}: ${stderr}`); + } + } catch (e) { + console.error(`${logPrefix} Error logging into registry ${reg.name}:`, e); + } + } +} + // ============================================================================= // COMPOSE COMMAND EXECUTION // ============================================================================= @@ -364,11 +733,14 @@ async function executeLocalCompose( envVars?: Record, secretVars?: Record, forceRecreate?: boolean, - removeVolumes?: boolean + removeVolumes?: boolean, + envId?: number | null ): Promise { const logPrefix = `[Stack:${stackName}]`; - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); + // For operations that write (up), use getStackDir; for others, try to find existing first + const stackDir = operation === 'up' + ? await getStackDir(stackName, envId) + : (await findStackDir(stackName, envId) || await getStackDir(stackName, envId)); mkdirSync(stackDir, { recursive: true }); const composeFile = join(stackDir, 'docker-compose.yml'); @@ -433,6 +805,11 @@ async function executeLocalCompose( console.log(`${logPrefix} Env vars being injected (masked):`, JSON.stringify(maskSecrets(envVars), null, 2)); } + // Login to registries before pulling images + if (operation === 'up' || operation === 'pull') { + await loginToRegistries(dockerHost, logPrefix); + } + try { console.log(`${logPrefix} Spawning docker compose process...`); const proc = Bun.spawn(args, { @@ -577,6 +954,20 @@ async function executeComposeViaHawser( } } + // Fetch registry credentials for Hawser to use for docker login + const { getRegistries } = await import('./db.js'); + const allRegistries = await getRegistries(); + const registries = allRegistries + .filter(r => r.username && r.password) + .map(r => ({ + url: r.url, + username: r.username!, + password: r.password! + })); + if (registries.length > 0) { + console.log(`${logPrefix} Sending ${registries.length} registry credentials to Hawser`); + } + const body = JSON.stringify({ operation, projectName: stackName, @@ -584,7 +975,8 @@ async function executeComposeViaHawser( envVars: allEnvVars, // All vars (including secrets) - Hawser injects via shell env files, // Files including .env (secrets NOT in .env file) forceRecreate: forceRecreate || false, - removeVolumes: removeVolumes || false + removeVolumes: removeVolumes || false, + registries // Registry credentials for docker login }); console.log(`${logPrefix} Sending request to Hawser agent...`); @@ -665,7 +1057,8 @@ async function executeComposeCommand( envVars, secretVars, forceRecreate, - removeVolumes + removeVolumes, + envId ); } @@ -695,7 +1088,8 @@ async function executeComposeCommand( envVars, secretVars, forceRecreate, - removeVolumes + removeVolumes, + envId ); } @@ -709,7 +1103,8 @@ async function executeComposeCommand( envVars, secretVars, forceRecreate, - removeVolumes + removeVolumes, + envId ); } } @@ -806,6 +1201,37 @@ async function getStackContainers(stackName: string, envId?: number | null): Pro return containers.filter((c) => c.labels['com.docker.compose.project'] === stackName); } +/** + * Extract path hints from Docker container labels for a stack. + * Docker Compose adds labels like: + * - com.docker.compose.project.working_dir: /path/to/stack + * - com.docker.compose.project.config_files: /path/to/docker-compose.yml[,...] + */ +export async function getStackPathHints( + stackName: string, + envId?: number | null +): Promise<{ + workingDir: string | null; + configFiles: string[] | null; +}> { + const containers = await getStackContainers(stackName, envId); + + if (containers.length === 0) { + return { workingDir: null, configFiles: null }; + } + + // Get labels from first container (all containers in stack have same project labels) + const labels = containers[0].labels || {}; + + const workingDir = labels['com.docker.compose.project.working_dir'] || null; + const configFilesRaw = labels['com.docker.compose.project.config_files'] || null; + + // Config files can be comma-separated if multiple compose files were used + const configFiles = configFilesRaw ? configFilesRaw.split(',').map((f: string) => f.trim()) : null; + + return { workingDir, configFiles }; +} + /** * Helper to perform container-based operations for external stacks * Used as fallback when no compose file exists. @@ -878,12 +1304,25 @@ async function withContainerFallback( // ============================================================================= /** - * Ensure we have a compose file for operations, throw appropriate error if not. + * Result type for requireComposeFile - can indicate stack needs file location + */ +export interface RequireComposeResult { + success: boolean; + content?: string; + envVars?: Record; + secretVars?: Record; + needsFileLocation?: boolean; + error?: string; +} + +/** + * Get compose file and env vars for stack operations. * * Returns: * - content: The compose file content * - envVars: Non-secret variables (from .env file, with DB fallback) * - secretVars: Secret variables (from DB only, for shell injection) + * - needsFileLocation: true if stack needs user to specify file paths * * SECURITY: Secrets are NEVER written to .env files. They are stored in the database * and injected via shell environment variables at runtime. @@ -891,16 +1330,22 @@ async function withContainerFallback( async function requireComposeFile( stackName: string, envId?: number | null -): Promise<{ content: string; envVars: Record; secretVars: Record }> { - const composeResult = await getStackComposeFile(stackName); +): Promise { + const composeResult = await getStackComposeFile(stackName, envId); + // If compose file not found, return info about what's needed if (!composeResult.success) { - // Check if this is an external stack - const source = await getStackSource(stackName, envId); - if (!source || source.sourceType === 'external') { - throw new ExternalStackError(stackName); + if (composeResult.needsFileLocation) { + return { + success: false, + needsFileLocation: true, + error: composeResult.error + }; } - throw new ComposeFileNotFoundError(stackName); + return { + success: false, + error: composeResult.error || `Compose file not found for stack "${stackName}"` + }; } // Get SECRET variables from database (for shell injection at runtime) @@ -910,12 +1355,26 @@ async function requireComposeFile( // Get non-secret variables from database (for backward compatibility) const dbNonSecretVars = await getNonSecretEnvVarsAsRecord(stackName, envId); - // Read non-secret vars from .env file (user can edit this file manually) - const stackDir = join(getStacksDir(), stackName); - const envFilePath = join(stackDir, '.env'); + // Read non-secret vars from .env file + // For stacks with custom path, use the env path if set (and not empty string which means "no env file") + // Otherwise, use the .env file in the stack directory + let envFilePath: string | null = null; + + if (composeResult.composePath && composeResult.envPath) { + // Custom compose path with explicit env path + envFilePath = composeResult.envPath; + } else if (composeResult.composePath && composeResult.envPath === '') { + // Custom compose path with explicit "no env file" - don't read any file + envFilePath = null; + } else { + // Default location - look for .env in stack directory + const stackDir = composeResult.stackDir || await findStackDir(stackName, envId) || await getStackDir(stackName, envId); + envFilePath = join(stackDir, '.env'); + } + let fileEnvVars: Record = {}; - if (existsSync(envFilePath)) { + if (envFilePath && existsSync(envFilePath)) { try { const content = await Bun.file(envFilePath).text(); for (const line of content.split('\n')) { @@ -941,85 +1400,80 @@ async function requireComposeFile( // This ensures external edits to .env are respected during deployment const envVars = { ...dbNonSecretVars, ...fileEnvVars }; - return { content: composeResult.content!, envVars, secretVars }; + return { success: true, content: composeResult.content!, envVars, secretVars }; } /** * Start a stack using docker compose up - * Falls back to individual container start for external stacks + * Falls back to individual container start for stacks without compose files */ export async function startStack( stackName: string, envId?: number | null ): Promise { - try { - const { content, envVars, secretVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('up', { stackName, envId }, content, envVars, secretVars); - } catch (err) { - if (err instanceof ExternalStackError) { - return withContainerFallback(stackName, envId, 'start'); - } - throw err; + const result = await requireComposeFile(stackName, envId); + + if (!result.success) { + // No compose file - fall back to container-based operations + return withContainerFallback(stackName, envId, 'start'); } + + return executeComposeCommand('up', { stackName, envId }, result.content!, result.envVars, result.secretVars); } /** * Stop a stack using docker compose stop - * Falls back to individual container stop for external stacks + * Falls back to individual container stop for stacks without compose files */ export async function stopStack( stackName: string, envId?: number | null ): Promise { - try { - const { content, envVars, secretVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('stop', { stackName, envId }, content, envVars, secretVars); - } catch (err) { - if (err instanceof ExternalStackError) { - return withContainerFallback(stackName, envId, 'stop'); - } - throw err; + const result = await requireComposeFile(stackName, envId); + + if (!result.success) { + // No compose file - fall back to container-based operations + return withContainerFallback(stackName, envId, 'stop'); } + + return executeComposeCommand('stop', { stackName, envId }, result.content!, result.envVars, result.secretVars); } /** * Restart a stack using docker compose restart - * Falls back to individual container restart for external stacks + * Falls back to individual container restart for stacks without compose files */ export async function restartStack( stackName: string, envId?: number | null ): Promise { - try { - const { content, envVars, secretVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('restart', { stackName, envId }, content, envVars, secretVars); - } catch (err) { - if (err instanceof ExternalStackError) { - return withContainerFallback(stackName, envId, 'restart'); - } - throw err; + const result = await requireComposeFile(stackName, envId); + + if (!result.success) { + // No compose file - fall back to container-based operations + return withContainerFallback(stackName, envId, 'restart'); } + + return executeComposeCommand('restart', { stackName, envId }, result.content!, result.envVars, result.secretVars); } /** * Down a stack using docker compose down (removes containers, keeps files) - * For external stacks, this is equivalent to stop (no compose file to "down") + * For stacks without compose files, this is equivalent to stop */ export async function downStack( stackName: string, envId?: number | null, removeVolumes = false ): Promise { - try { - const { content, envVars, secretVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('down', { stackName, envId, removeVolumes }, content, envVars, secretVars); - } catch (err) { - if (err instanceof ExternalStackError) { - // For external stacks, down is the same as stop (no compose file to tear down) - return withContainerFallback(stackName, envId, 'stop'); - } - throw err; + const result = await requireComposeFile(stackName, envId); + + if (!result.success) { + // No compose file - down is the same as stop + return withContainerFallback(stackName, envId, 'stop'); } + + return executeComposeCommand('down', { stackName, envId, removeVolumes }, result.content!, result.envVars, result.secretVars); } /** @@ -1080,8 +1534,7 @@ export async function removeStack( const cleanupErrors: string[] = []; // Delete compose file and directory - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); + const stackDir = await findStackDir(stackName, envId) || await getStackDir(stackName, envId); if (existsSync(stackDir)) { try { rmSync(stackDir, { recursive: true, force: true }); @@ -1166,19 +1619,19 @@ export async function deployStack(options: DeployStackOptions): Promise { - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, name); + const stackDir = await getStackDir(name, envId); // Read all files from source directory if provided (for Hawser deployments) let stackFiles: Record | undefined; @@ -1248,9 +1701,16 @@ export async function pullStackImages( stackName: string, envId?: number | null ): Promise<{ success: boolean; output?: string; error?: string }> { - const { content, envVars, secretVars } = await requireComposeFile(stackName, envId); + const result = await requireComposeFile(stackName, envId); - return executeComposeCommand('pull', { stackName, envId }, content, envVars, secretVars); + if (!result.success) { + return { + success: false, + error: result.error || 'Compose file not found' + }; + } + + return executeComposeCommand('pull', { stackName, envId }, result.content!, result.envVars, result.secretVars); } // ============================================================================= @@ -1279,10 +1739,19 @@ export async function saveStackEnvVarsToDb( */ export async function writeStackEnvFile( stackName: string, - variables: { key: string; value: string; isSecret?: boolean }[] + variables: { key: string; value: string; isSecret?: boolean }[], + envId?: number | null, + customEnvPath?: string ): Promise { - const stacksDir = getStacksDir(); - const envFilePath = join(stacksDir, stackName, '.env'); + const envFilePath = customEnvPath + ? customEnvPath + : join(await findStackDir(stackName, envId) || await getStackDir(stackName, envId), '.env'); + + // Ensure parent directory exists + const dir = dirname(envFilePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } // SECURITY: Only write non-secret variables to .env file // Secrets are stored in DB and injected via shell environment at runtime @@ -1302,17 +1771,20 @@ export async function writeStackEnvFile( */ export async function writeRawStackEnvFile( stackName: string, - rawContent: string + rawContent: string, + envId?: number | null, + customEnvPath?: string ): Promise { - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); - - // Ensure stack directory exists - if (!existsSync(stackDir)) { - mkdirSync(stackDir, { recursive: true }); + const envFilePath = customEnvPath + ? customEnvPath + : join(await findStackDir(stackName, envId) || await getStackDir(stackName, envId), '.env'); + + // Ensure parent directory exists + const dir = dirname(envFilePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); } - const envFilePath = join(stackDir, '.env'); await Bun.write(envFilePath, rawContent); } @@ -1329,12 +1801,13 @@ export async function writeRawStackEnvFile( export async function saveStackEnvVars( stackName: string, variables: { key: string; value: string; isSecret?: boolean }[], - envId?: number | null + envId?: number | null, + customEnvPath?: string ): Promise { // Save to database for secret tracking await saveStackEnvVarsToDb(stackName, variables, envId); // Write .env file to disk for Docker Compose - await writeStackEnvFile(stackName, variables); + await writeStackEnvFile(stackName, variables, envId, customEnvPath); } // ============================================================================= diff --git a/src/lib/server/subprocess-manager.ts b/src/lib/server/subprocess-manager.ts index 6db4ec5..f29c770 100644 --- a/src/lib/server/subprocess-manager.ts +++ b/src/lib/server/subprocess-manager.ts @@ -102,7 +102,12 @@ export interface ShutdownCommand { type: 'shutdown'; } -export type MainProcessCommand = RefreshEnvironmentsCommand | ShutdownCommand; +export interface UpdateIntervalCommand { + type: 'update_interval'; + intervalMs: number; +} + +export type MainProcessCommand = RefreshEnvironmentsCommand | ShutdownCommand | UpdateIntervalCommand; // Subprocess configuration interface SubprocessConfig { @@ -198,6 +203,20 @@ class SubprocessManager { this.sendToEvents({ type: 'refresh_environments' }); } + /** + * Send message to metrics subprocess + */ + sendToMetricsSubprocess(message: MainProcessCommand): void { + this.sendToMetrics(message); + } + + /** + * Send message to events subprocess + */ + sendToEventsSubprocess(message: MainProcessCommand): void { + this.sendToEvents(message); + } + /** * Start the metrics collection subprocess */ @@ -591,3 +610,21 @@ export function refreshSubprocessEnvironments(): void { manager.refreshEnvironments(); } } + +/** + * Send message to event subprocess + */ +export function sendToEventSubprocess(message: MainProcessCommand): void { + if (manager) { + manager.sendToEventsSubprocess(message); + } +} + +/** + * Send message to metrics subprocess + */ +export function sendToMetricsSubprocess(message: MainProcessCommand): void { + if (manager) { + manager.sendToMetricsSubprocess(message); + } +} diff --git a/src/lib/server/subprocesses/event-subprocess.ts b/src/lib/server/subprocesses/event-subprocess.ts index bf37d58..5abfc68 100644 --- a/src/lib/server/subprocesses/event-subprocess.ts +++ b/src/lib/server/subprocesses/event-subprocess.ts @@ -7,7 +7,7 @@ * Communication with main process via IPC (process.send). */ -import { getEnvironments, type ContainerEventAction } from '../db'; +import { getEnvironments, getEventCollectionMode, getEventPollInterval, type ContainerEventAction } from '../db'; import { getDockerEvents } from '../docker'; import type { MainProcessCommand } from '../subprocess-manager'; @@ -19,9 +19,15 @@ const MAX_RECONNECT_DELAY = 60000; // 1 minute max // Only send notifications on status CHANGES, not on every reconnect attempt const environmentOnlineStatus: Map = new Map(); -// Active collectors per environment +// Active collectors per environment (for streaming mode) const collectors: Map = new Map(); +// Poll intervals per environment (for polling mode) +const pollIntervals: Map> = new Map(); + +// Last poll timestamp per environment (for polling mode) +const lastPollTime: Map = new Map(); + // Recent event cache for deduplication (key: timeNano-containerId-action) const recentEvents: Map = new Map(); const DEDUP_WINDOW_MS = 5000; // 5 second window for deduplication @@ -30,6 +36,10 @@ const CACHE_CLEANUP_INTERVAL_MS = 30000; // Clean up cache every 30 seconds let cacheCleanupInterval: ReturnType | null = null; let isShuttingDown = false; +// Track current settings to detect changes +let currentPollInterval: number = 60000; +let currentMode: 'stream' | 'poll' = 'stream'; + // Actions we care about for container activity const CONTAINER_ACTIONS: ContainerEventAction[] = [ 'create', @@ -211,6 +221,76 @@ function processEvent(event: DockerEvent, envId: number) { }); } +/** + * Poll events for a specific environment (polling mode) + */ +async function pollEnvironmentEvents(envId: number, envName: string) { + try { + // Calculate 'since' timestamp (use last poll time, or start from 30s ago if first poll) + const now = Math.floor(Date.now() / 1000); // Unix timestamp in seconds + const since = lastPollTime.get(envId) || (now - 30); // Default to 30s ago on first poll + + // Fetch events since last check until now + // IMPORTANT: 'until' is required for polling mode, otherwise Docker keeps the connection open + const eventStream = await getDockerEvents( + { type: ['container'] }, + envId, + { since: since.toString(), until: now.toString() } + ); + + if (!eventStream) { + console.error(`[EventSubprocess] Failed to fetch events for ${envName}`); + updateEnvironmentStatus(envId, envName, false, 'Failed to fetch Docker events'); + return; + } + + // Mark environment as online + updateEnvironmentStatus(envId, envName, true); + + // Read and process all events + const reader = eventStream.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.trim()) { + try { + const event = JSON.parse(line) as DockerEvent; + processEvent(event, envId); + } catch { + // Ignore parse errors + } + } + } + } + } finally { + try { + reader.releaseLock(); + } catch { + // Reader already released + } + } + + // Update last poll time + lastPollTime.set(envId, now); + + } catch (error: any) { + if (!isShuttingDown) { + console.error(`[EventSubprocess] Poll error for ${envName}:`, error.message); + updateEnvironmentStatus(envId, envName, false, error.message); + } + } +} + /** * Start collecting events for a specific environment */ @@ -332,7 +412,42 @@ async function startEnvironmentCollector(envId: number, envName: string) { } /** - * Stop collecting events for a specific environment + * Start polling mode for a specific environment + */ +async function startEnvironmentPoller(envId: number, envName: string, interval: number) { + // Stop existing poller if any + stopEnvironmentPoller(envId); + + console.log(`[EventSubprocess] Starting poller for ${envName} (every ${interval / 1000}s)`); + + // Initial poll immediately + await pollEnvironmentEvents(envId, envName); + + // Set up interval for subsequent polls + const intervalId = setInterval(async () => { + if (!isShuttingDown) { + await pollEnvironmentEvents(envId, envName); + } + }, interval); + + pollIntervals.set(envId, intervalId); +} + +/** + * Stop polling for a specific environment + */ +function stopEnvironmentPoller(envId: number) { + const intervalId = pollIntervals.get(envId); + if (intervalId) { + clearInterval(intervalId); + pollIntervals.delete(envId); + lastPollTime.delete(envId); + environmentOnlineStatus.delete(envId); + } +} + +/** + * Stop collecting events for a specific environment (streaming mode) */ function stopEnvironmentCollector(envId: number) { const controller = collectors.get(envId); @@ -351,6 +466,21 @@ async function refreshEventCollectors() { try { const environments = await getEnvironments(); + const mode = await getEventCollectionMode(); + const pollInterval = await getEventPollInterval(); + + // Detect if settings changed + const modeChanged = mode !== currentMode; + const intervalChanged = pollInterval !== currentPollInterval; + + if (modeChanged) { + console.log(`[EventSubprocess] Mode changed from ${currentMode} to ${mode}`); + currentMode = mode; + } + if (intervalChanged) { + console.log(`[EventSubprocess] Poll interval changed from ${currentPollInterval}ms to ${pollInterval}ms`); + currentPollInterval = pollInterval; + } // Filter: only collect for environments with activity enabled AND not Hawser Edge const activeEnvIds = new Set( @@ -362,18 +492,55 @@ async function refreshEventCollectors() { // Stop collectors for removed environments or those with collection disabled for (const envId of collectors.keys()) { if (!activeEnvIds.has(envId)) { - console.log(`[EventSubprocess] Stopping collector for environment ${envId}`); + console.log(`[EventSubprocess] Stopping stream collector for environment ${envId}`); stopEnvironmentCollector(envId); } } - // Start collectors for environments with collection enabled + // Stop pollers for removed environments or those with collection disabled + // Also restart all pollers if interval changed + for (const envId of pollIntervals.keys()) { + if (!activeEnvIds.has(envId)) { + console.log(`[EventSubprocess] Stopping poller for environment ${envId}`); + stopEnvironmentPoller(envId); + } else if (intervalChanged && mode === 'poll') { + // Restart poller with new interval + console.log(`[EventSubprocess] Restarting poller for environment ${envId} with new interval`); + stopEnvironmentPoller(envId); + } + } + + // Start collectors based on mode for (const env of environments) { // Skip Hawser Edge (handled by main process) if (env.connectionType === 'hawser-edge') continue; - if (env.collectActivity && !collectors.has(env.id)) { - startEnvironmentCollector(env.id, env.name); + // Skip if activity collection is disabled + if (!env.collectActivity) continue; + + const hasStreamCollector = collectors.has(env.id); + const hasPoller = pollIntervals.has(env.id); + + if (mode === 'stream') { + // Switch from polling to streaming if needed + if (hasPoller) { + console.log(`[EventSubprocess] Switching ${env.name} from poll to stream`); + stopEnvironmentPoller(env.id); + } + // Start stream if not already running + if (!hasStreamCollector) { + startEnvironmentCollector(env.id, env.name); + } + } else if (mode === 'poll') { + // Switch from streaming to polling if needed + if (hasStreamCollector) { + console.log(`[EventSubprocess] Switching ${env.name} from stream to poll`); + stopEnvironmentCollector(env.id); + } + // Start poller if not already running (will also restart after interval change above) + if (!hasPoller) { + startEnvironmentPoller(env.id, env.name, pollInterval); + } } } } catch (error) { @@ -392,6 +559,13 @@ function handleCommand(command: MainProcessCommand): void { refreshEventCollectors(); break; + case 'update_interval': + // This is used by metrics subprocess, but we handle it here too for consistency + // Event subprocess re-reads interval from DB on refresh + console.log('[EventSubprocess] Interval update - refreshing collectors...'); + refreshEventCollectors(); + break; + case 'shutdown': console.log('[EventSubprocess] Shutdown requested'); shutdown(); @@ -411,11 +585,16 @@ function shutdown(): void { cacheCleanupInterval = null; } - // Stop all environment collectors + // Stop all environment stream collectors for (const envId of collectors.keys()) { stopEnvironmentCollector(envId); } + // Stop all environment pollers + for (const envId of pollIntervals.keys()) { + stopEnvironmentPoller(envId); + } + // Clear the deduplication cache recentEvents.clear(); @@ -429,6 +608,15 @@ function shutdown(): void { async function start(): Promise { console.log('[EventSubprocess] Starting container event collection...'); + // Initialize current settings from database + try { + currentMode = await getEventCollectionMode(); + currentPollInterval = await getEventPollInterval(); + console.log(`[EventSubprocess] Initial mode: ${currentMode}, poll interval: ${currentPollInterval}ms`); + } catch (error) { + console.error('[EventSubprocess] Failed to load settings, using defaults:', error); + } + // Start collectors for all environments await refreshEventCollectors(); diff --git a/src/lib/server/subprocesses/metrics-subprocess.ts b/src/lib/server/subprocesses/metrics-subprocess.ts index 139e6fa..74669c0 100644 --- a/src/lib/server/subprocesses/metrics-subprocess.ts +++ b/src/lib/server/subprocesses/metrics-subprocess.ts @@ -7,12 +7,12 @@ * Communication with main process via IPC (process.send). */ -import { getEnvironments, getEnvSetting } from '../db'; +import { getEnvironments, getEnvSetting, getMetricsCollectionInterval } from '../db'; import { listContainers, getContainerStats, getDockerInfo, getDiskUsage } from '../docker'; import os from 'node:os'; import type { MainProcessCommand } from '../subprocess-manager'; -const COLLECT_INTERVAL = 10000; // 10 seconds +let COLLECT_INTERVAL = 30000; // 30 seconds (default, will be loaded from settings) const DISK_CHECK_INTERVAL = 300000; // 5 minutes const DEFAULT_DISK_THRESHOLD = 80; // 80% threshold for disk warnings const ENV_METRICS_TIMEOUT = 15000; // 15 seconds timeout per environment for metrics @@ -82,12 +82,16 @@ async function collectEnvMetrics(env: { id: number; name: string; host?: string; cpuPercent = (cpuDelta / systemDelta) * cpuCount * 100; } - // Get container memory usage (subtract cache for actual usage) + // Get container memory usage using the same formula as Docker CLI + // Docker subtracts cache (inactive_file) from total usage + // - cgroup v2: uses 'inactive_file' + // - cgroup v1: uses 'total_inactive_file' const memUsage = stats.memory_stats?.usage || 0; - const memCache = stats.memory_stats?.stats?.cache || 0; - const actualMemUsed = memUsage - memCache; + const memStats = stats.memory_stats?.stats || {}; + const memCache = memStats.inactive_file ?? memStats.total_inactive_file ?? 0; + const actualMemUsed = memCache > 0 && memCache < memUsage ? memUsage - memCache : memUsage; - return { cpuPercent, memUsage: actualMemUsed > 0 ? actualMemUsed : memUsage }; + return { cpuPercent, memUsage: actualMemUsed }; } catch { return { cpuPercent: 0, memUsage: 0 }; } @@ -356,6 +360,16 @@ function handleCommand(command: MainProcessCommand): void { // The next collection cycle will pick up the new environments break; + case 'update_interval': + console.log(`[MetricsSubprocess] Updating collection interval to ${command.intervalMs}ms`); + COLLECT_INTERVAL = command.intervalMs; + // Clear existing interval and restart with new timing + if (collectInterval) { + clearInterval(collectInterval); + collectInterval = setInterval(collectMetrics, COLLECT_INTERVAL); + } + break; + case 'shutdown': console.log('[MetricsSubprocess] Shutdown requested'); shutdown(); @@ -386,8 +400,15 @@ function shutdown(): void { /** * Start the metrics collector */ -function start(): void { - console.log('[MetricsSubprocess] Starting metrics collection (every 10s)...'); +async function start(): Promise { + // Load interval from settings + try { + COLLECT_INTERVAL = await getMetricsCollectionInterval(); + console.log(`[MetricsSubprocess] Starting metrics collection (every ${COLLECT_INTERVAL / 1000}s)...`); + } catch (error) { + console.error('[MetricsSubprocess] Failed to load interval from settings, using default 30s'); + COLLECT_INTERVAL = 30000; + } // Initial collection collectMetrics(); diff --git a/src/lib/stores/grid-preferences.ts b/src/lib/stores/grid-preferences.ts index 3c174af..cdcb276 100644 --- a/src/lib/stores/grid-preferences.ts +++ b/src/lib/stores/grid-preferences.ts @@ -70,8 +70,21 @@ function createGridPreferencesStore() { return getDefaultColumnPreferences(gridId); } - // Return columns in saved order, filtering to visible ones - return gridPrefs.columns.filter((col) => col.visible); + // Merge with defaults to ensure new columns are included + const defaults = getDefaultColumnPreferences(gridId); + const savedIds = new Set(gridPrefs.columns.map((c) => c.id)); + + // Start with saved visible columns + const result = gridPrefs.columns.filter((col) => col.visible); + + // Add any new default columns that aren't in saved preferences + for (const def of defaults) { + if (!savedIds.has(def.id) && def.visible) { + result.push(def); + } + } + + return result; }, // Get all columns for a grid (visible and hidden, in order) diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index d067e7f..d88e1ce 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -4,6 +4,7 @@ import { browser } from '$app/environment'; export type TimeFormat = '12h' | '24h'; export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY'; export type DownloadFormat = 'tar' | 'tar.gz'; +export type EventCollectionMode = 'stream' | 'poll'; export interface AppSettings { confirmDestructive: boolean; @@ -22,6 +23,11 @@ export interface AppSettings { eventCleanupEnabled: boolean; logBufferSizeKb: number; defaultTimezone: string; + eventCollectionMode: EventCollectionMode; + eventPollInterval: number; + metricsCollectionInterval: number; + externalStackPaths: string[]; + primaryStackLocation: string | null; } const DEFAULT_SETTINGS: AppSettings = { @@ -40,7 +46,12 @@ const DEFAULT_SETTINGS: AppSettings = { scheduleCleanupEnabled: true, eventCleanupEnabled: true, logBufferSizeKb: 500, - defaultTimezone: 'UTC' + defaultTimezone: 'UTC', + eventCollectionMode: 'stream', + eventPollInterval: 60000, + metricsCollectionInterval: 30000, + externalStackPaths: [], + primaryStackLocation: null }; // Create a writable store for app settings @@ -73,7 +84,12 @@ function createSettingsStore() { scheduleCleanupEnabled: settings.scheduleCleanupEnabled ?? DEFAULT_SETTINGS.scheduleCleanupEnabled, eventCleanupEnabled: settings.eventCleanupEnabled ?? DEFAULT_SETTINGS.eventCleanupEnabled, logBufferSizeKb: settings.logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, - defaultTimezone: settings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone + defaultTimezone: settings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, + eventCollectionMode: settings.eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode, + eventPollInterval: settings.eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval, + metricsCollectionInterval: settings.metricsCollectionInterval ?? DEFAULT_SETTINGS.metricsCollectionInterval, + externalStackPaths: settings.externalStackPaths ?? DEFAULT_SETTINGS.externalStackPaths, + primaryStackLocation: settings.primaryStackLocation ?? DEFAULT_SETTINGS.primaryStackLocation }); } } catch { @@ -109,7 +125,12 @@ function createSettingsStore() { scheduleCleanupEnabled: updatedSettings.scheduleCleanupEnabled ?? DEFAULT_SETTINGS.scheduleCleanupEnabled, eventCleanupEnabled: updatedSettings.eventCleanupEnabled ?? DEFAULT_SETTINGS.eventCleanupEnabled, logBufferSizeKb: updatedSettings.logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, - defaultTimezone: updatedSettings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone + defaultTimezone: updatedSettings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, + eventCollectionMode: updatedSettings.eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode, + eventPollInterval: updatedSettings.eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval, + metricsCollectionInterval: updatedSettings.metricsCollectionInterval ?? DEFAULT_SETTINGS.metricsCollectionInterval, + externalStackPaths: updatedSettings.externalStackPaths ?? DEFAULT_SETTINGS.externalStackPaths, + primaryStackLocation: updatedSettings.primaryStackLocation ?? DEFAULT_SETTINGS.primaryStackLocation }); } } catch (error) { @@ -248,6 +269,41 @@ function createSettingsStore() { return newSettings; }); }, + setEventCollectionMode: (value: EventCollectionMode) => { + update((current) => { + const newSettings = { ...current, eventCollectionMode: value }; + saveSettings({ eventCollectionMode: value }); + return newSettings; + }); + }, + setEventPollInterval: (value: number) => { + update((current) => { + const newSettings = { ...current, eventPollInterval: value }; + saveSettings({ eventPollInterval: value }); + return newSettings; + }); + }, + setMetricsCollectionInterval: (value: number) => { + update((current) => { + const newSettings = { ...current, metricsCollectionInterval: value }; + saveSettings({ metricsCollectionInterval: value }); + return newSettings; + }); + }, + setExternalStackPaths: (value: string[]) => { + update((current) => { + const newSettings = { ...current, externalStackPaths: value }; + saveSettings({ externalStackPaths: value }); + return newSettings; + }); + }, + setPrimaryStackLocation: (value: string | null) => { + update((current) => { + const newSettings = { ...current, primaryStackLocation: value }; + saveSettings({ primaryStackLocation: value }); + return newSettings; + }); + }, // Manual refresh from database refresh: loadSettings }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 17bf013..a9309d2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -34,6 +34,7 @@ export interface ImageInfo { size: number; virtualSize: number; labels: Record; + containers: number; // Number of containers using this image } export interface VolumeUsage { @@ -90,7 +91,9 @@ export interface ContainerStats { id: string; name: string; cpuPercent: number; - memoryUsage: number; + memoryUsage: number; // Actual usage (total - cache), same as docker stats + memoryRaw: number; // Raw total usage before cache subtraction + memoryCache: number; // File cache (inactive_file) memoryLimit: number; memoryPercent: number; networkRx: number; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a589049..83c36c3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -335,7 +335,7 @@ connectionType: env.connectionType || 'socket', labels: env.labels || [], scannerEnabled: false, - online: false, + online: undefined, // undefined = connecting, false = offline, true = online containers: { total: 0, running: 0, stopped: 0, paused: 0, restarting: 0, unhealthy: 0 }, images: { total: 0, totalSize: 0 }, volumes: { total: 0, totalSize: 0 }, diff --git a/src/routes/activity/+page.svelte b/src/routes/activity/+page.svelte index c859190..172f39d 100644 --- a/src/routes/activity/+page.svelte +++ b/src/routes/activity/+page.svelte @@ -30,7 +30,9 @@ Loader2, FileX, Heart, - Search + Search, + Wifi, + Radio } from 'lucide-svelte'; import PageHeader from '$lib/components/PageHeader.svelte'; import { currentEnvironment, environments as environmentsStore } from '$lib/stores/environment'; @@ -38,7 +40,7 @@ import { canAccess } from '$lib/stores/auth'; import ConfirmPopover from '$lib/components/ConfirmPopover.svelte'; import { toast } from 'svelte-sonner'; - import { formatDateTime } from '$lib/stores/settings'; + import { formatDateTime, appSettings } from '$lib/stores/settings'; import { NoEnvironment } from '$lib/components/ui/empty-state'; import { DataGrid } from '$lib/components/data-grid'; @@ -643,7 +645,20 @@
- 0 ? `${visibleStart}-${visibleEnd}` : undefined} total={total > 0 ? total : undefined} countClass="min-w-32" /> +
+ 0 ? `${visibleStart}-${visibleEnd}` : undefined} total={total > 0 ? total : undefined} countClass="min-w-32" /> + + {#if ($appSettings.eventCollectionMode || 'stream') === 'stream'} + + Stream + {:else if ($appSettings.eventCollectionMode || 'stream') === 'poll'} + + Poll({($appSettings.eventPollInterval || 60000) / 1000}s) + {:else} + Off + {/if} + +
diff --git a/src/routes/api/containers/[id]/logs/stream/+server.ts b/src/routes/api/containers/[id]/logs/stream/+server.ts index 9e2b77d..fd0d83a 100644 --- a/src/routes/api/containers/[id]/logs/stream/+server.ts +++ b/src/routes/api/containers/[id]/logs/stream/+server.ts @@ -285,11 +285,19 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { const inspectUrl = `${config.type}://${config.host}:${config.port}${inspectPath}`; const inspectHeaders: Record = {}; if (config.hawserToken) inspectHeaders['X-Hawser-Token'] = config.hawserToken; - inspectResponse = await fetch(inspectUrl, { - headers: inspectHeaders, - // @ts-ignore - tls: config.type === 'https' ? { ca: config.ca, cert: config.cert, key: config.key } : undefined - }); + const fetchOpts: any = { headers: inspectHeaders }; + if (config.type === 'https') { + fetchOpts.tls = { + sessionTimeout: 0, // Disable TLS session caching for mTLS + servername: config.host, + rejectUnauthorized: true + }; + if (config.ca) fetchOpts.tls.ca = [config.ca]; + if (config.cert) fetchOpts.tls.cert = [config.cert]; + if (config.key) fetchOpts.tls.key = config.key; + fetchOpts.keepalive = false; + } + inspectResponse = await fetch(inspectUrl, fetchOpts); } if (inspectResponse.ok) { @@ -341,12 +349,22 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { const logsUrl = `${config.type}://${config.host}:${config.port}${logsPath}`; const logsHeaders: Record = {}; if (config.hawserToken) logsHeaders['X-Hawser-Token'] = config.hawserToken; - response = await fetch(logsUrl, { + const fetchOpts: any = { headers: logsHeaders, - signal: abortController?.signal, - // @ts-ignore - tls: config.type === 'https' ? { ca: config.ca, cert: config.cert, key: config.key } : undefined - }); + signal: abortController?.signal + }; + if (config.type === 'https') { + fetchOpts.tls = { + sessionTimeout: 0, // Disable TLS session caching for mTLS + servername: config.host, + rejectUnauthorized: true + }; + if (config.ca) fetchOpts.tls.ca = [config.ca]; + if (config.cert) fetchOpts.tls.cert = [config.cert]; + if (config.key) fetchOpts.tls.key = config.key; + fetchOpts.keepalive = false; + } + response = await fetch(logsUrl, fetchOpts); } if (!response.ok) { diff --git a/src/routes/api/containers/[id]/stats/+server.ts b/src/routes/api/containers/[id]/stats/+server.ts index 5690d85..c06e99c 100644 --- a/src/routes/api/containers/[id]/stats/+server.ts +++ b/src/routes/api/containers/[id]/stats/+server.ts @@ -1,6 +1,6 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { getContainerStats } from '$lib/server/docker'; +import { getContainerStats, EnvironmentNotFoundError } from '$lib/server/docker'; import { authorize } from '$lib/server/authorize'; import { hasEnvironments } from '$lib/server/db'; @@ -47,6 +47,28 @@ function calculateBlockIO(stats: any): { read: number; write: number } { return { read, write }; } +/** + * Calculate memory usage the same way Docker CLI does. + * Docker subtracts cache (inactive_file) from total usage to show actual memory consumption. + * - cgroup v2: subtract inactive_file from stats + * - cgroup v1: subtract total_inactive_file from stats + * See: https://docs.docker.com/engine/containers/runmetrics/ + * + * Returns: { usage: actual memory (minus cache), raw: total usage, cache: file cache } + */ +function calculateMemoryUsage(memoryStats: any): { usage: number; raw: number; cache: number } { + const raw = memoryStats?.usage || 0; + const stats = memoryStats?.stats || {}; + + // cgroup v2 uses 'inactive_file', cgroup v1 uses 'total_inactive_file' + const cache = stats.inactive_file ?? stats.total_inactive_file ?? 0; + + // Only subtract cache if it's less than raw usage (sanity check) + const usage = (cache > 0 && cache < raw) ? raw - cache : raw; + + return { usage, raw, cache }; +} + export const GET: RequestHandler = async ({ params, url, cookies }) => { const auth = await authorize(cookies); @@ -67,15 +89,17 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { const stats = await getContainerStats(params.id, envIdNum) as any; const cpuPercent = calculateCpuPercent(stats); - const memoryUsage = stats.memory_stats?.usage || 0; + const memory = calculateMemoryUsage(stats.memory_stats); const memoryLimit = stats.memory_stats?.limit || 1; - const memoryPercent = (memoryUsage / memoryLimit) * 100; + const memoryPercent = (memory.usage / memoryLimit) * 100; const networkIO = calculateNetworkIO(stats); const blockIO = calculateBlockIO(stats); return json({ cpuPercent: Math.round(cpuPercent * 100) / 100, - memoryUsage, + memoryUsage: memory.usage, + memoryRaw: memory.raw, + memoryCache: memory.cache, memoryLimit, memoryPercent: Math.round(memoryPercent * 100) / 100, networkRx: networkIO.rx, @@ -85,6 +109,10 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { timestamp: Date.now() }); } catch (error: any) { + // Return 404 for deleted environments so client can clear stale cache + if (error instanceof EnvironmentNotFoundError) { + return json({ error: 'Environment not found' }, { status: 404 }); + } console.error('Failed to get container stats:', error); return json({ error: error.message || 'Failed to get stats' }, { status: 500 }); } diff --git a/src/routes/api/containers/stats/+server.ts b/src/routes/api/containers/stats/+server.ts index 60b78a5..ed1bf6e 100644 --- a/src/routes/api/containers/stats/+server.ts +++ b/src/routes/api/containers/stats/+server.ts @@ -1,6 +1,6 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { listContainers, getContainerStats } from '$lib/server/docker'; +import { listContainers, getContainerStats, EnvironmentNotFoundError } from '$lib/server/docker'; import { authorize } from '$lib/server/authorize'; import { hasEnvironments } from '$lib/server/db'; import type { ContainerStats } from '$lib/types'; @@ -48,6 +48,28 @@ function calculateBlockIO(stats: any): { read: number; write: number } { return { read, write }; } +/** + * Calculate memory usage the same way Docker CLI does. + * Docker subtracts cache (inactive_file) from total usage to show actual memory consumption. + * - cgroup v2: subtract inactive_file from stats + * - cgroup v1: subtract total_inactive_file from stats + * See: https://docs.docker.com/engine/containers/runmetrics/ + * + * Returns: { usage: actual memory (minus cache), raw: total usage, cache: file cache } + */ +function calculateMemoryUsage(memoryStats: any): { usage: number; raw: number; cache: number } { + const raw = memoryStats?.usage || 0; + const stats = memoryStats?.stats || {}; + + // cgroup v2 uses 'inactive_file', cgroup v1 uses 'total_inactive_file' + const cache = stats.inactive_file ?? stats.total_inactive_file ?? 0; + + // Only subtract cache if it's less than raw usage (sanity check) + const usage = (cache > 0 && cache < raw) ? raw - cache : raw; + + return { usage, raw, cache }; +} + // Helper to add timeout to promises function withTimeout(promise: Promise, ms: number, fallback: T): Promise { return Promise.race([ @@ -112,10 +134,10 @@ export const GET: RequestHandler = async ({ url, cookies }) => { if (!stats) return null; const cpuPercent = calculateCpuPercent(stats); - // Use raw memory usage (total memory attributed to container) - const memoryUsage = stats.memory_stats?.usage || 0; + // Calculate memory usage the same way Docker CLI does (excludes cache) + const memory = calculateMemoryUsage(stats.memory_stats); const memoryLimit = stats.memory_stats?.limit || 1; - const memoryPercent = (memoryUsage / memoryLimit) * 100; + const memoryPercent = (memory.usage / memoryLimit) * 100; const networkIO = calculateNetworkIO(stats); const blockIO = calculateBlockIO(stats); @@ -123,7 +145,9 @@ export const GET: RequestHandler = async ({ url, cookies }) => { id: container.id, name: container.name, cpuPercent: Math.round(cpuPercent * 100) / 100, - memoryUsage, + memoryUsage: memory.usage, + memoryRaw: memory.raw, + memoryCache: memory.cache, memoryLimit, memoryPercent: Math.round(memoryPercent * 100) / 100, networkRx: networkIO.rx, @@ -142,6 +166,10 @@ export const GET: RequestHandler = async ({ url, cookies }) => { return json(validStats); } catch (error: any) { + // Return 404 for deleted environments so client can clear stale cache + if (error instanceof EnvironmentNotFoundError) { + return json({ error: 'Environment not found' }, { status: 404 }); + } console.error('Failed to get container stats:', error); return json([], { status: 200 }); // Return empty array instead of error } diff --git a/src/routes/api/dashboard/stats/+server.ts b/src/routes/api/dashboard/stats/+server.ts index 02bfe99..50ec6e3 100644 --- a/src/routes/api/dashboard/stats/+server.ts +++ b/src/routes/api/dashboard/stats/+server.ts @@ -52,7 +52,7 @@ export interface EnvironmentStats { updateCheckAutoUpdate: boolean; labels?: string[]; connectionType: 'socket' | 'direct' | 'hawser-standard' | 'hawser-edge'; - online: boolean; + online?: boolean; // undefined = connecting, false = offline, true = online error?: string; containers: { total: number; diff --git a/src/routes/api/dashboard/stats/stream/+server.ts b/src/routes/api/dashboard/stats/stream/+server.ts index 4fc1f8b..e8b35b4 100644 --- a/src/routes/api/dashboard/stats/stream/+server.ts +++ b/src/routes/api/dashboard/stats/stream/+server.ts @@ -12,7 +12,6 @@ import { listContainers, listImages, listNetworks, - getDockerInfo, getContainerStats, getDiskUsage } from '$lib/server/docker'; @@ -95,6 +94,28 @@ function calculateCpuPercent(stats: any): number { return 0; } +/** + * Calculate memory usage the same way Docker CLI does. + * Docker subtracts cache (inactive_file) from total usage to show actual memory consumption. + * - cgroup v2: subtract inactive_file from stats + * - cgroup v1: subtract total_inactive_file from stats + * See: https://docs.docker.com/engine/containers/runmetrics/ + */ +function calculateMemoryUsage(memoryStats: any): number { + const usage = memoryStats?.usage || 0; + const stats = memoryStats?.stats || {}; + + // cgroup v2 uses 'inactive_file', cgroup v1 uses 'total_inactive_file' + const cache = stats.inactive_file ?? stats.total_inactive_file ?? 0; + + // Only subtract cache if it's less than usage (sanity check) + if (cache > 0 && cache < usage) { + return usage - cache; + } + + return usage; +} + // Progressive stats loading - returns stats object and emits partial updates via callback async function getEnvironmentStatsProgressive( env: any, @@ -150,23 +171,9 @@ async function getEnvironmentStatsProgressive( envStats.updateCheckAutoUpdate = updateCheckSettings.autoUpdate; } - // Check if Docker is accessible (with 5 second timeout) - const dockerInfo = await withTimeout(getDockerInfo(env.id), 5000, null); - if (!dockerInfo) { - envStats.error = 'Connection timeout or Docker not accessible'; - envStats.loading = undefined; // Clear loading states on error - // Send offline status to client - onPartialUpdate({ - id: env.id, - online: false, - error: envStats.error, - loading: undefined - }); - return envStats; - } - envStats.online = true; - // Get all database stats in parallel for better performance + // NOTE: We do NOT block on getDockerInfo() here - slow environments would block all others + // Instead, we determine online status from whether listContainers succeeds const [latestMetrics, eventStats, recentEventsResult, metricsHistory] = await Promise.all([ getLatestHostMetrics(env.id), getContainerEventStats(env.id), @@ -204,10 +211,9 @@ async function getEnvironmentStatsProgressive( })); } - // Send initial update with DB data and online status + // Send initial update with DB data (online status determined later by Docker API success) onPartialUpdate({ id: env.id, - online: true, metrics: envStats.metrics, events: envStats.events, recentEvents: envStats.recentEvents, @@ -223,9 +229,22 @@ async function getEnvironmentStatsProgressive( return size && size > 0 ? size : 0; }; - // PHASE 1: Containers (usually fast) - const containersPromise = withTimeout(listContainers(true, env.id).catch(() => []), 10000, []) + // Track if Docker API is accessible - determined by listContainers success + let dockerApiAccessible = false; + let dockerApiError: string | null = null; + + // PHASE 1: Containers (usually fast) - this determines online status + // Use 10s timeout - this is the critical path that determines if env is online + const containersPromise = withTimeout(listContainers(true, env.id), 10000, null) .then(async (containers) => { + // Timeout returns null + if (containers === null) { + throw new Error('Connection timeout'); + } + // If we got here, Docker API is accessible + dockerApiAccessible = true; + envStats.online = true; + envStats.containers.total = containers.length; envStats.containers.running = containers.filter((c: any) => c.state === 'running').length; envStats.containers.stopped = containers.filter((c: any) => c.state === 'exited').length; @@ -236,11 +255,39 @@ async function getEnvironmentStatsProgressive( onPartialUpdate({ id: env.id, + online: true, containers: { ...envStats.containers }, loading: { ...envStats.loading! } }); return containers; + }) + .catch((error) => { + // Docker API failed - mark as offline + dockerApiAccessible = false; + const errorStr = String(error); + if (errorStr.includes('not connected') || errorStr.includes('Edge agent')) { + dockerApiError = 'Agent not connected'; + } else if (errorStr.includes('FailedToOpenSocket') || errorStr.includes('ECONNREFUSED')) { + dockerApiError = 'Docker socket not accessible'; + } else if (errorStr.includes('ECONNRESET') || errorStr.includes('connection was closed')) { + dockerApiError = 'Connection lost'; + } else if (errorStr.includes('timeout') || errorStr.includes('Timeout')) { + dockerApiError = 'Connection timeout'; + } else { + dockerApiError = 'Connection error'; + } + envStats.error = dockerApiError; + envStats.loading!.containers = false; + + onPartialUpdate({ + id: env.id, + online: false, + error: dockerApiError, + loading: { ...envStats.loading! } + }); + + return [] as any[]; }); // PHASE 2: Images, Networks, Stacks (medium speed) - run in parallel @@ -339,7 +386,7 @@ async function getEnvironmentStatsProgressive( if (!stats) return null; const cpuPercent = calculateCpuPercent(stats); - const memoryUsage = stats.memory_stats?.usage || 0; + const memoryUsage = calculateMemoryUsage(stats.memory_stats); const memoryLimit = stats.memory_stats?.limit || 1; const memoryPercent = (memoryUsage / memoryLimit) * 100; diff --git a/src/routes/api/environments/+server.ts b/src/routes/api/environments/+server.ts index d97c978..46b8d71 100644 --- a/src/routes/api/environments/+server.ts +++ b/src/routes/api/environments/+server.ts @@ -1,9 +1,10 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { getEnvironments, createEnvironment, assignUserRole, getRoleByName, getEnvironmentPublicIps, setEnvironmentPublicIp, getEnvUpdateCheckSettings, getEnvironmentTimezone, type Environment } from '$lib/server/db'; +import { getEnvironments, getEnvironmentByName, createEnvironment, assignUserRole, getRoleByName, getEnvironmentPublicIps, setEnvironmentPublicIp, getEnvUpdateCheckSettings, getEnvironmentTimezone, type Environment } from '$lib/server/db'; import { authorize } from '$lib/server/authorize'; import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager'; import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors'; +import { cleanPem } from '$lib/utils/pem'; export const GET: RequestHandler = async ({ cookies }) => { const auth = await authorize(cookies); @@ -69,6 +70,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => { return json({ error: 'Name is required' }, { status: 400 }); } + // Check if environment with this name already exists + const existing = await getEnvironmentByName(data.name); + if (existing) { + return json({ error: 'An environment with this name already exists' }, { status: 409 }); + } + // Host is required for direct and hawser-standard connections const connectionType = data.connectionType || 'socket'; if ((connectionType === 'direct' || connectionType === 'hawser-standard') && !data.host) { @@ -83,9 +90,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => { host: data.host, port: data.port || 2375, protocol: data.protocol || 'http', - tlsCa: data.tlsCa, - tlsCert: data.tlsCert, - tlsKey: data.tlsKey, + tlsCa: cleanPem(data.tlsCa), + tlsCert: cleanPem(data.tlsCert), + tlsKey: cleanPem(data.tlsKey), tlsSkipVerify: data.tlsSkipVerify || false, icon: data.icon || 'globe', socketPath: data.socketPath || '/var/run/docker.sock', @@ -124,7 +131,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => { return json(env); } catch (error) { console.error('Failed to create environment:', error); - const message = error instanceof Error ? error.message : 'Failed to create environment'; - return json({ error: message }, { status: 500 }); + return json({ error: 'Failed to create environment' }, { status: 500 }); } }; diff --git a/src/routes/api/environments/[id]/+server.ts b/src/routes/api/environments/[id]/+server.ts index 52c4ac6..1f34bd9 100644 --- a/src/routes/api/environments/[id]/+server.ts +++ b/src/routes/api/environments/[id]/+server.ts @@ -6,6 +6,7 @@ import { deleteGitStackFiles } from '$lib/server/git'; import { authorize } from '$lib/server/authorize'; import { refreshSubprocessEnvironments } from '$lib/server/subprocess-manager'; import { serializeLabels, parseLabels, MAX_LABELS } from '$lib/utils/label-colors'; +import { cleanPem } from '$lib/utils/pem'; import { unregisterSchedule } from '$lib/server/scheduler'; import { closeEdgeConnection } from '$lib/server/hawser'; @@ -62,9 +63,9 @@ export const PUT: RequestHandler = async ({ params, request, cookies }) => { host: data.host, port: data.port, protocol: data.protocol, - tlsCa: data.tlsCa, - tlsCert: data.tlsCert, - tlsKey: data.tlsKey, + tlsCa: cleanPem(data.tlsCa), + tlsCert: cleanPem(data.tlsCert), + tlsKey: cleanPem(data.tlsKey), tlsSkipVerify: data.tlsSkipVerify, icon: data.icon, socketPath: data.socketPath, diff --git a/src/routes/api/environments/test/+server.ts b/src/routes/api/environments/test/+server.ts index f7221ff..7222c8b 100644 --- a/src/routes/api/environments/test/+server.ts +++ b/src/routes/api/environments/test/+server.ts @@ -60,50 +60,54 @@ export const POST: RequestHandler = async ({ request }) => { headers['X-Hawser-Token'] = config.hawserToken; } - // For HTTPS with custom CA or skip verification, use subprocess to avoid Vite dev server TLS issues - if (protocol === 'https' && (config.tlsCa || config.tlsSkipVerify)) { - const fs = await import('node:fs'); - let tempCaPath = ''; - - // Clean the certificate - remove leading/trailing whitespace from each line - let cleanedCa = ''; - if (config.tlsCa && !config.tlsSkipVerify) { - cleanedCa = config.tlsCa - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .join('\n'); - - tempCaPath = `/tmp/dockhand-ca-${Date.now()}.pem`; - fs.writeFileSync(tempCaPath, cleanedCa); - } - - // Build Bun script that runs outside Vite's process (Vite interferes with TLS) - const tlsConfig = config.tlsSkipVerify - ? `tls: { rejectUnauthorized: false }` - : `tls: { ca: await Bun.file('${tempCaPath}').text() }`; - + // For HTTPS with custom CA, client certs, or skip verification, use subprocess to avoid Vite dev server TLS issues + if (protocol === 'https' && (config.tlsCa || config.tlsCert || config.tlsSkipVerify)) { + // Clean PEM content (remove extra whitespace) + const cleanPem = (pem: string) => pem + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .join('\n'); + + // Pass config as base64-encoded JSON to avoid escaping issues + const tlsConfig = { + url: `https://${host}:${port}/info`, + headers, + tlsSkipVerify: config.tlsSkipVerify || false, + ca: config.tlsCa && !config.tlsSkipVerify ? cleanPem(config.tlsCa) : null, + cert: config.tlsCert ? cleanPem(config.tlsCert) : null, + key: config.tlsKey ? cleanPem(config.tlsKey) : null, + host + }; + const configBase64 = Buffer.from(JSON.stringify(tlsConfig)).toString('base64'); + + // Inline script with config embedded (bun -e doesn't pass argv correctly) const scriptContent = ` -const response = await fetch('https://${host}:${port}/info', { - headers: ${JSON.stringify(headers)}, - ${tlsConfig} -}); -const body = await response.text(); -console.log(JSON.stringify({ status: response.status, body })); +const config = JSON.parse(Buffer.from('${configBase64}', 'base64').toString()); +try { + const tls = { + sessionTimeout: 0, + servername: config.host, + rejectUnauthorized: !config.tlsSkipVerify + }; + if (config.ca) tls.ca = [config.ca]; + if (config.cert) tls.cert = [config.cert]; + if (config.key) tls.key = config.key; + const response = await fetch(config.url, { + headers: config.headers, + tls, + keepalive: false + }); + const body = await response.text(); + console.log(JSON.stringify({ status: response.status, body })); +} catch (e) { + console.log(JSON.stringify({ error: e.message })); +} `; - const scriptPath = `/tmp/dockhand-test-${Date.now()}.ts`; - fs.writeFileSync(scriptPath, scriptContent); - - const proc = Bun.spawn(['bun', scriptPath], { stdout: 'pipe', stderr: 'pipe' }); + const proc = Bun.spawn(['bun', '-e', scriptContent], { stdout: 'pipe', stderr: 'pipe' }); const output = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); - // Cleanup temp files - if (tempCaPath) { - try { fs.unlinkSync(tempCaPath); } catch {} - } - try { fs.unlinkSync(scriptPath); } catch {} - if (!output.trim()) { throw new Error(stderr || 'Empty response from TLS test subprocess'); } diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts index caeb8c0..2ade909 100644 --- a/src/routes/api/events/+server.ts +++ b/src/routes/api/events/+server.ts @@ -1,5 +1,5 @@ import type { RequestHandler } from './$types'; -import { getDockerEvents } from '$lib/server/docker'; +import { getDockerEvents, EnvironmentNotFoundError } from '$lib/server/docker'; import { getEnvironment } from '$lib/server/db'; export const GET: RequestHandler = async ({ url }) => { @@ -118,8 +118,13 @@ export const GET: RequestHandler = async ({ url }) => { processEvents(); } catch (error: any) { - console.error('Failed to connect to Docker events:', error); - sendEvent('error', { message: error.message || 'Failed to connect to Docker' }); + if (error instanceof EnvironmentNotFoundError) { + // Expected error when environment doesn't exist - don't spam logs + sendEvent('error', { message: 'Environment not found' }); + } else { + console.error('Failed to connect to Docker events:', error); + sendEvent('error', { message: error.message || 'Failed to connect to Docker' }); + } clearInterval(heartbeatInterval); controller.close(); } diff --git a/src/routes/api/git/stacks/+server.ts b/src/routes/api/git/stacks/+server.ts index 22e5f28..f769413 100644 --- a/src/routes/api/git/stacks/+server.ts +++ b/src/routes/api/git/stacks/+server.ts @@ -11,7 +11,7 @@ import { import { deployGitStack } from '$lib/server/git'; import { authorize } from '$lib/server/authorize'; import { registerSchedule } from '$lib/server/scheduler'; -import crypto from 'node:crypto'; +import { secureRandomBytes } from '$lib/server/crypto-fallback'; export const GET: RequestHandler = async ({ url, cookies }) => { const auth = await authorize(cookies); @@ -94,7 +94,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { // Generate webhook secret if webhook is enabled let webhookSecret = data.webhookSecret; if (data.webhookEnabled && !webhookSecret) { - webhookSecret = crypto.randomBytes(32).toString('hex'); + webhookSecret = secureRandomBytes(32).toString('hex'); } const gitStack = await createGitStack({ diff --git a/src/routes/api/registry/tags/+server.ts b/src/routes/api/registry/tags/+server.ts index cba2360..b19312a 100644 --- a/src/routes/api/registry/tags/+server.ts +++ b/src/routes/api/registry/tags/+server.ts @@ -16,7 +16,16 @@ function isDockerHub(url: string): boolean { lower.includes('registry.hub.docker.com'); } -async function fetchDockerHubTags(imageName: string): Promise { +interface PaginatedTags { + tags: TagInfo[]; + total: number; + page: number; + pageSize: number; + hasNext: boolean; + hasPrev: boolean; +} + +async function fetchDockerHubTags(imageName: string, page: number = 1, pageSize: number = 20): Promise { // Docker Hub uses a different API // For official images: https://hub.docker.com/v2/repositories/library//tags // For user images: https://hub.docker.com/v2/repositories///tags @@ -27,7 +36,7 @@ async function fetchDockerHubTags(imageName: string): Promise { repoPath = `library/${imageName}`; } - const url = `https://hub.docker.com/v2/repositories/${repoPath}/tags?page_size=100&ordering=last_updated`; + const url = `https://hub.docker.com/v2/repositories/${repoPath}/tags?page_size=${pageSize}&page=${page}&ordering=last_updated`; const response = await fetch(url, { headers: { @@ -45,12 +54,21 @@ async function fetchDockerHubTags(imageName: string): Promise { const data = await response.json(); const results = data.results || []; - return results.map((tag: any) => ({ + const tags = results.map((tag: any) => ({ name: tag.name, size: tag.full_size || tag.images?.[0]?.size, lastUpdated: tag.last_updated || tag.tag_last_pushed, digest: tag.images?.[0]?.digest })); + + return { + tags, + total: data.count || 0, + page, + pageSize, + hasNext: !!data.next, + hasPrev: !!data.previous + }; } async function fetchRegistryTags(registry: any, imageName: string): Promise { @@ -104,16 +122,18 @@ export const GET: RequestHandler = async ({ url }) => { try { const registryId = url.searchParams.get('registry'); const imageName = url.searchParams.get('image'); + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('pageSize') || '20'); if (!imageName) { return json({ error: 'Image name is required' }, { status: 400 }); } - let tags: TagInfo[]; + let result: PaginatedTags; if (!registryId) { // No registry specified, assume Docker Hub - tags = await fetchDockerHubTags(imageName); + result = await fetchDockerHubTags(imageName, page, pageSize); } else { const registry = await getRegistry(parseInt(registryId)); if (!registry) { @@ -121,13 +141,22 @@ export const GET: RequestHandler = async ({ url }) => { } if (isDockerHub(registry.url)) { - tags = await fetchDockerHubTags(imageName); + result = await fetchDockerHubTags(imageName, page, pageSize); } else { - tags = await fetchRegistryTags(registry, imageName); + // V2 registries don't support pagination well, return all tags + const tags = await fetchRegistryTags(registry, imageName); + result = { + tags, + total: tags.length, + page: 1, + pageSize: tags.length, + hasNext: false, + hasPrev: false + }; } } - return json(tags); + return json(result); } catch (error: any) { console.error('Error fetching tags:', error); diff --git a/src/routes/api/settings/general/+server.ts b/src/routes/api/settings/general/+server.ts index 6cd65ba..012429d 100644 --- a/src/routes/api/settings/general/+server.ts +++ b/src/routes/api/settings/general/+server.ts @@ -15,14 +15,26 @@ import { getEventCleanupEnabled, setEventCleanupEnabled, getDefaultTimezone, - setDefaultTimezone + setDefaultTimezone, + getEventCollectionMode, + setEventCollectionMode, + getEventPollInterval, + setEventPollInterval, + getMetricsCollectionInterval, + setMetricsCollectionInterval, + getExternalStackPaths, + setExternalStackPaths, + getPrimaryStackLocation, + setPrimaryStackLocation } from '$lib/server/db'; import { authorize } from '$lib/server/authorize'; import { refreshSystemJobs } from '$lib/server/scheduler'; +import { sendToEventSubprocess, sendToMetricsSubprocess, type UpdateIntervalCommand } from '$lib/server/subprocess-manager'; export type TimeFormat = '12h' | '24h'; export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY'; export type DownloadFormat = 'tar' | 'tar.gz'; +export type EventCollectionMode = 'stream' | 'poll'; export interface GeneralSettings { confirmDestructive: boolean; @@ -41,6 +53,10 @@ export interface GeneralSettings { eventCleanupEnabled: boolean; logBufferSizeKb: number; defaultTimezone: string; + // Background monitoring settings + eventCollectionMode: EventCollectionMode; + eventPollInterval: number; + metricsCollectionInterval: number; // Theme settings (for when auth is disabled) lightTheme: string; darkTheme: string; @@ -48,6 +64,10 @@ export interface GeneralSettings { fontSize: string; gridFontSize: string; terminalFont: string; + // External stack paths + externalStackPaths: string[]; + // Primary stack location + primaryStackLocation: string | null; } const DEFAULT_SETTINGS: Omit = { @@ -61,6 +81,9 @@ const DEFAULT_SETTINGS: Omit { eventCleanupEnabled, logBufferSizeKb, defaultTimezone, + eventCollectionMode, + eventPollInterval, + metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, - terminalFont + terminalFont, + externalStackPaths, + primaryStackLocation ] = await Promise.all([ getSetting('confirm_destructive'), getSetting('show_stopped_containers'), @@ -127,12 +155,17 @@ export const GET: RequestHandler = async ({ cookies }) => { getEventCleanupEnabled(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), + getEventCollectionMode(), + getEventPollInterval(), + getMetricsCollectionInterval(), getSetting('theme_light'), getSetting('theme_dark'), getSetting('theme_font'), getSetting('theme_font_size'), getSetting('theme_grid_font_size'), - getSetting('theme_terminal_font') + getSetting('theme_terminal_font'), + getExternalStackPaths(), + getPrimaryStackLocation() ]); const settings: GeneralSettings = { @@ -152,12 +185,17 @@ export const GET: RequestHandler = async ({ cookies }) => { eventCleanupEnabled, logBufferSizeKb: logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, + eventCollectionMode: (eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode, + eventPollInterval: eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval, + metricsCollectionInterval: metricsCollectionInterval ?? DEFAULT_SETTINGS.metricsCollectionInterval, lightTheme: lightTheme ?? DEFAULT_SETTINGS.lightTheme, darkTheme: darkTheme ?? DEFAULT_SETTINGS.darkTheme, font: font ?? DEFAULT_SETTINGS.font, fontSize: fontSize ?? DEFAULT_SETTINGS.fontSize, gridFontSize: gridFontSize ?? DEFAULT_SETTINGS.gridFontSize, - terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont + terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont, + externalStackPaths, + primaryStackLocation }; return json(settings); @@ -175,7 +213,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { try { const body = await request.json(); - const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont } = body; + const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, externalStackPaths, primaryStackLocation } = body; if (confirmDestructive !== undefined) { await setSetting('confirm_destructive', confirmDestructive); @@ -228,6 +266,25 @@ export const POST: RequestHandler = async ({ request, cookies }) => { // Refresh system jobs to use the new timezone await refreshSystemJobs(); } + if (eventCollectionMode !== undefined && (eventCollectionMode === 'stream' || eventCollectionMode === 'poll')) { + await setEventCollectionMode(eventCollectionMode); + // Notify event subprocess to refresh collectors with new mode + sendToEventSubprocess({ type: 'refresh_environments' }); + } + if (eventPollInterval !== undefined && typeof eventPollInterval === 'number') { + // Validate: 30s - 300s (30 seconds to 5 minutes) + const validatedInterval = Math.max(30000, Math.min(300000, eventPollInterval)); + await setEventPollInterval(validatedInterval); + // Notify event subprocess to refresh collectors with new interval + sendToEventSubprocess({ type: 'refresh_environments' }); + } + if (metricsCollectionInterval !== undefined && typeof metricsCollectionInterval === 'number') { + // Validate: 10s - 300s (10 seconds to 5 minutes) + const validatedInterval = Math.max(10000, Math.min(300000, metricsCollectionInterval)); + await setMetricsCollectionInterval(validatedInterval); + // Notify metrics subprocess to update its collection interval + sendToMetricsSubprocess({ type: 'update_interval', intervalMs: validatedInterval }); + } if (lightTheme !== undefined && VALID_LIGHT_THEMES.includes(lightTheme)) { await setSetting('theme_light', lightTheme); } @@ -246,6 +303,20 @@ export const POST: RequestHandler = async ({ request, cookies }) => { if (terminalFont !== undefined && VALID_TERMINAL_FONTS.includes(terminalFont)) { await setSetting('theme_terminal_font', terminalFont); } + if (externalStackPaths !== undefined && Array.isArray(externalStackPaths)) { + // Filter to valid non-empty strings + const validPaths = externalStackPaths.filter((p: unknown) => typeof p === 'string' && p.trim()); + await setExternalStackPaths(validPaths); + } + if (primaryStackLocation !== undefined) { + // Accept string or null + if (primaryStackLocation === null || (typeof primaryStackLocation === 'string' && primaryStackLocation.trim())) { + await setPrimaryStackLocation(primaryStackLocation); + } else if (primaryStackLocation === '') { + // Empty string means clear the setting + await setPrimaryStackLocation(null); + } + } // Fetch all settings in parallel for the response const [ @@ -265,12 +336,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => { eventCleanupEnabledVal, logBufferSizeKbVal, defaultTimezoneVal, + eventCollectionModeVal, + eventPollIntervalVal, + metricsCollectionIntervalVal, lightThemeVal, darkThemeVal, fontVal, fontSizeVal, gridFontSizeVal, - terminalFontVal + terminalFontVal, + externalStackPathsVal, + primaryStackLocationVal ] = await Promise.all([ getSetting('confirm_destructive'), getSetting('show_stopped_containers'), @@ -288,12 +364,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => { getEventCleanupEnabled(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), + getEventCollectionMode(), + getEventPollInterval(), + getMetricsCollectionInterval(), getSetting('theme_light'), getSetting('theme_dark'), getSetting('theme_font'), getSetting('theme_font_size'), getSetting('theme_grid_font_size'), - getSetting('theme_terminal_font') + getSetting('theme_terminal_font'), + getExternalStackPaths(), + getPrimaryStackLocation() ]); const settings: GeneralSettings = { @@ -313,12 +394,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => { eventCleanupEnabled: eventCleanupEnabledVal, logBufferSizeKb: logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezoneVal ?? DEFAULT_SETTINGS.defaultTimezone, + eventCollectionMode: (eventCollectionModeVal ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode, + eventPollInterval: eventPollIntervalVal ?? DEFAULT_SETTINGS.eventPollInterval, + metricsCollectionInterval: metricsCollectionIntervalVal ?? DEFAULT_SETTINGS.metricsCollectionInterval, lightTheme: lightThemeVal ?? DEFAULT_SETTINGS.lightTheme, darkTheme: darkThemeVal ?? DEFAULT_SETTINGS.darkTheme, font: fontVal ?? DEFAULT_SETTINGS.font, fontSize: fontSizeVal ?? DEFAULT_SETTINGS.fontSize, gridFontSize: gridFontSizeVal ?? DEFAULT_SETTINGS.gridFontSize, - terminalFont: terminalFontVal ?? DEFAULT_SETTINGS.terminalFont + terminalFont: terminalFontVal ?? DEFAULT_SETTINGS.terminalFont, + externalStackPaths: externalStackPathsVal, + primaryStackLocation: primaryStackLocationVal }; return json(settings); diff --git a/src/routes/api/stacks/+server.ts b/src/routes/api/stacks/+server.ts index 39a675e..14c245d 100644 --- a/src/routes/api/stacks/+server.ts +++ b/src/routes/api/stacks/+server.ts @@ -35,11 +35,9 @@ export const GET: RequestHandler = async ({ url, cookies }) => { const existingNames = new Set(stacks.map((s) => s.name)); for (const source of stackSources) { - // Only add internal/git stacks that aren't already in the list - if ( - !existingNames.has(source.stackName) && - (source.sourceType === 'internal' || source.sourceType === 'git') - ) { + // Add stacks from database that aren't already in the Docker list + // This includes internal, git, and external (adopted) stacks that are currently down + if (!existingNames.has(source.stackName)) { stacks.push({ name: source.stackName, containers: [], @@ -78,7 +76,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { try { const body = await request.json(); - const { name, compose, start, envVars, rawEnvContent } = body; + const { name, compose, start, envVars, rawEnvContent, composePath, envPath } = body; if (!name || typeof name !== 'string') { return json({ error: 'Stack name is required' }, { status: 400 }); @@ -90,7 +88,10 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { // If start is false, only create the compose file without deploying if (start === false) { - const result = await saveStackComposeFile(name, compose, true); + const result = await saveStackComposeFile(name, compose, true, envIdNum, { + composePath: composePath || undefined, + envPath: envPath || undefined + }); if (!result.success) { return json({ error: result.error }, { status: 400 }); } @@ -100,7 +101,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { // - envVars: ALL vars → DB (secrets stored for shell injection, non-secrets for metadata) if (rawEnvContent) { // Write raw content to .env file (should NOT contain secrets) - await writeRawStackEnvFile(name, rawEnvContent); + await writeRawStackEnvFile(name, rawEnvContent, envIdNum, envPath || undefined); } // Save ALL vars to DB (secrets for shell injection at runtime) if (envVars && Array.isArray(envVars) && envVars.length > 0) { @@ -108,14 +109,16 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { } // Fallback: if no rawEnvContent, generate .env from non-secret vars if (!rawEnvContent && envVars && Array.isArray(envVars) && envVars.length > 0) { - await saveStackEnvVars(name, envVars, envIdNum); + await saveStackEnvVars(name, envVars, envIdNum, envPath || undefined); } - // Record the stack as internally created + // Record the stack as internally created with custom paths if provided await upsertStackSource({ stackName: name, environmentId: envIdNum, - sourceType: 'internal' + sourceType: 'internal', + composePath: composePath || undefined, + envPath: envPath || undefined }); return json({ success: true, started: false }); @@ -124,13 +127,16 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { // Save environment variables BEFORE deploying so they're available during start if (rawEnvContent || (envVars && Array.isArray(envVars) && envVars.length > 0)) { // First ensure the stack directory exists by saving compose file - await saveStackComposeFile(name, compose, true); + await saveStackComposeFile(name, compose, true, envIdNum, { + composePath: composePath || undefined, + envPath: envPath || undefined + }); // - rawEnvContent: non-secret vars with comments → .env file // - envVars: ALL vars → DB (secrets stored for shell injection, non-secrets for metadata) if (rawEnvContent) { // Write raw content to .env file (should NOT contain secrets) - await writeRawStackEnvFile(name, rawEnvContent); + await writeRawStackEnvFile(name, rawEnvContent, envIdNum, envPath || undefined); } // Save ALL vars to DB (secrets for shell injection at runtime) if (envVars && Array.isArray(envVars) && envVars.length > 0) { @@ -138,7 +144,7 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { } // Fallback: if no rawEnvContent, generate .env from non-secret vars if (!rawEnvContent && envVars && Array.isArray(envVars) && envVars.length > 0) { - await saveStackEnvVars(name, envVars, envIdNum); + await saveStackEnvVars(name, envVars, envIdNum, envPath || undefined); } } @@ -146,18 +152,22 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => { const result = await deployStack({ name, compose, - envId: envIdNum + envId: envIdNum, + composePath: composePath || undefined, + envPath: envPath || undefined }); if (!result.success) { return json({ error: result.error, output: result.output }, { status: 400 }); } - // Record the stack as internally created + // Record the stack as internally created with custom paths if provided await upsertStackSource({ stackName: name, environmentId: envIdNum, - sourceType: 'internal' + sourceType: 'internal', + composePath: composePath || undefined, + envPath: envPath || undefined }); return json({ success: true, started: true, output: result.output }); diff --git a/src/routes/api/stacks/[name]/+server.ts b/src/routes/api/stacks/[name]/+server.ts index 38098b5..9ed0945 100644 --- a/src/routes/api/stacks/[name]/+server.ts +++ b/src/routes/api/stacks/[name]/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { removeStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks'; +import { removeStack, ComposeFileNotFoundError } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import { auditStack } from '$lib/server/audit'; import type { RequestHandler } from './$types'; @@ -34,9 +34,6 @@ export const DELETE: RequestHandler = async (event) => { } return json({ success: true }); } catch (error) { - if (error instanceof ExternalStackError) { - return json({ error: error.message }, { status: 400 }); - } if (error instanceof ComposeFileNotFoundError) { return json({ error: error.message }, { status: 404 }); } diff --git a/src/routes/api/stacks/[name]/compose/+server.ts b/src/routes/api/stacks/[name]/compose/+server.ts index fa2664a..750838f 100644 --- a/src/routes/api/stacks/[name]/compose/+server.ts +++ b/src/routes/api/stacks/[name]/compose/+server.ts @@ -4,22 +4,36 @@ import { getStackComposeFile, deployStack, saveStackComposeFile } from '$lib/ser import { authorize } from '$lib/server/authorize'; // GET /api/stacks/[name]/compose - Get compose file content -export const GET: RequestHandler = async ({ params, cookies }) => { +export const GET: RequestHandler = async ({ params, url, cookies }) => { const auth = await authorize(cookies); if (auth.authEnabled && !(await auth.can('stacks', 'view'))) { return json({ error: 'Permission denied' }, { status: 403 }); } const { name } = params; + const envId = url.searchParams.get('env'); + const envIdNum = envId ? parseInt(envId) : undefined; try { - const result = await getStackComposeFile(name); + const result = await getStackComposeFile(name, envIdNum); if (!result.success) { - return json({ error: result.error }, { status: 404 }); + // Return info about what's needed - unified response for all missing compose files + return json({ + error: result.error, + needsFileLocation: result.needsFileLocation || false, + composePath: result.composePath, + envPath: result.envPath + }, { status: 404 }); } - return json({ content: result.content, stackDir: result.stackDir }); + return json({ + content: result.content, + stackDir: result.stackDir, + composePath: result.composePath, + envPath: result.envPath, + suggestedEnvPath: result.suggestedEnvPath + }); } catch (error: any) { console.error(`Error getting compose file for stack ${name}:`, error); return json({ error: error.message || 'Failed to get compose file' }, { status: 500 }); @@ -41,16 +55,29 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) => try { const body = await request.json(); - const { content, restart = false } = body; + const { content, restart = false, composePath, envPath, moveFromDir, oldComposePath, oldEnvPath } = body; if (!content || typeof content !== 'string') { return json({ error: 'Compose file content is required' }, { status: 400 }); } + // Build options object for custom paths, move operation, and file renames + const pathOptions = (composePath || envPath !== undefined || moveFromDir || oldComposePath || oldEnvPath) + ? { composePath, envPath, moveFromDir, oldComposePath, oldEnvPath } + : undefined; + let result; if (restart) { // Deploy with docker compose up -d --force-recreate // Force recreate ensures env var changes are applied + // Note: deployStack uses requireComposeFile which will use saved paths + // Save paths first if provided + if (pathOptions) { + const saveResult = await saveStackComposeFile(name, content, false, envIdNum, pathOptions); + if (!saveResult.success) { + return json({ error: saveResult.error }, { status: 500 }); + } + } result = await deployStack({ name, compose: content, @@ -58,8 +85,8 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) => forceRecreate: true }); } else { - // Just save the file without restarting - result = await saveStackComposeFile(name, content); + // Just save the file without restarting (update operation, not create) + result = await saveStackComposeFile(name, content, false, envIdNum, pathOptions); } if (!result.success) { diff --git a/src/routes/api/stacks/[name]/down/+server.ts b/src/routes/api/stacks/[name]/down/+server.ts index 1995f71..19f6aa0 100644 --- a/src/routes/api/stacks/[name]/down/+server.ts +++ b/src/routes/api/stacks/[name]/down/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { downStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks'; +import { downStack, ComposeFileNotFoundError } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import { auditStack } from '$lib/server/audit'; import type { RequestHandler } from './$types'; @@ -42,9 +42,6 @@ export const POST: RequestHandler = async (event) => { } return json({ success: true, output: result.output }); } catch (error) { - if (error instanceof ExternalStackError) { - return json({ error: error.message }, { status: 400 }); - } if (error instanceof ComposeFileNotFoundError) { return json({ error: error.message }, { status: 404 }); } diff --git a/src/routes/api/stacks/[name]/env/+server.ts b/src/routes/api/stacks/[name]/env/+server.ts index 4105e12..0265db6 100644 --- a/src/routes/api/stacks/[name]/env/+server.ts +++ b/src/routes/api/stacks/[name]/env/+server.ts @@ -1,9 +1,9 @@ import { json } from '@sveltejs/kit'; -import { getStackEnvVars, setStackEnvVars } from '$lib/server/db'; -import { getStacksDir } from '$lib/server/stacks'; +import { getStackEnvVars, setStackEnvVars, getStackSource } from '$lib/server/db'; +import { findStackDir } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import { existsSync } from 'node:fs'; -import { join } from 'node:path'; +import { join, dirname } from 'node:path'; import type { RequestHandler } from './$types'; /** @@ -61,12 +61,37 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { const dbVariables = await getStackEnvVars(stackName, envIdNum, true); const dbByKey = new Map(dbVariables.map(v => [v.key, v])); - // Try to read .env file from stack directory (only contains non-secrets) - const stacksDir = getStacksDir(); - const envFilePath = join(stacksDir, stackName, '.env'); + // Check if this stack has a custom compose path configured + const source = await getStackSource(stackName, envIdNum); + + // Determine the env file path based on path resolution rules: + // - envPath = '' (empty string) → explicitly no env file + // - envPath = '/path/.env' → use custom path + // - envPath = null with composePath → suggest .env next to compose (but don't auto-load) + // - envPath = null without composePath → use default location + let envFilePath: string | null = null; + + if (source?.envPath === '') { + // Empty string = explicitly no env file + envFilePath = null; + } else if (source?.envPath) { + // Custom env path specified + envFilePath = source.envPath; + } else if (source?.composePath) { + // Custom compose path but no env path - suggest .env next to compose + // For loading, check if it exists (but don't fail if it doesn't) + envFilePath = join(dirname(source.composePath), '.env'); + } else { + // Default location - .env in stack directory + const stackDir = await findStackDir(stackName, envIdNum); + if (stackDir) { + envFilePath = join(stackDir, '.env'); + } + } + let fileVars: Record = {}; - if (existsSync(envFilePath)) { + if (envFilePath && existsSync(envFilePath)) { try { const content = await Bun.file(envFilePath).text(); fileVars = parseEnvFile(content); diff --git a/src/routes/api/stacks/[name]/env/raw/+server.ts b/src/routes/api/stacks/[name]/env/raw/+server.ts index e5fac4d..6fbc3c5 100644 --- a/src/routes/api/stacks/[name]/env/raw/+server.ts +++ b/src/routes/api/stacks/[name]/env/raw/+server.ts @@ -1,8 +1,9 @@ import { json } from '@sveltejs/kit'; -import { getStacksDir } from '$lib/server/stacks'; +import { findStackDir, getStackDir } from '$lib/server/stacks'; +import { getStackSource } from '$lib/server/db'; import { authorize } from '$lib/server/authorize'; import { existsSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; +import { join, dirname } from 'node:path'; import type { RequestHandler } from './$types'; /** @@ -26,11 +27,36 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => { try { const stackName = decodeURIComponent(params.name); - const stacksDir = getStacksDir(); - const envFilePath = join(stacksDir, stackName, '.env'); + + // Check if this stack has custom paths configured + const source = await getStackSource(stackName, envIdNum); + + // Determine the env file path based on path resolution rules: + // - envPath = '' (empty string) → explicitly no env file + // - envPath = '/path/.env' → use custom path + // - envPath = null with composePath → suggest .env next to compose + // - envPath = null without composePath → use default location + let envFilePath: string | null = null; + + if (source?.envPath === '') { + // Empty string = explicitly no env file + return json({ content: '', noEnvFile: true }); + } else if (source?.envPath) { + // Custom env path specified + envFilePath = source.envPath; + } else if (source?.composePath) { + // Custom compose path but no env path - suggest .env next to compose + envFilePath = join(dirname(source.composePath), '.env'); + } else { + // Default location - .env in stack directory + const stackDir = await findStackDir(stackName, envIdNum); + if (stackDir) { + envFilePath = join(stackDir, '.env'); + } + } let content = ''; - if (existsSync(envFilePath)) { + if (envFilePath && existsSync(envFilePath)) { try { content = await Bun.file(envFilePath).text(); } catch { @@ -73,12 +99,35 @@ export const PUT: RequestHandler = async ({ params, url, cookies, request }) => return json({ error: 'Invalid request body: content string required' }, { status: 400 }); } - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); - const envFilePath = join(stackDir, '.env'); + // Check if this stack has custom paths configured + const source = await getStackSource(stackName, envIdNum); + + // Determine the env file path based on path resolution rules: + // - envPath = '' (empty string) → explicitly no env file, don't write + // - envPath = '/path/.env' → use custom path + // - envPath = null with composePath → suggest .env next to compose + // - envPath = null without composePath → use default location + let envFilePath: string | null = null; + + if (source?.envPath === '') { + // Empty string = explicitly no env file - don't allow writes + return json({ success: true, noEnvFile: true }); + } else if (source?.envPath) { + // Custom env path specified + envFilePath = source.envPath; + } else if (source?.composePath) { + // Custom compose path but no env path - suggest .env next to compose + envFilePath = join(dirname(source.composePath), '.env'); + } else { + // Default location - .env in stack directory + const stackDir = await findStackDir(stackName, envIdNum); + if (stackDir) { + envFilePath = join(stackDir, '.env'); + } + } - // Only write if stack directory exists - if (!existsSync(stackDir)) { + // Only write if we have a valid path + if (!envFilePath) { return json({ error: 'Stack directory not found' }, { status: 404 }); } diff --git a/src/routes/api/stacks/[name]/restart/+server.ts b/src/routes/api/stacks/[name]/restart/+server.ts index 29ecbf5..b4d9ce3 100644 --- a/src/routes/api/stacks/[name]/restart/+server.ts +++ b/src/routes/api/stacks/[name]/restart/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { restartStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks'; +import { restartStack, ComposeFileNotFoundError } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import { auditStack } from '$lib/server/audit'; import type { RequestHandler } from './$types'; @@ -33,9 +33,6 @@ export const POST: RequestHandler = async (event) => { } return json({ success: true, output: result.output }); } catch (error) { - if (error instanceof ExternalStackError) { - return json({ error: error.message }, { status: 400 }); - } if (error instanceof ComposeFileNotFoundError) { return json({ error: error.message }, { status: 404 }); } diff --git a/src/routes/api/stacks/[name]/start/+server.ts b/src/routes/api/stacks/[name]/start/+server.ts index 7e5fc5f..928841e 100644 --- a/src/routes/api/stacks/[name]/start/+server.ts +++ b/src/routes/api/stacks/[name]/start/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { startStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks'; +import { startStack, ComposeFileNotFoundError } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import { auditStack } from '$lib/server/audit'; import type { RequestHandler } from './$types'; @@ -33,9 +33,6 @@ export const POST: RequestHandler = async (event) => { } return json({ success: true, output: result.output }); } catch (error) { - if (error instanceof ExternalStackError) { - return json({ error: error.message }, { status: 400 }); - } if (error instanceof ComposeFileNotFoundError) { return json({ error: error.message }, { status: 404 }); } diff --git a/src/routes/api/stacks/[name]/stop/+server.ts b/src/routes/api/stacks/[name]/stop/+server.ts index d57fa7b..2c2a0fe 100644 --- a/src/routes/api/stacks/[name]/stop/+server.ts +++ b/src/routes/api/stacks/[name]/stop/+server.ts @@ -1,5 +1,5 @@ import { json } from '@sveltejs/kit'; -import { stopStack, ExternalStackError, ComposeFileNotFoundError } from '$lib/server/stacks'; +import { stopStack, ComposeFileNotFoundError } from '$lib/server/stacks'; import { authorize } from '$lib/server/authorize'; import { auditStack } from '$lib/server/audit'; import type { RequestHandler } from './$types'; @@ -33,9 +33,6 @@ export const POST: RequestHandler = async (event) => { } return json({ success: true, output: result.output }); } catch (error) { - if (error instanceof ExternalStackError) { - return json({ error: error.message }, { status: 400 }); - } if (error instanceof ComposeFileNotFoundError) { return json({ error: error.message }, { status: 404 }); } diff --git a/src/routes/api/stacks/sources/+server.ts b/src/routes/api/stacks/sources/+server.ts index d0688d9..a2f1a17 100644 --- a/src/routes/api/stacks/sources/+server.ts +++ b/src/routes/api/stacks/sources/+server.ts @@ -18,10 +18,11 @@ export const GET: RequestHandler = async ({ url, cookies }) => { const sources = await getStackSources(envIdNum); // Convert to a map for easier lookup in the frontend - const sourceMap: Record = {}; + const sourceMap: Record = {}; for (const source of sources) { sourceMap[source.stackName] = { sourceType: source.sourceType, + composePath: source.composePath, repository: source.repository }; } diff --git a/src/routes/api/system/+server.ts b/src/routes/api/system/+server.ts index 04c369a..118fb4f 100644 --- a/src/routes/api/system/+server.ts +++ b/src/routes/api/system/+server.ts @@ -186,6 +186,7 @@ export const GET: RequestHandler = async ({ url, cookies }) => { nodeVersion: process.version, platform: os.platform(), arch: os.arch(), + kernel: os.release(), memory: bunInfo.memory, container: containerRuntime, ownContainer diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte index 438ad50..5984f2e 100644 --- a/src/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -1,6 +1,7 @@ - + @@ -450,7 +501,7 @@ -
+
{#if loading}
@@ -461,7 +512,7 @@
{:else if containerData} - + showLogs = false}>Overview showLogs = true}>Logs showLogs = false}>Layers @@ -470,6 +521,7 @@ showLogs = false}>Mounts showLogs = false}>Files showLogs = false}>Environment + showLogs = false}>Labels showLogs = false}>Security showLogs = false}>Resources showLogs = false}>Health @@ -642,23 +694,6 @@
{/if} - - {#if containerData.Config?.Labels && Object.keys(containerData.Config.Labels).length > 0} -
- - Labels ({Object.keys(containerData.Config.Labels).length}) - -
- {#each Object.entries(containerData.Config.Labels) as [key, value]} -
- {key} - = - {value} -
- {/each} -
-
- {/if} @@ -845,8 +880,22 @@ {#each Object.entries(containerData.NetworkSettings.Ports) as [containerPort, hostBindings]} {#if hostBindings && hostBindings.length > 0} {#each hostBindings as binding} + {@const url = getPortUrl(parseInt(binding.HostPort))}
- {binding.HostIp || '0.0.0.0'}:{binding.HostPort} + {#if url} + + {binding.HostIp || '0.0.0.0'}:{binding.HostPort} + + + {:else} + {binding.HostIp || '0.0.0.0'}:{binding.HostPort} + {/if} {containerPort}
@@ -944,6 +993,37 @@ {/if} + + + {#if containerData.Config?.Labels && Object.keys(containerData.Config.Labels).length > 0} +
+ {#each Object.entries(containerData.Config.Labels).sort((a, b) => a[0].localeCompare(b[0])) as [key, value]} +
+
+ {key} + = + {value} +
+ +
+ {/each} +
+ {:else} +

No labels

+ {/if} +
+ @@ -1187,7 +1267,7 @@ - + diff --git a/src/routes/containers/ContainerSettingsTab.svelte b/src/routes/containers/ContainerSettingsTab.svelte index ee0c604..90ba559 100644 --- a/src/routes/containers/ContainerSettingsTab.svelte +++ b/src/routes/containers/ContainerSettingsTab.svelte @@ -35,12 +35,12 @@ id: number; name: string; description?: string; - env_vars?: { key: string; value: string }[]; + envVars?: { key: string; value: string }[]; labels?: { key: string; value: string }[]; ports?: { hostPort: string; containerPort: string; protocol: string }[]; volumes?: { hostPath: string; containerPath: string; mode: string }[]; - network_mode: string; - restart_policy: string; + networkMode: string; + restartPolicy: string; } interface DockerNetwork { @@ -315,15 +315,15 @@ const configSet = configSets.find((c) => c.id === parseInt(configSetId)); if (!configSet) return; - if (configSet.env_vars && configSet.env_vars.length > 0) { + if (configSet.envVars && configSet.envVars.length > 0) { if (mode === 'edit') { // Merge mode for edit const existingKeys = new Set(envVars.map(e => e.key).filter(k => k)); - const newEnvVars = configSet.env_vars.filter(e => !existingKeys.has(e.key)); + const newEnvVars = configSet.envVars.filter(e => !existingKeys.has(e.key)); envVars = [...envVars.filter(e => e.key), ...newEnvVars.map(e => ({ ...e }))]; if (envVars.length === 0) envVars = [{ key: '', value: '' }]; } else { - envVars = configSet.env_vars.map((e) => ({ ...e })); + envVars = configSet.envVars.map((e) => ({ ...e })); } } if (configSet.labels && configSet.labels.length > 0) { @@ -356,11 +356,11 @@ volumeMappings = configSet.volumes.map((v) => ({ ...v })); } } - if (configSet.network_mode) { - networkMode = configSet.network_mode; + if (configSet.networkMode) { + networkMode = configSet.networkMode; } - if (configSet.restart_policy) { - restartPolicy = configSet.restart_policy; + if (configSet.restartPolicy) { + restartPolicy = configSet.restartPolicy; } } diff --git a/src/routes/containers/CreateContainerModal.svelte b/src/routes/containers/CreateContainerModal.svelte index 0620e78..71893e8 100644 --- a/src/routes/containers/CreateContainerModal.svelte +++ b/src/routes/containers/CreateContainerModal.svelte @@ -49,12 +49,12 @@ id: number; name: string; description?: string; - env_vars?: { key: string; value: string }[]; + envVars?: { key: string; value: string }[]; labels?: { key: string; value: string }[]; ports?: { hostPort: string; containerPort: string; protocol: string }[]; volumes?: { hostPath: string; containerPath: string; mode: string }[]; - network_mode: string; - restart_policy: string; + networkMode: string; + restartPolicy: string; } @@ -564,7 +564,7 @@ isOpen && focusFirstInput()}> - + Create new container
@@ -785,6 +848,16 @@ {group.tags[0].tag} {/if} + {#if group.containers === 0} + + Unused + + {:else if group.tags.length > 1 && group.tags.some(t => t.containers === 0)} + + + Some unused + + {/if}
{:else if column.id === 'tags'} @@ -867,6 +940,22 @@ {formatSize(tagInfo.size)} {:else if column.id === 'created'} {formatImageDate(tagInfo.created)} + {:else if column.id === 'used'} + {#if tagInfo.containers > 0} + + {tagInfo.containers} container{tagInfo.containers === 1 ? '' : 's'} + + {:else if tagInfo.containers === 0} + + Unused + + {:else} + + {/if} {:else if column.id === 'actions'}
{#if $canAccess('images', 'inspect')} @@ -930,7 +1019,7 @@ {/if} - {#if $canAccess('images', 'remove')} + {#if $canAccess('images', 'remove') && tagInfo.containers === 0}
{@render children()} - +
diff --git a/src/routes/networks/CreateNetworkModal.svelte b/src/routes/networks/CreateNetworkModal.svelte index 5a108b5..fa28bb8 100644 --- a/src/routes/networks/CreateNetworkModal.svelte +++ b/src/routes/networks/CreateNetworkModal.svelte @@ -277,7 +277,7 @@ -
+
diff --git a/src/routes/networks/NetworkInspectModal.svelte b/src/routes/networks/NetworkInspectModal.svelte index 5c9eb9c..983599d 100644 --- a/src/routes/networks/NetworkInspectModal.svelte +++ b/src/routes/networks/NetworkInspectModal.svelte @@ -61,7 +61,7 @@ - + diff --git a/src/routes/registry/+page.svelte b/src/routes/registry/+page.svelte index 65b7add..a7b5404 100644 --- a/src/routes/registry/+page.svelte +++ b/src/routes/registry/+page.svelte @@ -43,8 +43,13 @@ interface ExpandedImageState { loading: boolean; + loadingMore: boolean; error: string; tags: TagInfo[]; + total: number; + page: number; + pageSize: number; + hasNext: boolean; } let registries = $state([]); @@ -226,38 +231,100 @@ const { [imageName]: _, ...rest } = expandedImages; expandedImages = rest; } else { - // Expand and fetch tags - expandedImages = { - ...expandedImages, - [imageName]: { loading: true, error: '', tags: [] } - }; + // Expand and fetch first page + await fetchTagsPage(imageName, 1, true); + } + } - try { - let url = `/api/registry/tags?image=${encodeURIComponent(imageName)}`; - if (selectedRegistryId) { - url += `®istry=${selectedRegistryId}`; - } + async function loadMoreTags(imageName: string) { + const state = expandedImages[imageName]; + if (!state || state.loading || state.loadingMore || !state.hasNext) return; + await fetchTagsPage(imageName, state.page + 1, false); + } - const response = await fetch(url); - if (response.ok) { - const tags = await response.json(); - expandedImages = { - ...expandedImages, - [imageName]: { loading: false, error: '', tags } - }; - } else { - const data = await response.json(); - expandedImages = { - ...expandedImages, - [imageName]: { loading: false, error: data.error || 'Failed to fetch tags', tags: [] } - }; - } - } catch (error: any) { + async function fetchTagsPage(imageName: string, page: number, isFirstLoad: boolean) { + const currentState = expandedImages[imageName]; + + expandedImages = { + ...expandedImages, + [imageName]: { + loading: isFirstLoad, + loadingMore: !isFirstLoad, + error: '', + tags: currentState?.tags || [], + total: currentState?.total || 0, + page: currentState?.page || 0, + pageSize: 20, + hasNext: currentState?.hasNext || false + } + }; + + try { + let url = `/api/registry/tags?image=${encodeURIComponent(imageName)}&page=${page}&pageSize=20`; + if (selectedRegistryId) { + url += `®istry=${selectedRegistryId}`; + } + + const response = await fetch(url); + if (response.ok) { + const data = await response.json(); + const prevState = expandedImages[imageName]; + const existingTags = isFirstLoad ? [] : (prevState?.tags || []); expandedImages = { ...expandedImages, - [imageName]: { loading: false, error: error.message || 'Failed to fetch tags', tags: [] } + [imageName]: { + loading: false, + loadingMore: false, + error: '', + tags: [...existingTags, ...data.tags], + total: data.total, + page: data.page, + pageSize: data.pageSize, + hasNext: data.hasNext + } + }; + } else { + const data = await response.json(); + expandedImages = { + ...expandedImages, + [imageName]: { + ...expandedImages[imageName], + loading: false, + loadingMore: false, + error: data.error || 'Failed to fetch tags' + } }; } + } catch (error: any) { + expandedImages = { + ...expandedImages, + [imageName]: { + ...expandedImages[imageName], + loading: false, + loadingMore: false, + error: error.message || 'Failed to fetch tags' + } + }; + } + } + + function handleTagsWheel(event: WheelEvent, imageName: string) { + const target = event.currentTarget as HTMLElement; + + // Prevent page scroll when at top/bottom of tags list + const atTop = target.scrollTop === 0; + const atBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 1; + + if ((atTop && event.deltaY < 0) || (atBottom && event.deltaY > 0)) { + event.preventDefault(); + } + + // Load more when near bottom + const state = expandedImages[imageName]; + if (!state || !state.hasNext || state.loading || state.loadingMore) return; + + if (target.scrollHeight - target.scrollTop - target.clientHeight < 50) { + loadMoreTags(imageName); } } @@ -328,7 +395,8 @@ ...expandedImages, [imageName]: { ...state, - tags: state.tags.filter(t => t.name !== tag) + tags: state.tags.filter(t => t.name !== tag), + total: Math.max(0, state.total - 1) } }; } @@ -514,9 +582,9 @@ {expandState.error}
{:else if expandState?.tags && expandState.tags.length > 0} -
+
handleTagsWheel(e, result.name)}> - + @@ -590,7 +658,20 @@ {/each}
Tag Size
+ + {#if expandState.loadingMore} +
+ + Loading more... +
+ {/if}
+ + {#if expandState.total > 0} +
+ {expandState.tags.length} of {expandState.total} tags loaded +
+ {/if} {:else}
No tags found diff --git a/src/routes/settings/about/AboutTab.svelte b/src/routes/settings/about/AboutTab.svelte index 802e389..c2547b5 100644 --- a/src/routes/settings/about/AboutTab.svelte +++ b/src/routes/settings/about/AboutTab.svelte @@ -3,7 +3,7 @@ import * as Card from '$lib/components/ui/card'; import { Badge } from '$lib/components/ui/badge'; import { Input } from '$lib/components/ui/input'; - import { Box, Images, HardDrive, Network, Cpu, Server, Crown, Building2, Layers, Clock, Code, Package, ExternalLink, Search, FileText, Tag, Sparkles, Bug, ChevronDown, ChevronRight, Plug, ScrollText, Shield, MessageSquarePlus, GitBranch, Coffee } from 'lucide-svelte'; + import { Box, Images, HardDrive, Network, Cpu, Server, Crown, Building2, Layers, Clock, Code, Package, ExternalLink, Search, FileText, Tag, Sparkles, Bug, ChevronDown, ChevronRight, Plug, ScrollText, Shield, MessageSquarePlus, GitBranch, Coffee, Monitor, Cog, MemoryStick, Database } from 'lucide-svelte'; import * as Tabs from '$lib/components/ui/tabs'; import { onMount, onDestroy } from 'svelte'; import { licenseStore } from '$lib/stores/license'; @@ -198,6 +198,7 @@ nodeVersion: string; platform: string; arch: string; + kernel: string; memory: { heapUsed: number; heapTotal: number; @@ -438,7 +439,7 @@
{/if} {#if serverUptime !== null} -
+
Uptime {formatUptime(serverUptime)}
@@ -558,10 +559,13 @@ {/if} | - Platform + {systemInfo.runtime.platform}/{systemInfo.runtime.arch} | - Memory + + {systemInfo.runtime.kernel} + | + {formatBytes(systemInfo.runtime.memory.rss)}
{#if systemInfo.runtime.container.inContainer} @@ -585,7 +589,7 @@
- + Database
diff --git a/src/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte index b04f2f7..c0ccfd7 100644 --- a/src/routes/settings/environments/EnvironmentModal.svelte +++ b/src/routes/settings/environments/EnvironmentModal.svelte @@ -415,6 +415,10 @@ newLabelInput = ''; formPublicIp = environment.publicIp || ''; modalTab = 'general'; + // Reset token state for this environment (important when switching between envs) + hawserToken = null; + generatedToken = null; + pendingToken = null; // Load scanner settings, notifications, update check settings, and timezone loadScannerSettings(environment.id); loadEnvNotifications(environment.id); @@ -825,6 +829,11 @@ } } + // Reload only availability/versions without overwriting user's unsaved settings changes + async function reloadScannerAvailability(envId?: number) { + await loadScannerVersionsAsync(envId); + } + async function saveScannerSettings(envId?: number) { try { const response = await fetch('/api/settings/scanner', { @@ -930,7 +939,8 @@ checkingGrypeUpdate = true; grypeUpdateStatus = 'idle'; try { - const response = await fetch('/api/settings/scanner?checkUpdates=true'); + const envParam = environment?.id ? `&env=${environment.id}` : ''; + const response = await fetch(`/api/settings/scanner?checkUpdates=true${envParam}`); const data = await response.json(); if (data.updates) { grypeUpdateStatus = data.updates.grype?.hasUpdate ? 'update-available' : 'up-to-date'; @@ -947,7 +957,8 @@ checkingTrivyUpdate = true; trivyUpdateStatus = 'idle'; try { - const response = await fetch('/api/settings/scanner?checkUpdates=true'); + const envParam = environment?.id ? `&env=${environment.id}` : ''; + const response = await fetch(`/api/settings/scanner?checkUpdates=true${envParam}`); const data = await response.json(); if (data.updates) { trivyUpdateStatus = data.updates.trivy?.hasUpdate ? 'update-available' : 'up-to-date'; @@ -1198,7 +1209,7 @@ { if (o) focusFirstInput(); else onClose(); }}> - + {#if !isEditing} @@ -1362,7 +1373,7 @@ - +
@@ -2112,7 +2123,7 @@ {/if} {#if !loadingScannerVersions} {#if !scannerAvailability.grype} - loadScannerSettings(environment?.id)}> + reloadScannerAvailability(environment?.id)}> + {/if}
@@ -1281,7 +1274,8 @@ {#snippet cell(column, stack, rowState)} {@const source = getStackSource(stack.name)} {#if column.id === 'name'} - {#if source.sourceType === 'internal'} + {#if source.sourceType !== 'git'} + - {:else if source.sourceType === 'git' && source.gitStack} + {:else} + {/if} {/if} @@ -1947,6 +1947,12 @@ onSaved={fetchStacks} /> + showImportModal = false} + onAdopted={fetchStacks} +/> + (null); - // Stack location (for edit mode) - let stackLocation = $state(null); - let pathCopied = $state(false); - function copyPath() { - if (stackLocation) { - navigator.clipboard.writeText(stackLocation); - pathCopied = true; - setTimeout(() => pathCopied = false, 2000); + // ─── Path State (Simplified) ───────────────────────────────────────────────── + // Working paths: what we're currently editing (always strings, never null) + let workingComposePath = $state(''); + let workingEnvPath = $state(''); + + // Original paths: loaded from server (for dirty/change detection in edit mode) + let originalComposePath = $state(null); + let originalEnvPath = $state(null); + + // Auto-computed path from API (for create mode - tracks what the default would be) + let autoComputedComposePath = $state(''); + + // Path source info (for hint display) + let pathSource = $state<'default' | 'custom' | 'browsed' | null>(null); + + // UI state + let composePathCopied = $state(false); + let envPathCopied = $state(false); + let needsFileLocation = $state(false); + + // Container info for untracked stacks + let stackContainers = $state<{ name: string; state: string; image: string }[]>([]); + + // Derived: has user customized the compose path from auto-computed default? + const isComposePathCustom = $derived( + workingComposePath !== '' && workingComposePath !== autoComputedComposePath + ); + + // Derived: suggested env path when workingEnvPath is empty + const suggestedEnvPath = $derived( + !workingEnvPath && workingComposePath + ? workingComposePath.replace(/\/[^/]+$/, '/.env') + : null + ); + + // Derived: display path for env (actual or suggested) + const displayEnvPath = $derived(workingEnvPath || suggestedEnvPath || ''); + + // Derived: is env path just a suggestion (not explicitly set)? + const isEnvPathSuggested = $derived(!workingEnvPath && !!suggestedEnvPath); + + // Derived: source hint text for the path bar (only in create mode) + const pathSourceHint = $derived.by(() => { + if (mode !== 'create' || !workingComposePath) return undefined; + switch (pathSource) { + case 'browsed': + return 'Custom location'; + case 'custom': + return 'Using saved location'; + case 'default': + return 'Using default location'; + default: + return undefined; + } + }); + + // Path change confirmation dialog state + let showPathChangeConfirm = $state(false); + let pathChangeOldDir = $state(null); // Old directory to move files from + let pathChangeFileCount = $state(0); // Number of files in old directory + let pendingSaveRestart = $state(false); // Whether user clicked "Save & restart" vs "Save" + + // Browse confirmation dialog state (when selecting different file would replace content) + let showBrowseConfirm = $state(false); + let pendingBrowsePath = $state(null); + let pendingBrowseName = $state(null); + + // Single file browser with dynamic config + let showFileBrowser = $state(false); + let fileBrowserConfig = $state<{ + title: string; + icon?: Component<{ class?: string }>; + selectFilter?: RegExp; + selectMode: 'file' | 'directory' | 'file_or_directory'; + onSelect: (path: string, name: string) => void; + }>({ + title: '', + icon: undefined, + selectFilter: /.*/, + selectMode: 'file', + onSelect: () => {} + }); + + function openComposeBrowser() { + // For untracked stacks (needsFileLocation), only allow selecting files + // For tracked stacks, allow both files and directories + const isUntracked = needsFileLocation; + fileBrowserConfig = { + title: isUntracked ? 'Select compose file' : 'Select compose file or directory', + selectFilter: /\.ya?ml$/, + selectMode: isUntracked ? 'file' : 'file_or_directory', + onSelect: handleComposeSelect + }; + showFileBrowser = true; + } + + function openEnvBrowser() { + fileBrowserConfig = { + title: 'Select environment file or directory', + selectFilter: /\.env($|\.)/, // matches .env, .env.local, app.env, etc. + selectMode: 'file_or_directory', + onSelect: handleEnvSelect + }; + showFileBrowser = true; + } + + function openChangeLocationBrowser() { + const displayName = mode === 'edit' ? stackName : newStackName; + fileBrowserConfig = { + title: `Relocate ${displayName}`, + icon: FolderSync, + selectMode: 'directory', + onSelect: handleChangeLocation + }; + showFileBrowser = true; + } + + // State for change location confirmation + let pendingNewLocation = $state(null); + let pendingNewComposePath = $state(null); + let pendingNewEnvPath = $state(null); + let showChangeLocationConfirm = $state(false); + let changeLocationFileCount = $state(0); + let changeLocationOldDir = $state(null); + let movingLocation = $state(false); + + async function handleChangeLocation(selectedDir: string, _name: string) { + showFileBrowser = false; + + // Get the current compose filename + const currentComposePath = workingComposePath; + const composeFilename = currentComposePath ? currentComposePath.split('/').pop() : 'docker-compose.yml'; + + // Build new paths: create a subfolder with the stack name inside selected directory + const displayName = mode === 'edit' ? stackName : newStackName; + const newDir = `${selectedDir}/${displayName}`; + const newComposePath = `${newDir}/${composeFilename}`; + const newEnvPath = workingEnvPath ? `${newDir}/.env` : ''; + + // Check if old directory has files to move + const envId = $currentEnvironment?.id ?? null; + try { + const response = await fetch( + appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/check-path-change`, envId), + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ newComposePath }) + } + ); + + if (response.ok) { + const data = await response.json(); + if (data.hasChanges && data.oldDir && data.fileCount > 0) { + // Show confirmation dialog + pendingNewLocation = newDir; + pendingNewComposePath = newComposePath; + pendingNewEnvPath = newEnvPath; + changeLocationOldDir = data.oldDir; + changeLocationFileCount = data.fileCount; + showChangeLocationConfirm = true; + return; + } + } + } catch (e) { + console.warn('Failed to check path changes:', e); + } + + // No files to move, just update paths + workingComposePath = newComposePath; + workingEnvPath = newEnvPath; + isDirty = true; + } + + function cancelChangeLocation() { + showChangeLocationConfirm = false; + pendingNewLocation = null; + pendingNewComposePath = null; + pendingNewEnvPath = null; + changeLocationOldDir = null; + changeLocationFileCount = 0; + } + + async function confirmChangeLocation() { + if (!pendingNewComposePath || !changeLocationOldDir) return; + + movingLocation = true; + const envId = $currentEnvironment?.id ?? null; + + try { + // Call API to move files + const response = await fetch( + appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/relocate`, envId), + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + oldDir: changeLocationOldDir, + newComposePath: pendingNewComposePath, + newEnvPath: pendingNewEnvPath || undefined + }) + } + ); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to move files'); + } + + const result = await response.json(); + + // Update paths + workingComposePath = pendingNewComposePath; + workingEnvPath = pendingNewEnvPath || ''; + originalComposePath = pendingNewComposePath; + originalEnvPath = pendingNewEnvPath || null; + + // Reload content from new location + if (result.composeContent) { + composeContent = result.composeContent; + } + if (result.envVars) { + envVars = result.envVars; + } + if (result.rawEnvContent !== undefined) { + rawEnvContent = result.rawEnvContent; + } + + // Reset dirty flag since we just reloaded + isDirty = false; + + } catch (e: any) { + operationError = { + title: 'Failed to move files', + message: e.message || 'An error occurred while moving files' + }; + } finally { + movingLocation = false; + showChangeLocationConfirm = false; + pendingNewLocation = null; + pendingNewComposePath = null; + pendingNewEnvPath = null; + changeLocationOldDir = null; + changeLocationFileCount = 0; + } + } + + // Generic copy function that returns a reset callback + function copyToClipboard(text: string | null, setCopied: (v: boolean) => void) { + if (text) { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + } + + // Parse env vars from raw content + function parseEnvVarsFromRaw(content: string) { + const vars: EnvVar[] = []; + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIndex = trimmed.indexOf('='); + if (eqIndex > 0) { + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + vars.push({ key, value, isSecret: false }); + } + } + envVars = vars; + } + + // Handle compose file selection from browser + async function handleComposeSelect(path: string, name: string) { + const isDirectory = !path.match(/\.ya?ml$/i); + + // If selecting a file in edit mode with existing content, show confirmation + if (mode === 'edit' && !isDirectory && composeContent.trim()) { + // Check if it's the same file (no confirmation needed) + const normalizedPath = path.endsWith('/') ? path.slice(0, -1) : path; + if (normalizedPath !== workingComposePath) { + pendingBrowsePath = path; + pendingBrowseName = name; + showBrowseConfirm = true; + showFileBrowser = false; + return; + } + } + + // Continue with file selection + await proceedWithComposeSelect(path, name); + } + + // Proceed with compose file selection (after optional confirmation) + async function proceedWithComposeSelect(path: string, name: string) { + // Check if it's a directory (no extension or doesn't end with .yml/.yaml) + const isDirectory = !path.match(/\.ya?ml$/i); + const baseDir = path.endsWith('/') ? path.slice(0, -1) : path; + let finalPath = path; + + if (isDirectory) { + const stackName = newStackName.trim(); + if (stackName) { + // If we have a stack name, include the subfolder + finalPath = `${baseDir}/${stackName}/docker-compose.yml`; + } else { + // No stack name yet - just show the selected directory + finalPath = `${baseDir}/`; + } + } + + workingComposePath = finalPath; + pathSource = 'browsed'; + showFileBrowser = false; + + // Auto-suggest .env in the same directory (only if we have a full path) + if (!isDirectory || newStackName.trim()) { + const dir = finalPath.replace(/\/[^/]+$/, ''); + if (!workingEnvPath) { + workingEnvPath = `${dir}/.env`; + } + } + + // Load compose file content when selecting a file (not directory) + if (!isDirectory) { + await loadFilesFromLocalFilesystem(finalPath, workingEnvPath || suggestedEnvPath || ''); + } + isDirty = true; + } + + // Cancel browse confirmation + function cancelBrowseConfirm() { + showBrowseConfirm = false; + pendingBrowsePath = null; + pendingBrowseName = null; + } + + // Confirm browse and load the new file + async function confirmBrowseAndLoad() { + showBrowseConfirm = false; + if (pendingBrowsePath && pendingBrowseName) { + await proceedWithComposeSelect(pendingBrowsePath, pendingBrowseName); + } + pendingBrowsePath = null; + pendingBrowseName = null; + } + + // Handle env file selection from browser + async function handleEnvSelect(path: string, name: string) { + // Check if it's a directory (no extension or doesn't contain .env) + const isDirectory = !path.match(/\.env($|\.)/i); + let finalPath = path; + if (isDirectory) { + // Append default env filename + finalPath = path.endsWith('/') ? `${path}.env` : `${path}/.env`; + } + + workingEnvPath = finalPath; + showFileBrowser = false; + + // Load env content when selecting a file (not directory) + if (!isDirectory) { + try { + const envResponse = await fetch(`/api/system/files/content?path=${encodeURIComponent(finalPath)}`); + if (envResponse.ok) { + const envData = await envResponse.json(); + rawEnvContent = envData.content || ''; + parseEnvVarsFromRaw(rawEnvContent); + } else { + rawEnvContent = ''; + } + } catch (e) { + console.error('Failed to load env file:', e); + } + } + isDirty = true; + } + + // Load files from local filesystem (when user selects paths) + async function loadFilesFromLocalFilesystem(composeFilePath: string, envFilePath: string) { + try { + // Load compose file + const composeResponse = await fetch(`/api/system/files/content?path=${encodeURIComponent(composeFilePath)}`); + if (composeResponse.ok) { + const composeData = await composeResponse.json(); + composeContent = composeData.content || ''; + workingComposePath = composeFilePath; + // Clear the needsFileLocation flag since we now have content + needsFileLocation = false; + stackContainers = []; + } else { + const err = await composeResponse.json(); + console.error('Failed to load compose file:', err.error); + } + + // Try to load .env file (only set workingEnvPath if it exists) + if (envFilePath) { + const envResponse = await fetch(`/api/system/files/content?path=${encodeURIComponent(envFilePath)}`); + if (envResponse.ok) { + const envData = await envResponse.json(); + rawEnvContent = envData.content || ''; + workingEnvPath = envFilePath; + parseEnvVarsFromRaw(rawEnvContent); + } else { + // .env file not found - clear env path + rawEnvContent = ''; + workingEnvPath = ''; + } + } + } catch (e) { + console.error('Failed to load files:', e); } } @@ -255,50 +666,92 @@ services: loading = true; loadError = null; error = null; + needsFileLocation = false; try { const envId = $currentEnvironment?.id ?? null; // Load compose file - const response = await fetch(`/api/stacks/${encodeURIComponent(stackName)}/compose`); + const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/compose`, envId)); const data = await response.json(); if (!response.ok) { + // Check if this stack needs file location selection + if (data.needsFileLocation) { + needsFileLocation = true; + // Initialize paths from response (may have suggested paths) + workingComposePath = data.composePath || ''; + workingEnvPath = data.envPath || ''; + // Show empty editors - user can browse for files + composeContent = ''; + rawEnvContent = ''; + loadError = null; + loading = false; // Important: stop loading spinner + + // Fetch containers for this stack to show what's running + try { + const stacksRes = await fetch(appendEnvParam('/api/stacks', envId)); + if (stacksRes.ok) { + const stacks = await stacksRes.json(); + const thisStack = stacks.find((s: any) => s.name === stackName); + if (thisStack?.containerDetails) { + stackContainers = thisStack.containerDetails.map((c: any) => ({ + name: c.name || 'unknown', + state: c.state || 'unknown', + image: c.image || 'unknown' + })); + } + } + } catch (e) { + console.error('Failed to fetch stack containers:', e); + } + return; + } throw new Error(data.error || 'Failed to load compose file'); } composeContent = data.content; - stackLocation = data.stackDir || null; - - // Load environment variables (parsed) - const envResponse = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/env`, envId)); + // Set working paths + workingComposePath = data.composePath || ''; + workingEnvPath = data.envPath || ''; + // Track original paths for detecting changes + originalComposePath = data.composePath || null; + originalEnvPath = data.envPath || null; + + // Load both env endpoints in parallel, then process results together + const [envResponse, rawEnvResponse] = await Promise.all([ + fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/env`, envId)), + fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/env/raw`, envId)) + ]); + + // Process env vars from DB + let loadedVars: EnvVar[] = []; if (envResponse.ok) { const envData = await envResponse.json(); - envVars = envData.variables || []; - // Track if DB had any vars (for proper cleanup on clear-all) - hadExistingDbVars = envVars.length > 0; - // Track existing secret keys (secrets loaded from DB cannot have visibility toggled) + loadedVars = envData.variables || []; + hadExistingDbVars = loadedVars.length > 0; existingSecretKeys = new Set( - envVars.filter(v => v.isSecret && v.key.trim()).map(v => v.key.trim()) + loadedVars.filter(v => v.isSecret && v.key.trim()).map(v => v.key.trim()) ); } - // Load raw .env file content (for preserving comments) - const rawEnvResponse = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/env/raw`, envId)); + // Process raw .env file content + let loadedRawContent = ''; if (rawEnvResponse.ok) { const rawEnvData = await rawEnvResponse.json(); - rawEnvContent = rawEnvData.content || ''; - console.log('[loadComposeFile] rawEnvContent loaded:', rawEnvContent); + loadedRawContent = rawEnvData.content || ''; } + + // Pass data directly to syncAfterLoad - no tick() needed + // This sets both envVars and rawEnvContent synchronously via the panel + loading = false; + await tick(); // Wait for panel ref to be available + envVarsPanelRef?.syncAfterLoad(loadedVars, loadedRawContent); + isDirty = false; + } catch (e: any) { loadError = e.message; - } finally { loading = false; - // Merge variables and rawContent after both are loaded - await tick(); - envVarsPanelRef?.mergeOnLoad(); - // Reset dirty flag after loading completes - isDirty = false; } } @@ -367,23 +820,36 @@ services: try { const envId = $currentEnvironment?.id ?? null; - // Create the stack (include env vars and raw content for .env file) + // Build request body + const requestBody: Record = { + name: newStackName.trim(), + compose: content, + start, + // Send raw env content (non-secrets only, preserves comments/formatting) + rawEnvContent: prepared.rawContent.trim() ? prepared.rawContent : undefined, + // Also send parsed vars for DB secret tracking (includes secrets) + envVars: prepared.variables.length > 0 ? prepared.variables.map(v => ({ + key: v.key.trim(), + value: v.value, + isSecret: v.isSecret + })) : undefined + }; + + // Include custom paths if specified + if (workingComposePath.trim()) { + requestBody.composePath = workingComposePath.trim(); + } + // Use working env path or suggested path + const envPathToSave = workingEnvPath.trim() || suggestedEnvPath || ''; + if (envPathToSave) { + requestBody.envPath = envPathToSave; + } + + // Create the stack const response = await fetch(appendEnvParam('/api/stacks', envId), { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: newStackName.trim(), - compose: content, - start, - // Send raw env content (non-secrets only, preserves comments/formatting) - rawEnvContent: prepared.rawContent.trim() ? prepared.rawContent : undefined, - // Also send parsed vars for DB secret tracking (includes secrets) - envVars: prepared.variables.length > 0 ? prepared.variables.map(v => ({ - key: v.key.trim(), - value: v.value, - isSecret: v.isSecret - })) : undefined - }) + body: JSON.stringify(requestBody) }); if (!response.ok) { @@ -404,14 +870,56 @@ services: } } - async function handleSave(restart = false) { + async function handleSave(restart = false, moveFromDir: string | null = null) { errors = {}; - if (!composeContent.trim()) { - errors.compose = 'Compose file content cannot be empty'; + // Validate compose content (unless file location is needed and we have a path) + if (!composeContent.trim() && !workingComposePath.trim()) { + errors.compose = 'Compose file content or path is required'; + return; + } + + // If file location is needed, require a compose path + if (needsFileLocation && !workingComposePath.trim()) { + errors.compose = 'Please select a compose file location'; return; } + const envId = $currentEnvironment?.id ?? null; + + // Check if directory has changed (edit mode only, and not already confirmed) + if (mode === 'edit' && !moveFromDir) { + const newComposePath = workingComposePath.trim() || null; + + // Only check if compose path changed (which means directory changed) + if (newComposePath && originalComposePath && newComposePath !== originalComposePath) { + try { + const checkResponse = await fetch( + appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/check-path-change`, envId), + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ newComposePath }) + } + ); + if (checkResponse.ok) { + const checkData = await checkResponse.json(); + if (checkData.hasChanges && checkData.oldDir && checkData.fileCount > 0) { + // Show confirmation dialog + pathChangeOldDir = checkData.oldDir; + pathChangeFileCount = checkData.fileCount; + pendingSaveRestart = restart; + showPathChangeConfirm = true; + return; + } + } + } catch (e) { + console.warn('Failed to check path changes:', e); + // Continue with save even if check fails + } + } + } + saving = true; savingWithRestart = restart; error = null; @@ -419,19 +927,46 @@ services: // Prepare env vars for saving - syncs variables and rawContent const prepared = envVarsPanelRef?.prepareForSave() || { rawContent: '', variables: [] }; + // Resolve env path (use working or suggested) + const envPathToSave = workingEnvPath.trim() || suggestedEnvPath || ''; + try { - const envId = $currentEnvironment?.id ?? null; + // Build request body - include paths if they've been set/changed + const requestBody: Record = { + content: composeContent, + restart + }; + + // Include compose path if set (either custom path or user selected) + if (workingComposePath.trim()) { + requestBody.composePath = workingComposePath.trim(); + } + + // Include env path - empty string means "no env file", null/undefined means "use default" + if (envPathToSave) { + requestBody.envPath = envPathToSave; + } - // Save compose file + // Include old paths for file move/rename operations + if (originalComposePath && workingComposePath.trim() && originalComposePath !== workingComposePath.trim()) { + requestBody.oldComposePath = originalComposePath; + } + if (originalEnvPath && envPathToSave && originalEnvPath !== envPathToSave) { + requestBody.oldEnvPath = originalEnvPath; + } + + // Include old directory to move files from if user confirmed + if (moveFromDir) { + requestBody.moveFromDir = moveFromDir; + } + + // Save compose file (with optional paths) const response = await fetch( appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/compose`, envId), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - content: composeContent, - restart - }) + body: JSON.stringify(requestBody) } ); @@ -505,6 +1040,19 @@ services: } } + // Handle path change confirmation - move files to new location and proceed + function confirmPathChangeAndMove() { + showPathChangeConfirm = false; + handleSave(pendingSaveRestart, pathChangeOldDir); + } + + // Handle path change - keep old files and proceed (just save without moving) + function confirmPathChangeKeepFiles() { + showPathChangeConfirm = false; + // Pass empty string to skip move check this time (not null, which means "not checked yet") + handleSave(pendingSaveRestart, ''); + } + function tryClose() { if (isDirty) { showConfirmClose = true; @@ -535,7 +1083,25 @@ services: showConfirmClose = false; codeEditorRef = null; operationError = null; - stackLocation = null; + // Reset path state + workingComposePath = ''; + workingEnvPath = ''; + originalComposePath = null; + originalEnvPath = null; + autoComputedComposePath = ''; + pathSource = null; + needsFileLocation = false; + stackContainers = []; + showFileBrowser = false; + // Reset path change confirmation state + showPathChangeConfirm = false; + pathChangeOldDir = null; + pathChangeFileCount = 0; + pendingSaveRestart = false; + // Reset browse confirmation state + showBrowseConfirm = false; + pendingBrowsePath = null; + pendingBrowseName = null; onClose(); } @@ -580,6 +1146,56 @@ services: return () => clearTimeout(timeout); }); + + // Auto-update default paths when stack name changes in create mode + $effect(() => { + if (mode !== 'create' || !open) return; + // Don't overwrite if user has browsed and selected a path + if (pathSource === 'browsed') return; + + const name = newStackName.trim(); + const location = $appSettings.primaryStackLocation; + + if (!name) { + // Clear paths when no name + workingComposePath = ''; + workingEnvPath = ''; + autoComputedComposePath = ''; + pathSource = null; + return; + } + + // Fetch the actual absolute path from the backend + const envId = $currentEnvironment?.id ?? null; + const fetchDefaultPath = async () => { + try { + const params = new URLSearchParams({ name }); + if (envId) params.set('env', String(envId)); + if (location) { + params.set('location', location); + } + const response = await fetch(`/api/stacks/default-path?${params}`); + if (response.ok) { + const data = await response.json(); + // Check if user has customized before updating auto-computed + // Compare current working path against OLD auto path (before we update it) + const userHasCustomized = workingComposePath !== '' && + workingComposePath !== autoComputedComposePath; + // Track the auto-computed path + autoComputedComposePath = data.composePath; + // Only update working paths if user hasn't customized + if (!userHasCustomized) { + workingComposePath = data.composePath; + workingEnvPath = data.envPath; + pathSource = data.source || 'default'; + } + } + } catch (e) { + console.error('Failed to fetch default path:', e); + } + }; + fetchDefaultPath(); + }); @@ -620,24 +1238,8 @@ services: {#if mode === 'create'} Create a new Docker Compose stack - {:else if stackLocation} - - - {stackLocation} - - {:else} - Edit compose file and view stack structure + Edit compose file and environment variables {/if}
@@ -704,82 +1306,160 @@ services: Loading compose file...
- {:else if mode === 'edit' && loadError} -
-
-
- -
-

Could not load compose file

-

{loadError}

-

- This stack may have been created outside of Dockhand or the compose file may have been moved. -

-
-
{:else} - + {#if mode === 'create'}
-
- - errors.stackName = undefined} - /> - {#if errors.stackName} -

{errors.stackName}

- {/if} +
+
+ + errors.stackName = undefined} + /> + {#if errors.stackName} +

{errors.stackName}

+ {/if} +
+
+
+ {/if} + + + {#if mode === 'edit' && needsFileLocation} +
+
+ +
+

+ Untracked stack. Select the compose file location to start managing this stack with Dockhand. +

+ {#if stackContainers.length > 0} +
+ Running containers: +
+ {#each stackContainers as container} + + + {container.name} + + {/each} +
+
+ {/if} +
{/if} -
+
{#if activeTab === 'editor'} - -
- {#if open} -
- -
- {/if} -
- - From f972378117078583297abeaaf9cb3807233157bd Mon Sep 17 00:00:00 2001 From: sieren Date: Thu, 8 Jan 2026 13:24:40 +0100 Subject: [PATCH 29/30] Mobile: Only show total of stacks The detailed display of stacks (following x/x/x/x) is too wide for mobile display. So for mobile display only, we limit this information to the total number of stacks. --- src/routes/+page.svelte | 1 + src/routes/dashboard/EnvironmentTile.svelte | 15 ++++++++------- .../dashboard/dashboard-resource-stats.svelte | 5 +++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2a206df..ff79a1a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -994,6 +994,7 @@ width={2} height={Math.max(item.h, 2)} oneventsclick={() => handleEventsClick(tile.stats!.id)} + showStacksBreakdown={false} />
{/if} diff --git a/src/routes/dashboard/EnvironmentTile.svelte b/src/routes/dashboard/EnvironmentTile.svelte index 04f5d07..1f170f3 100644 --- a/src/routes/dashboard/EnvironmentTile.svelte +++ b/src/routes/dashboard/EnvironmentTile.svelte @@ -26,9 +26,10 @@ width?: number; height?: number; oneventsclick?: () => void; + showStacksBreakdown?: boolean; } - let { stats, width = 1, height = 1, oneventsclick }: Props = $props(); + let { stats, width = 1, height = 1, oneventsclick, showStacksBreakdown = true }: Props = $props(); const EnvIcon = $derived(getIconComponent(stats.icon)); @@ -349,7 +350,7 @@ {#if stats.collectMetrics && stats.metrics} {/if} - +
{/if} @@ -450,7 +451,7 @@ {#if stats.collectMetrics && stats.metrics} {/if} - + {#if stats.recentEvents} @@ -554,7 +555,7 @@ {#if stats.collectMetrics && stats.metrics} {/if} - + {#if stats.recentEvents} @@ -599,7 +600,7 @@ {#if stats.metrics} {/if} - +
@@ -645,7 +646,7 @@ {#if stats.metrics} {/if} - + {#if stats.recentEvents} @@ -697,7 +698,7 @@ {#if stats.metrics} {/if} - + {#if stats.recentEvents} diff --git a/src/routes/dashboard/dashboard-resource-stats.svelte b/src/routes/dashboard/dashboard-resource-stats.svelte index 5dd3457..321c15f 100644 --- a/src/routes/dashboard/dashboard-resource-stats.svelte +++ b/src/routes/dashboard/dashboard-resource-stats.svelte @@ -14,9 +14,10 @@ networks: { total: number }; stacks: { total: number; running: number; partial: number; stopped: number }; loading?: LoadingStates; + showStacksBreakdown?: boolean; } - let { images, volumes, networks, stacks, loading }: Props = $props(); + let { images, volumes, networks, stacks, loading, showStacksBreakdown = true }: Props = $props(); // Only show skeleton if loading AND we don't have data yet // This prevents blinking when refreshing with existing data @@ -46,7 +47,7 @@ {:else} {stacks.total} - {#if stacks.total > 0} + {#if showStacksBreakdown && stacks.total > 0} {stacks.running}/{stacks.partial}/{stacks.stopped} {/if} From c98eceb43cfc6ef1d765f826f50d8662a6660d86 Mon Sep 17 00:00:00 2001 From: zincognity <67128655+zincognity@users.noreply.github.com> Date: Tue, 13 Jan 2026 03:59:30 -0500 Subject: [PATCH 30/30] feat: add terminal attach mode and refactor container selection - Introduced a new mode toggle for terminal operations: 'exec' and 'attach'. - Updated container fetching logic to filter based on the selected mode. - Enhanced container selection to handle both exec and attach modes. - Refactored terminal component to support attach functionality with a new AttachTerminal component. - Improved UI elements for better user experience when switching modes. - Added handling for connection states and terminal commands based on the selected mode. --- scripts/patch-build.ts | 45 +- src/lib/data/dependencies.json | 22 +- src/lib/server/docker.ts | 22 +- src/lib/server/subprocess-manager.ts | 7 + .../api/containers/[id]/attach/+server.ts | 38 ++ src/routes/attach/AttachPanel.svelte | 188 ++++++++ src/routes/attach/AttachTerminal.svelte | 287 ++++++++++++ src/routes/attach/[id]/+page.svelte | 340 ++++++++++++++ src/routes/containers/+page.svelte | 438 ++++++++++-------- src/routes/terminal/+page.svelte | 408 +++++++++------- src/routes/terminal/Terminal.svelte | 4 +- vite.config.ts | 48 +- 12 files changed, 1451 insertions(+), 396 deletions(-) create mode 100644 src/routes/api/containers/[id]/attach/+server.ts create mode 100644 src/routes/attach/AttachPanel.svelte create mode 100644 src/routes/attach/AttachTerminal.svelte create mode 100644 src/routes/attach/[id]/+page.svelte diff --git a/scripts/patch-build.ts b/scripts/patch-build.ts index e10ecbc..df1b85a 100644 --- a/scripts/patch-build.ts +++ b/scripts/patch-build.ts @@ -54,6 +54,17 @@ var handleUpgrade = (request, bunServer) => { } } + // Handle terminal attach WebSocket + if (url.pathname.includes('/api/containers/') && url.pathname.includes('/attach')) { + const pathParts = url.pathname.split('/'); + const containerIdIndex = pathParts.indexOf('containers') + 1; + const containerId = pathParts[containerIdIndex]; + const envId = url.searchParams.get('envId') ? parseInt(url.searchParams.get('envId'), 10) : undefined; + if (bunServer.upgrade(request, { data: { type: 'terminal', mode: 'attach', containerId, envId } })) { + return new Response(null, { status: 101 }); + } + } + // Handle Hawser Edge WebSocket if (url.pathname === '/api/hawser/connect') { if (bunServer.upgrade(request, { data: { type: 'hawser' } })) { @@ -415,10 +426,11 @@ const combinedWebsocket = { const connId = 'ws-' + (++_wsConnCounter); ws.data = ws.data || {}; ws.data.connId = connId; - const { containerId, shell, user, envId } = ws.data; + const { containerId, shell, user, envId, mode } = ws.data; if (!containerId) { ws.send(JSON.stringify({ type: 'error', message: 'No container ID' })); ws.close(); return; } const target = await _getDockerTarget(envId); - console.log('[WS] Open:', connId, containerId, 'target:', target.type); + const isAttach = mode === 'attach'; + console.log('[WS] Open:', connId, containerId, 'mode:', mode || 'exec', 'target:', target.type); // Handle Hawser Edge terminal if (target.type === 'hawser-edge') { @@ -427,13 +439,32 @@ const combinedWebsocket = { const execId = crypto.randomUUID(); _edgeExecSessions.set(execId, { ws, execId, environmentId: target.environmentId }); ws.data.edgeExecId = execId; - conn.ws.send(JSON.stringify({ type: 'exec_start', execId, containerId, cmd: shell || '/bin/sh', user: user || 'root', cols: 120, rows: 30 })); + if (isAttach) { + conn.ws.send(JSON.stringify({ type: 'attach', containerId, attachId: execId })); + } else { + conn.ws.send(JSON.stringify({ type: 'exec_start', execId, containerId, cmd: shell || '/bin/sh', user: user || 'root', cols: 120, rows: 30 })); + } return; } try { - const exec = await createExec(containerId, [shell || '/bin/sh'], user || 'root', target); - const execId = exec.Id; + let execId; + let httpRequest; + + if (isAttach) { + // Attach directly to container streams + execId = crypto.randomUUID(); + const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : ''; + httpRequest = 'POST /containers/' + containerId + '/attach?stream=1&stdout=1&stderr=1&stdin=1 HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: 0\\r\\n\\r\\n'; + } else { + // Create exec instance for shell + const exec = await createExec(containerId, [shell || '/bin/sh'], user || 'root', target); + execId = exec.Id; + const body = JSON.stringify({ Detach: false, Tty: true }); + const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : ''; + httpRequest = 'POST /exec/' + execId + '/start HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: ' + body.length + '\\r\\n\\r\\n' + body; + } + let dockerStream; let headersStripped = false; let isChunked = false; @@ -454,9 +485,7 @@ const combinedWebsocket = { close() { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'exit' })); ws.close(); } }, error() {}, open(socket) { - const body = JSON.stringify({ Detach: false, Tty: true }); - const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : ''; - socket.write('POST /exec/' + execId + '/start HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: ' + body.length + '\\r\\n\\r\\n' + body); + socket.write(httpRequest); } }; if (target.type === 'unix') { diff --git a/src/lib/data/dependencies.json b/src/lib/data/dependencies.json index 61afae1..732c300 100644 --- a/src/lib/data/dependencies.json +++ b/src/lib/data/dependencies.json @@ -5,12 +5,6 @@ "license": "MIT", "repository": "https://github.com/codemirror/autocomplete" }, - { - "name": "@codemirror/commands", - "version": "6.10.0", - "license": "MIT", - "repository": "https://github.com/codemirror/commands" - }, { "name": "@codemirror/commands", "version": "6.10.1", @@ -157,7 +151,7 @@ }, { "name": "@lezer/html", - "version": "1.3.12", + "version": "1.3.13", "license": "MIT", "repository": "https://github.com/lezer-parser/html" }, @@ -175,13 +169,13 @@ }, { "name": "@lezer/lr", - "version": "1.4.4", + "version": "1.4.7", "license": "MIT", "repository": "https://github.com/lezer-parser/lr" }, { "name": "@lezer/markdown", - "version": "1.6.1", + "version": "1.6.3", "license": "MIT", "repository": "https://github.com/lezer-parser/markdown" }, @@ -235,7 +229,7 @@ }, { "name": "@types/node", - "version": "24.10.1", + "version": "25.0.7", "license": "MIT", "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped" }, @@ -343,7 +337,7 @@ }, { "name": "devalue", - "version": "5.5.0", + "version": "5.6.1", "license": "MIT", "repository": "https://github.com/sveltejs/devalue" }, @@ -355,7 +349,7 @@ }, { "name": "dockhand", - "version": "1.0.3", + "version": "1.0.7", "license": "UNLICENSED", "repository": null }, @@ -553,7 +547,7 @@ }, { "name": "svelte", - "version": "5.46.1", + "version": "5.46.3", "license": "MIT", "repository": "https://github.com/sveltejs/svelte" }, @@ -589,7 +583,7 @@ }, { "name": "webidl-conversions", - "version": "8.0.0", + "version": "8.0.1", "license": "BSD-2-Clause", "repository": "https://github.com/jsdom/webidl-conversions" }, diff --git a/src/lib/server/docker.ts b/src/lib/server/docker.ts index 050953e..fbe94e0 100644 --- a/src/lib/server/docker.ts +++ b/src/lib/server/docker.ts @@ -103,6 +103,7 @@ export interface ContainerInspectResult { Hostname: string; User: string; Tty: boolean; + OpenStdin: boolean; Env: string[]; Cmd: string[]; Image: string; @@ -661,6 +662,7 @@ export interface ContainerInfo { mounts: Array<{ type: string; source: string; destination: string; mode: string; rw: boolean }>; labels: { [key: string]: string }; command: string; + attachable?: boolean } export interface ImageInfo { @@ -682,6 +684,9 @@ export async function listContainers(all = true, envId?: number | null): Promise const restartCounts = new Map(); const restartingContainers = containers.filter(c => c.State === 'restarting'); + const attachableMap = new Map(); + const runningContainers = containers.filter(c => c.State === 'running'); + await Promise.all( restartingContainers.map(async (container) => { try { @@ -693,6 +698,20 @@ export async function listContainers(all = true, envId?: number | null): Promise }) ); + await Promise.all( + runningContainers.map(async (container) => { + try { + const inspect = await inspectContainer(container.Id, envId); + + const attachable = (inspect.Config.Tty && inspect.Config.OpenStdin) || false; + + attachableMap.set(container.Id, attachable); + } catch (error) { + attachableMap.set(container.Id, false); + } + }) + ) + return containers.map((container) => { // Extract network info with IP addresses const networks: { [networkName: string]: { ipAddress: string } } = {}; @@ -736,7 +755,8 @@ export async function listContainers(all = true, envId?: number | null): Promise restartCount: restartCounts.get(container.Id) || 0, mounts, labels: container.Labels || {}, - command: container.Command || '' + command: container.Command || '', + attachable: attachableMap.get(container.Id) || false }; }); } diff --git a/src/lib/server/subprocess-manager.ts b/src/lib/server/subprocess-manager.ts index f29c770..1b30440 100644 --- a/src/lib/server/subprocess-manager.ts +++ b/src/lib/server/subprocess-manager.ts @@ -26,6 +26,13 @@ function getSubprocessPath(name: string): string { if (existsSync(prodPath)) { return prodPath; } + + // Local build path - when running built version locally + const localBuildPath = path.join(process.cwd(), 'build', 'subprocesses', `${name}.js`); + + if(existsSync(localBuildPath)) { + return localBuildPath; + } // Development path (relative to this file) - raw TS files return path.join(__dirname, 'subprocesses', `${name}.ts`); } diff --git a/src/routes/api/containers/[id]/attach/+server.ts b/src/routes/api/containers/[id]/attach/+server.ts new file mode 100644 index 0000000..fd03312 --- /dev/null +++ b/src/routes/api/containers/[id]/attach/+server.ts @@ -0,0 +1,38 @@ +import { authorize } from '$lib/server/authorize'; +import { getDockerConnectionInfo } from "$lib/server/docker"; +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ params, cookies, url }) => { + const auth = await authorize(cookies); + + if (auth.authEnabled && !auth.isAuthenticated) { + return json({ error: "Unauthorized" }, { status: 401 }); + } + + const containerId = params.id; + const envIdParam = url.searchParams.get("envId"); + const envId = envIdParam ? parseInt(envIdParam, 10) : undefined; + + // Permission check with environment context + if (!(await auth.can("containers", "attach", envId))) { + return json({ error: "Permission denied" }, { status: 403 }); + } + + try { + const connectionInfo = await getDockerConnectionInfo(envId); + + return json({ + containerId, + connectionInfo: { + type: connectionInfo.type, + host: connectionInfo.host, + port: connectionInfo.port, + }, + }); + } catch (error) { + console.error("Error attaching to container:", error); + + return json({ error: "Failed to attach to container" }, { status: 500 }); + } +}; diff --git a/src/routes/attach/AttachPanel.svelte b/src/routes/attach/AttachPanel.svelte new file mode 100644 index 0000000..0609d01 --- /dev/null +++ b/src/routes/attach/AttachPanel.svelte @@ -0,0 +1,188 @@ + + +
+
+
+ Attach: {containerName} + {#if connected} + + + Attached + + {:else} + Detached + {/if} +
+
+ + + + +
+
+ + {#if !fillHeight} +
+ {/if} + +
+ +
+
+ + diff --git a/src/routes/attach/AttachTerminal.svelte b/src/routes/attach/AttachTerminal.svelte new file mode 100644 index 0000000..03519da --- /dev/null +++ b/src/routes/attach/AttachTerminal.svelte @@ -0,0 +1,287 @@ + + +
+ + diff --git a/src/routes/attach/[id]/+page.svelte b/src/routes/attach/[id]/+page.svelte new file mode 100644 index 0000000..b10badb --- /dev/null +++ b/src/routes/attach/[id]/+page.svelte @@ -0,0 +1,340 @@ + + + + Attach - {containerName || 'Loading...'} + + +
+ +
+
+ + {containerName || containerId} + {#if containerValid} + {#if connected} + + + Attached + + {:else} + Connecting... + {/if} + {:else if validationError} + {validationError} + {:else} + Validating... + {/if} +
+
+ Attach Mode +
+
+ + +
+ {#if validationError} +
+
+ +

{validationError}

+

+ Go back to attach page +

+
+
+ {:else if xtermLoaded && containerValid} +
+ {:else} +
+ Loading... +
+ {/if} +
+
+ + diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte index 5984f2e..8018400 100644 --- a/src/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -51,9 +51,11 @@ ShieldX, Shield, ShieldCheck, - Box + Box, + Zap } from 'lucide-svelte'; import { broom } from '@lucide/lab'; + import AttachPanel from '../attach/AttachPanel.svelte'; import CreateContainerModal from './CreateContainerModal.svelte'; import EditContainerModal from './EditContainerModal.svelte'; import TerminalPanel from '../terminal/TerminalPanel.svelte'; @@ -206,11 +208,27 @@ user: string; } let activeTerminals = $state([]); - let currentTerminalContainerId = $state(null); let terminalPopoverStates = $state>({}); let terminalShell = $state('/bin/bash'); let terminalUser = $state('root'); + // Attach state - track active attach sessions per container + interface ActiveAttach { + containerId: string; + containerName: string; + } + let activeAttach = $state([]); + + // Helper to check if container has active terminal + function hasActiveAttach(containerId: string): boolean { + return activeAttach.some(t => t.containerId === containerId); + } + + // Helper to get active terminal + function getActiveAttach(containerId: string): ActiveAttach | undefined { + return activeAttach.find(t => t.containerId === containerId); + } + // Confirmation popover state let confirmStopId = $state(null); let confirmRestartId = $state(null); @@ -524,7 +542,6 @@ containerName: string; } let activeLogs = $state([]); - let currentLogsContainerId = $state(null); // Helper to check if container has active logs function hasActiveLogs(containerId: string): boolean { @@ -1017,8 +1034,7 @@ const existingTerminal = getActiveTerminal(container.id); if (existingTerminal) { // Just show the existing terminal - currentTerminalContainerId = container.id; - terminalPopoverStates[container.id] = false; + return; } else { // Show popover to configure new terminal terminalPopoverStates[container.id] = true; @@ -1026,6 +1042,11 @@ } function startTerminal(container: ContainerInfo) { + // Check if we already have 2 active sessions + if (activeTerminals.length + activeAttach.length >= 2) { + toast.error('Maximum 2 terminal sessions allowed'); + return; + } // Create new terminal session const terminal: ActiveTerminal = { containerId: container.id, @@ -1034,22 +1055,35 @@ user: terminalUser }; activeTerminals = [...activeTerminals, terminal]; - currentTerminalContainerId = container.id; - terminalPopoverStates[container.id] = false; } function closeTerminal(containerId: string) { activeTerminals = activeTerminals.filter(t => t.containerId !== containerId); - if (currentTerminalContainerId === containerId) { - currentTerminalContainerId = null; + } + + function startAttach(container: ContainerInfo) { + // Check if we already have 2 active sessions + if (activeTerminals.length + activeAttach.length >= 2) { + toast.error('Maximum 2 terminal sessions allowed'); + return; } + // Create new attach session - automatically open it + const attach: ActiveAttach = { + containerId: container.id, + containerName: container.name + }; + activeAttach = [...activeAttach, attach]; + } + + function closeAttach(containerId: string) { + activeAttach = activeAttach.filter((a) => a.containerId !== containerId); } function showLogs(container: ContainerInfo) { // Check if there's already active logs for this container if (hasActiveLogs(container.id)) { // Just show the existing logs - currentLogsContainerId = container.id; + return; } else { // Create new logs session const logs: ActiveLogs = { @@ -1057,33 +1091,15 @@ containerName: container.name }; activeLogs = [...activeLogs, logs]; - currentLogsContainerId = container.id; } } function closeLogs(containerId: string) { activeLogs = activeLogs.filter(l => l.containerId !== containerId); - if (currentLogsContainerId === containerId) { - currentLogsContainerId = null; - } } function selectContainer(container: ContainerInfo) { - // Handle logs - show if container has active logs, hide otherwise - if (hasActiveLogs(container.id)) { - currentLogsContainerId = container.id; - } else if (currentLogsContainerId) { - // Hide current logs but keep the session active - currentLogsContainerId = null; - } - - // Handle terminal - show if container has active terminal, hide otherwise - if (hasActiveTerminal(container.id)) { - currentTerminalContainerId = container.id; - } else if (currentTerminalContainerId) { - // Hide current terminal but keep the session active - currentTerminalContainerId = null; - } + // No longer needed since all panels are shown } function editContainer(id: string) { @@ -1370,42 +1386,32 @@ defaultIcon={Box} />
- {#if $canAccess('containers', 'create')} - - {/if} - {/if} - Check for updates - - {#if batchUpdateContainerIds.length > 0} - - {/if} + + + + + + {#if $canAccess('containers', 'remove')} { let classes = ''; - if (currentLogsContainerId === container.id) classes += 'bg-blue-500/10 hover:bg-blue-500/15 '; - if (currentTerminalContainerId === container.id) classes += 'bg-green-500/10 hover:bg-green-500/15 '; + if (hasActiveLogs(container.id)) classes += 'bg-blue-500/10 hover:bg-blue-500/15 '; + if (hasActiveTerminal(container.id)) classes += 'bg-green-500/10 hover:bg-green-500/15 '; if ($appSettings.highlightUpdates && containersWithUpdatesSet.has(container.id)) classes += 'has-update '; return classes; }} onRowClick={(container, e) => { - if (activeLogs.length > 0 || activeTerminals.length > 0) { - selectContainer(container); - } highlightedRowId = highlightedRowId === container.id ? null : container.id; }} > @@ -1900,8 +1903,8 @@ {#if hasActiveLogs(container.id)} {/if} {/if} - {#if container.state === 'running' && $canAccess('containers', 'exec')} - {#if hasActiveTerminal(container.id)} - - {:else} - { terminalPopoverStates[container.id] = open; }}> - e.stopPropagation()} - class="p-0.5 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer" + {#if container.state === 'running' && $canAccess('containers', 'exec')} + {#if hasActiveTerminal(container.id) || hasActiveAttach(container.id)} + + {:else} + { + terminalPopoverStates[container.id] = open; + }} > - - - -
-
+ e.stopPropagation()} + class="p-0.5 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer" + > + + + +
{container.name}
-
-
-
- - - - - {shellOptions.find(o => o.value === terminalShell)?.label || 'Select'} + +
+ + {#if $canAccess('containers', 'exec')} +
+ + + + + {shellOptions.find((o) => o.value === terminalShell)?.label || 'Shell'} {#each shellOptions as option} - - - {option.label} - + + {option.label} + {/each} - -
-
- - - - - {userOptions.find(o => o.value === terminalUser)?.label || 'Select'} + + + + + + {userOptions.find((o) => o.value === terminalUser)?.label || 'User'} {#each userOptions as option} - - - {option.label} - + + {option.label} + {/each} - + + + +
+ {/if} + + + {#if container.attachable && $canAccess('containers', 'attach')} +
+ + +
+ {/if}
- -
- - - {/if} + + + {/if} {/if} {#if $canAccess('containers', 'remove')} - + {operationError.message} -
@@ -2016,7 +2057,7 @@ - {#if layoutMode === 'vertical' && (currentLogsContainerId || currentTerminalContainerId)} + {#if layoutMode === 'vertical' && (activeLogs.length > 0 || activeTerminals.length > 0 || activeAttach.length > 0)}
- - {#if currentLogsContainerId} - {@const activeLog = activeLogs.find(l => l.containerId === currentLogsContainerId)} - {#if activeLog} -
- closeLogs(activeLog.containerId)} - /> -
- {/if} - {/if} - - - {#if currentTerminalContainerId} - {@const activeTerminal = activeTerminals.find(t => t.containerId === currentTerminalContainerId)} - {#if activeTerminal} -
- closeTerminal(activeTerminal.containerId)} - /> -
- {/if} - {/if} + + {#each activeLogs as activeLog (activeLog.containerId)} +
+ closeLogs(activeLog.containerId)} + /> +
+ {/each} + + + {#each activeTerminals as activeTerminal (activeTerminal.containerId)} +
+ closeTerminal(activeTerminal.containerId)} + /> +
+ {/each} + + + {#each activeAttach as activeAtt (activeAtt.containerId)} +
+ closeAttach(activeAtt.containerId)} + /> +
+ {/each}
{/if}
- {#if layoutMode === 'horizontal'} - - {#if currentLogsContainerId} - {@const activeLog = activeLogs.find(l => l.containerId === currentLogsContainerId)} - {#if activeLog} - closeLogs(activeLog.containerId)} - /> - {/if} - {/if} - - - {#if currentTerminalContainerId} - {@const activeTerminal = activeTerminals.find(t => t.containerId === currentTerminalContainerId)} - {#if activeTerminal} - closeTerminal(activeTerminal.containerId)} - /> - {/if} - {/if} + {#if layoutMode === 'horizontal' && (activeLogs.length > 0 || activeTerminals.length > 0 || activeAttach.length > 0)} + + {#each activeLogs as activeLog (activeLog.containerId)} + closeLogs(activeLog.containerId)} + /> + {/each} + + + {#each activeTerminals as activeTerminal (activeTerminal.containerId)} + closeTerminal(activeTerminal.containerId)} + /> + {/each} + + + {#each activeAttach as activeAtt (activeAtt.containerId)} + closeAttach(activeAtt.containerId)} + /> + {/each} {/if} {/if}
diff --git a/src/routes/terminal/+page.svelte b/src/routes/terminal/+page.svelte index 4dd3fc2..ebd6ca3 100644 --- a/src/routes/terminal/+page.svelte +++ b/src/routes/terminal/+page.svelte @@ -1,26 +1,31 @@ @@ -172,169 +204,203 @@
{:else} -
- -
- -
- -
- - - +
+ +
+
+ +
+ +
+ +
- - - {#if dropdownOpen} -
- {#if filteredContainers().length === 0} -
- {containers.length === 0 ? 'No running containers' : 'No matches found'} -
- {:else} - {#each filteredContainers() as container} - - {/each} - {/if} +
+ +
+ + +
+ + + {#if dropdownOpen} +
+ {#if filteredContainers().length === 0} +
+ {containers.length === 0 ? (mode === 'exec' ? 'No running containers' : 'No containers') : 'No matches found'} +
+ {:else} + {#each filteredContainers() as container} + + {/each} + {/if} +
+ {/if} +
+ + {#if selectedContainer} + {/if} -
- {#if selectedContainer} - - {/if} - - {#if !selectedContainer} -
- - - - - {shellOptions.find(o => o.value === selectedShell)?.label || 'Select'} - - - {#each shellOptions as option} - + {#if !selectedContainer} + {#if mode === 'exec'} +
+ + + - {option.label} - - {/each} - - -
-
- - - - - {userOptions.find(o => o.value === selectedUser)?.label || 'Select'} - - - {#each userOptions as option} - + {shellOptions.find((o) => o.value === selectedShell)?.label || 'Select'} + + + {#each shellOptions as option} + + + {option.label} + + {/each} + + +
+
+ + + - {option.label} - - {/each} - - -
- {/if} -
+ {userOptions.find((o) => o.value === selectedUser)?.label || 'Select'} + + + {#each userOptions as option} + + + {option.label} + + {/each} + + +
+ {/if} + {/if} +
- -
- {#if !selectedContainer} -
-
- -

Select a container to open shell

+ +
+ {#if !selectedContainer} +
+
+ +

Select a container to {mode === 'exec' ? 'open shell' : 'attach'}

+
-
- {:else} - -
-
- {#if connected} - - - Connected - - {:else} - Disconnected - {/if} + {:else} + +
+
+ {#if connected} + + + {mode === 'exec' ? 'Connected' : 'Attached'} + + {:else} + {mode === 'exec' ? 'Disconnected' : 'Detached'} + {/if} +
+
+ changeFontSize(Number(v))}> + + {terminalFontSize}px + + + {#each fontSizeOptions as size} + {size}px + {/each} + + + + + +
-
- changeFontSize(Number(v))}> - - {terminalFontSize}px - - - {#each fontSizeOptions as size} - {size}px - {/each} - - - - - +
+ {#key `${selectedContainer.id}-${mode}`} + {#if mode === 'exec'} + + {:else if mode === 'attach'} + + {/if} + {/key}
-
-
- {#key selectedContainer.id} - - {/key} -
- {/if} + {/if} +
-
{/if}