├── .dockerignore ├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── assets │ └── arcane.png ├── dependabot.yml ├── eslint-matcher.json ├── svelte-check-matcher.json └── workflows │ ├── build-next.yml │ ├── codeql.yml │ ├── e2e-tests.yml │ ├── eslint.yml │ ├── njsscan.yml │ ├── release-image.yml │ └── svelte-check.yml ├── .gitignore ├── .prettierrc ├── .revision ├── .version ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── app-settings.json ├── components.json ├── docker-compose.yml ├── docs ├── .gitignore ├── README.md ├── docs │ ├── development │ │ ├── building.md │ │ └── contributing.md │ ├── features │ │ ├── containers │ │ │ ├── _category_.json │ │ │ └── overview.md │ │ ├── images │ │ │ ├── _category_.json │ │ │ └── overview.md │ │ ├── networks │ │ │ ├── _category_.json │ │ │ └── overview.md │ │ ├── stacks │ │ │ ├── _category_.json │ │ │ └── overview.md │ │ └── volumes │ │ │ ├── _category_.json │ │ │ └── overview.md │ ├── getting-started │ │ ├── configuration.md │ │ ├── quickstart.md │ │ └── user-management.md │ ├── guides │ │ ├── auto-update.md │ │ ├── troubleshooting.md │ │ └── working-with-registries.md │ ├── intro.md │ └── templates │ │ ├── create-a-registry.md │ │ └── use-templates.md ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ ├── FeatureCard │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── FeatureGrid │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── icons │ │ │ ├── ChartIcon.tsx │ │ │ ├── CodeIcon.tsx │ │ │ ├── ContainersIcon.tsx │ │ │ ├── ImagesIcon.tsx │ │ │ ├── LogIcon.tsx │ │ │ ├── MaturityIcon.tsx │ │ │ ├── NetworkIcon.tsx │ │ │ ├── ShieldIcon.tsx │ │ │ ├── StackIcon.tsx │ │ │ ├── VolumeIcons.tsx │ │ │ └── index.ts │ ├── css │ │ ├── custom.css │ │ └── docs.css │ ├── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md │ └── theme │ │ └── Root.js ├── static │ ├── .nojekyll │ └── img │ │ ├── arcane-dash1.png │ │ └── arcane.png └── tsconfig.json ├── eslint.config.js ├── package-lock.json ├── package.json ├── playwright.config.ts ├── scripts ├── create-admin.ts ├── development │ └── create-release.sh ├── docker │ └── entrypoint.sh ├── init-test-env.ts └── setup-test-images.ts ├── src ├── app.css ├── app.d.ts ├── app.html ├── hooks.server.ts ├── lib │ ├── components │ │ ├── LogViewer.svelte │ │ ├── action-buttons.svelte │ │ ├── arcane-button.svelte │ │ ├── badges │ │ │ ├── status-badge.svelte │ │ │ └── unused-text-badge.svelte │ │ ├── confirm-dialog │ │ │ ├── confirm-dialog.svelte │ │ │ └── index.ts │ │ ├── dialogs │ │ │ ├── prune-confirmation-dialog.svelte │ │ │ ├── registry-form-dialog.svelte │ │ │ └── user-form-dialog.svelte │ │ ├── dropdown-card.svelte │ │ ├── env-editor.svelte │ │ ├── form │ │ │ └── form-input.svelte │ │ ├── maturity-item.svelte │ │ ├── sidebar.svelte │ │ ├── stat-card.svelte │ │ ├── template-selection-dialog.svelte │ │ ├── ui │ │ │ ├── accordion │ │ │ │ ├── accordion-content.svelte │ │ │ │ ├── accordion-item.svelte │ │ │ │ ├── accordion-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── alert │ │ │ │ ├── alert-description.svelte │ │ │ │ ├── alert-title.svelte │ │ │ │ ├── alert.svelte │ │ │ │ └── index.ts │ │ │ ├── badge │ │ │ │ ├── badge.svelte │ │ │ │ └── index.ts │ │ │ ├── breadcrumb │ │ │ │ ├── breadcrumb-ellipsis.svelte │ │ │ │ ├── breadcrumb-item.svelte │ │ │ │ ├── breadcrumb-link.svelte │ │ │ │ ├── breadcrumb-list.svelte │ │ │ │ ├── breadcrumb-page.svelte │ │ │ │ ├── breadcrumb-separator.svelte │ │ │ │ ├── breadcrumb.svelte │ │ │ │ └── index.ts │ │ │ ├── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ ├── card │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-header.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ ├── card.svelte │ │ │ │ └── index.ts │ │ │ ├── checkbox │ │ │ │ ├── checkbox.svelte │ │ │ │ └── index.ts │ │ │ ├── data-table │ │ │ │ ├── data-table.svelte.ts │ │ │ │ ├── flex-render.svelte │ │ │ │ ├── index.ts │ │ │ │ └── render-helpers.ts │ │ │ ├── dialog │ │ │ │ ├── dialog-close.svelte │ │ │ │ ├── dialog-content.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ ├── dialog-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ └── index.ts │ │ │ ├── form │ │ │ │ ├── form-button.svelte │ │ │ │ ├── form-description.svelte │ │ │ │ ├── form-element-field.svelte │ │ │ │ ├── form-field-errors.svelte │ │ │ │ ├── form-field.svelte │ │ │ │ ├── form-fieldset.svelte │ │ │ │ ├── form-label.svelte │ │ │ │ ├── form-legend.svelte │ │ │ │ └── index.ts │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ ├── label │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ ├── pagination │ │ │ │ ├── index.ts │ │ │ │ ├── pagination-content.svelte │ │ │ │ ├── pagination-ellipsis.svelte │ │ │ │ ├── pagination-item.svelte │ │ │ │ ├── pagination-link.svelte │ │ │ │ ├── pagination-next-button.svelte │ │ │ │ ├── pagination-prev-button.svelte │ │ │ │ └── pagination.svelte │ │ │ ├── progress │ │ │ │ ├── index.ts │ │ │ │ └── progress.svelte │ │ │ ├── radio-group │ │ │ │ ├── index.ts │ │ │ │ ├── radio-group-item.svelte │ │ │ │ └── radio-group.svelte │ │ │ ├── resizable │ │ │ │ ├── index.ts │ │ │ │ ├── resizable-handle.svelte │ │ │ │ └── resizable-pane-group.svelte │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ ├── select-content.svelte │ │ │ │ ├── select-group-heading.svelte │ │ │ │ ├── select-item.svelte │ │ │ │ ├── select-scroll-down-button.svelte │ │ │ │ ├── select-scroll-up-button.svelte │ │ │ │ ├── select-separator.svelte │ │ │ │ └── select-trigger.svelte │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ ├── sonner │ │ │ │ ├── index.ts │ │ │ │ └── sonner.svelte │ │ │ ├── switch │ │ │ │ ├── index.ts │ │ │ │ └── switch.svelte │ │ │ ├── table │ │ │ │ ├── index.ts │ │ │ │ ├── table-body.svelte │ │ │ │ ├── table-caption.svelte │ │ │ │ ├── table-cell.svelte │ │ │ │ ├── table-footer.svelte │ │ │ │ ├── table-head.svelte │ │ │ │ ├── table-header.svelte │ │ │ │ ├── table-row.svelte │ │ │ │ └── table.svelte │ │ │ ├── tabs │ │ │ │ ├── index.ts │ │ │ │ ├── tabs-content.svelte │ │ │ │ ├── tabs-list.svelte │ │ │ │ └── tabs-trigger.svelte │ │ │ ├── textarea │ │ │ │ ├── index.ts │ │ │ │ └── textarea.svelte │ │ │ └── tooltip │ │ │ │ ├── index.ts │ │ │ │ └── tooltip-content.svelte │ │ ├── universal-table.svelte │ │ └── yaml-editor.svelte │ ├── constants.ts │ ├── index.ts │ ├── services │ │ ├── api │ │ │ ├── api-service.ts │ │ │ ├── container-api-service.ts │ │ │ ├── image-api-service.ts │ │ │ ├── network-api-service.ts │ │ │ ├── stack-api-service.ts │ │ │ ├── system-api-service.ts │ │ │ ├── template-api-service.ts │ │ │ ├── user-api-service.ts │ │ │ └── volume-api-service.ts │ │ ├── app-config-service.ts │ │ ├── converter-service.ts │ │ ├── docker │ │ │ ├── auto-update-service.ts │ │ │ ├── container-service.ts │ │ │ ├── core.ts │ │ │ ├── image-service.ts │ │ │ ├── maturity-cache-service.ts │ │ │ ├── network-service.ts │ │ │ ├── scheduler-service.ts │ │ │ ├── stack-custom-service.ts │ │ │ ├── stack-migration-service.ts │ │ │ ├── stack-service.ts │ │ │ ├── system-service.ts │ │ │ └── volume-service.ts │ │ ├── encryption-service.ts │ │ ├── oidc-service.ts │ │ ├── paths-service.ts │ │ ├── session-handler.ts │ │ ├── settings-service.ts │ │ ├── template-registry-service.ts │ │ ├── template-service.ts │ │ └── user-service.ts │ ├── stores │ │ ├── maturity-store.ts │ │ ├── settings-store.ts │ │ └── table-store.ts │ ├── types │ │ ├── actions.type.ts │ │ ├── application-configuration.ts │ │ ├── converter.type.ts │ │ ├── docker │ │ │ ├── connection.type.ts │ │ │ ├── image.type.ts │ │ │ ├── index.ts │ │ │ ├── prune.type.ts │ │ │ └── stack.type.ts │ │ ├── errors.ts │ │ ├── errors.type.ts │ │ ├── form.type.ts │ │ ├── loading-states.type.ts │ │ ├── session.type.ts │ │ ├── settings.type.ts │ │ ├── statuses.ts │ │ ├── table-types.ts │ │ ├── template-registry.ts │ │ └── user.type.ts │ ├── utils.ts │ └── utils │ │ ├── api.util.ts │ │ ├── bytes.util.ts │ │ ├── errors.util.ts │ │ ├── form.utils.ts │ │ ├── fs.utils.ts │ │ ├── onboarding.utils.ts │ │ ├── registry.utils.ts │ │ ├── string.utils.ts │ │ └── try-catch.ts └── routes │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.server.ts │ ├── +page.svelte │ ├── api │ ├── containers │ │ ├── +server.ts │ │ ├── [containerId] │ │ │ ├── +server.ts │ │ │ ├── logs │ │ │ │ └── stream │ │ │ │ │ └── +server.ts │ │ │ ├── pull │ │ │ │ └── +server.ts │ │ │ ├── remove │ │ │ │ └── +server.ts │ │ │ ├── restart │ │ │ │ └── +server.ts │ │ │ ├── start │ │ │ │ └── +server.ts │ │ │ ├── stats │ │ │ │ └── stream │ │ │ │ │ └── +server.ts │ │ │ └── stop │ │ │ │ └── +server.ts │ │ ├── start-all │ │ │ └── +server.ts │ │ └── stop-all │ │ │ └── +server.ts │ ├── convert │ │ └── +server.ts │ ├── docker │ │ ├── +server.ts │ │ └── test-connection │ │ │ └── +server.ts │ ├── images │ │ ├── +server.ts │ │ ├── [id] │ │ │ ├── +server.ts │ │ │ └── maturity │ │ │ │ └── +server.ts │ │ ├── prune │ │ │ └── +server.ts │ │ ├── pull-stream │ │ │ └── [...name] │ │ │ │ └── +server.ts │ │ └── pull │ │ │ └── [...name] │ │ │ └── +server.ts │ ├── networks │ │ ├── [id] │ │ │ └── +server.ts │ │ └── create │ │ │ └── +server.ts │ ├── settings │ │ └── +server.ts │ ├── stacks │ │ ├── [stackId] │ │ │ ├── +server.ts │ │ │ ├── deploy │ │ │ │ └── +server.ts │ │ │ ├── destroy │ │ │ │ └── +server.ts │ │ │ ├── down │ │ │ │ └── +server.ts │ │ │ ├── logs │ │ │ │ └── +server.ts │ │ │ ├── migrate │ │ │ │ └── +server.ts │ │ │ ├── pull │ │ │ │ └── +server.ts │ │ │ ├── redeploy │ │ │ │ └── +server.ts │ │ │ └── restart │ │ │ │ └── +server.ts │ │ ├── create │ │ │ └── +server.ts │ │ └── import │ │ │ └── +server.ts │ ├── system │ │ ├── auto-update │ │ │ └── +server.ts │ │ └── prune │ │ │ └── +server.ts │ ├── templates │ │ ├── +server.ts │ │ ├── [id] │ │ │ ├── +server.ts │ │ │ ├── content │ │ │ │ └── +server.ts │ │ │ └── download │ │ │ │ └── +server.ts │ │ ├── registries │ │ │ ├── +server.ts │ │ │ └── test │ │ │ │ └── +server.ts │ │ └── stats │ │ │ └── +server.ts │ ├── users │ │ ├── +server.ts │ │ ├── [id] │ │ │ └── +server.ts │ │ └── password │ │ │ └── +server.ts │ └── volumes │ │ ├── +server.ts │ │ └── [name] │ │ └── +server.ts │ ├── auth │ ├── login │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── logout │ │ └── +page.server.ts │ └── oidc │ │ ├── callback │ │ └── +server.ts │ │ └── login │ │ └── +server.ts │ ├── containers │ ├── +page.server.ts │ ├── +page.svelte │ ├── [id] │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── create-container-dialog.svelte │ ├── images │ ├── +page.server.ts │ ├── +page.svelte │ ├── [imageId] │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── pull-image-dialog.svelte │ ├── networks │ ├── +page.server.ts │ ├── +page.svelte │ ├── CreateNetworkDialog.svelte │ └── [networkId] │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── onboarding │ ├── +layout.svelte │ ├── complete │ │ └── +page.svelte │ ├── password │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── settings │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── welcome │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── settings │ ├── docker │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── general │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── rbac │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── security │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── templates │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── users │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── stacks │ ├── +page.server.ts │ ├── +page.svelte │ ├── [stackId] │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── new │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── volumes │ ├── +page.server.ts │ ├── +page.svelte │ ├── [volumeName] │ ├── +page.server.ts │ └── +page.svelte │ └── create-volume-dialog.svelte ├── static ├── favicon.png └── img │ ├── arcane.png │ └── arcane.svg ├── svelte.config.js ├── template-registry.json ├── tests ├── .auth │ └── login.json ├── auth.setup.ts ├── converter.spec.ts ├── images.spec.ts ├── networks.spec.ts └── volumes.spec.ts ├── tsconfig.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | # Version control 2 | .git 3 | .github 4 | .gitignore 5 | 6 | # Build artifacts 7 | node_modules 8 | build 9 | .svelte-kit 10 | coverage 11 | .coverage 12 | .nyc_output 13 | .env 14 | .env.* 15 | !.env.example 16 | 17 | # Development tools 18 | .vscode 19 | .idea 20 | *.sublime-* 21 | *.swp 22 | *.swo 23 | 24 | # Docker related 25 | Dockerfile 26 | .dockerignore 27 | docker-compose*.yml 28 | 29 | # Documentation and non-essential files 30 | README.md 31 | LICENSE 32 | *.md 33 | docs 34 | tests 35 | **/*.test.js 36 | **/*.spec.js 37 | **/*.stories.js 38 | 39 | # Temporary files 40 | .tmp 41 | .temp 42 | .cache 43 | **/.DS_Store 44 | npm-debug.log 45 | yarn-debug.log 46 | yarn-error.log 47 | pnpm-debug.log 48 | 49 | # Large media files that should be mounted or stored elsewhere 50 | **/*.mp4 51 | **/*.mov 52 | **/*.iso 53 | **/*.tar 54 | **/*.gz 55 | **/*.zip -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_ENV=production 2 | PUBLIC_SESSION_SECRET=your-secure-random-32-character-string-here 3 | 4 | # OIDC Provider Settings 5 | PUBLIC_OIDC_ENABLED=false 6 | OIDC_CLIENT_ID="your_oidc_client_id" 7 | OIDC_CLIENT_SECRET="your_oidc_client_secret" 8 | OIDC_REDIRECT_URI="http://localhost:5173/auth/oidc/callback" 9 | OIDC_AUTHORIZATION_ENDPOINT="your_oidc_provider_authorization_endpoint_url" 10 | OIDC_TOKEN_ENDPOINT="your_oidc_provider_token_endpoint_url" 11 | OIDC_USERINFO_ENDPOINT="your_oidc_provider_userinfo_endpoint_url" 12 | OIDC_SCOPES="openid email profile" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kmendell 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/assets/arcane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofkm/arcane/076cbf490485b5062f1320f37ac89443d8cef25c/.github/assets/arcane.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | groups: 8 | prod-dependencies: 9 | patterns: 10 | - "*" 11 | exclude-patterns: 12 | - "@types/*" 13 | - "typescript" 14 | - "eslint*" 15 | - "prettier*" 16 | - "vite" 17 | - "*-plugin" 18 | - "*-loader" 19 | - "jest*" 20 | - "vitest*" 21 | - "@testing-library/*" 22 | - "@playwright/*" 23 | update-types: 24 | - "minor" 25 | - "patch" 26 | dev-dependencies: 27 | patterns: 28 | - "@types/*" 29 | - "typescript" 30 | - "eslint*" 31 | - "prettier*" 32 | - "vite" 33 | - "*-plugin" 34 | - "*-loader" 35 | - "jest*" 36 | - "vitest*" 37 | - "@testing-library/*" 38 | - "@playwright/*" 39 | update-types: 40 | - "minor" 41 | - "patch" 42 | frontend-major-updates: 43 | patterns: 44 | - "*" 45 | update-types: 46 | - "major" 47 | 48 | - package-ecosystem: 'github-actions' 49 | directory: '/' 50 | schedule: 51 | interval: 'daily' 52 | 53 | - package-ecosystem: 'docker' 54 | directory: '/' 55 | schedule: 56 | interval: 'daily' 57 | -------------------------------------------------------------------------------- /.github/eslint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "eslint", 5 | "pattern": [ 6 | { 7 | "regexp": "^([^\\s].*)$", 8 | "file": 1 9 | }, 10 | { 11 | "regexp": "^\\s+(\\d+):(\\d+)\\s+(error|warning|info)\\s+(.*)\\s\\s+(.*)$", 12 | "line": 1, 13 | "column": 2, 14 | "severity": 3, 15 | "message": 4, 16 | "code": 5 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/svelte-check-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "svelte-check", 5 | "pattern": [ 6 | { 7 | "regexp": "^([^\\s].*):(\\d+):(\\d+)$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3 11 | }, 12 | { 13 | "regexp": "^\\s*(Error|Warning):\\s*(.*)\\s+\\((?:ts|js|svelte)\\)$", 14 | "severity": 1, 15 | "message": 2, 16 | "loop": false 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/build-next.yml: -------------------------------------------------------------------------------- 1 | name: Docker Publish - Next 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - 'Dockerfile' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-and-push: 14 | runs-on: ubuntu-22.04 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 1 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Log in to GitHub Container Registry 32 | uses: docker/login-action@v3 33 | with: 34 | registry: ghcr.io 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@v6 40 | with: 41 | context: . 42 | push: true 43 | tags: ghcr.io/ofkm/arcane:next 44 | cache-from: type=gha 45 | cache-to: type=gha,mode=max 46 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | schedule: 16 | - cron: '41 18 * * 3' 17 | 18 | jobs: 19 | eslint: 20 | name: Run eslint scanning 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | security-events: write 25 | actions: read 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Install ESLint 31 | run: | 32 | npm install eslint@9.25.1 33 | npm install @microsoft/eslint-formatter-sarif@3.1.0 34 | 35 | - name: Run ESLint 36 | env: 37 | SARIF_ESLINT_IGNORE_SUPPRESSED: "true" 38 | run: npx eslint ./src 39 | --config eslint.config.js 40 | --ext .js,.jsx,.ts,.tsx 41 | --format @microsoft/eslint-formatter-sarif 42 | --output-file eslint-results.sarif 43 | continue-on-error: true 44 | 45 | - name: Upload analysis results to GitHub 46 | uses: github/codeql-action/upload-sarif@v3 47 | with: 48 | sarif_file: eslint-results.sarif 49 | wait-for-processing: true 50 | -------------------------------------------------------------------------------- /.github/workflows/njsscan.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow integrates njsscan with GitHub's Code Scanning feature 7 | # nodejsscan is a static security code scanner that finds insecure code patterns in your Node.js applications 8 | 9 | name: njsscan sarif 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | # The branches below must be a subset of the branches above 16 | branches: [ "main" ] 17 | schedule: 18 | - cron: '37 9 * * 4' 19 | 20 | permissions: 21 | contents: read 22 | 23 | jobs: 24 | njsscan: 25 | permissions: 26 | contents: read # for actions/checkout to fetch code 27 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 28 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 29 | runs-on: ubuntu-latest 30 | name: njsscan code scanning 31 | steps: 32 | - name: Checkout the code 33 | uses: actions/checkout@v4 34 | - name: nodejsscan scan 35 | id: njsscan 36 | uses: ajinabraham/njsscan-action@231750a435d85095d33be7d192d52ec650625146 37 | with: 38 | args: '. --sarif --output results.sarif || true' 39 | - name: Upload njsscan report 40 | uses: github/codeql-action/upload-sarif@v3 41 | with: 42 | sarif_file: results.sarif 43 | -------------------------------------------------------------------------------- /.github/workflows/svelte-check.yml: -------------------------------------------------------------------------------- 1 | name: Svelte Check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'src/**' 8 | - '.github/workflows/type-check.yml' 9 | - '.github/svelte-check-matcher.json' 10 | - 'package.json' 11 | - 'package-lock.json' 12 | - 'tsconfig.json' 13 | - 'svelte.config.js' 14 | pull_request: 15 | branches: [main] 16 | paths: 17 | - 'src/**' 18 | - '.github/workflows/type-check.yml' 19 | - '.github/svelte-check-matcher.json' 20 | - 'package.json' 21 | - 'package-lock.json' 22 | - 'tsconfig.json' 23 | - 'svelte.config.js' 24 | workflow_dispatch: 25 | 26 | jobs: 27 | type-check: 28 | name: Run Svelte Check 29 | # Don't run on dependabot branches 30 | if: github.actor != 'dependabot[bot]' 31 | runs-on: ubuntu-latest 32 | permissions: 33 | contents: read 34 | checks: write 35 | pull-requests: write 36 | 37 | steps: 38 | - name: Checkout code 39 | uses: actions/checkout@v4 40 | 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: 'lts/*' 45 | cache: 'npm' 46 | cache-dependency-path: package-lock.json 47 | 48 | - name: Install dependencies 49 | run: npm ci 50 | 51 | - name: Add svelte-check problem matcher 52 | run: echo "::add-matcher::.github/svelte-check-matcher.json" 53 | 54 | - name: Run svelte-check via npm script 55 | run: npm run check 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | 25 | .arcane/* 26 | .arcane/stacks/* 27 | .dev-data/* 28 | .dev-data/settings/* 29 | 30 | test-results/* 31 | tests/.report/* 32 | .test-data/* 33 | data/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 400, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.revision: -------------------------------------------------------------------------------- 1 | c47d15f 2 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 0.14.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "github.copilot.enable": { 3 | "*": false, 4 | "plaintext": false, 5 | "markdown": false, 6 | "scminput": false, 7 | "typescript": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Kyle Mendell 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Vulnerabilities 4 | 5 | Thank you for helping us keep this project secure! 6 | 7 | If you discover a security vulnerability (such as a CVE), **please do not file an issue on GitHub**. Publicly disclosing vulnerabilities in GitHub issues can put users at risk before the problem is addressed. 8 | 9 | Instead, please report vulnerabilities directly to our security team by emailing **[ksm@ofkm.us](mailto:ksm@ofkm.us)**. 10 | 11 | We will review your report promptly and work to resolve the issue as quickly as possible. When submitting a report, please include as much detail as possible, such as: 12 | 13 | - A description of the vulnerability. 14 | - Steps to reproduce the issue. 15 | - Potential impact of the vulnerability. 16 | - Any suggested fixes or mitigations. 17 | 18 | We appreciate your responsible disclosure and your assistance in keeping this project secure. 19 | 20 | --- 21 | 22 | For non-security-related issues, feel free to use the GitHub issue tracker as usual. 23 | -------------------------------------------------------------------------------- /app-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerHost": "unix:///var/run/docker.sock", 3 | "autoRefresh": true, 4 | "pollingInterval": 5, 5 | "stacksDirectory": "/app/data/stacks" 6 | } 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "css": "src/app.css", 6 | "baseColor": "violet" 7 | }, 8 | "aliases": { 9 | "components": "$lib/components", 10 | "utils": "$lib/utils", 11 | "ui": "$lib/components/ui", 12 | "hooks": "$lib/hooks" 13 | }, 14 | "typescript": true, 15 | "registry": "https://next.shadcn-svelte.com/registry" 16 | } 17 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/docs/features/containers/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Containers", 3 | "collapsible": true, 4 | "collapsed": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/features/images/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Images", 3 | "collapsible": true, 4 | "collapsed": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/features/networks/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Networks", 3 | "collapsible": true, 4 | "collapsed": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/features/stacks/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Stacks", 3 | "collapsible": true, 4 | "collapsed": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/features/volumes/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Volumes", 3 | "collapsible": true, 4 | "collapsed": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/docs/features/volumes/overview.md: -------------------------------------------------------------------------------- 1 | # Volume Management 2 | 3 | ## What Can You Do With Volumes in Arcane? 4 | 5 | - **View Volumes:** See a list of all Docker volumes on your system, along with details like name, driver, and usage. 6 | - **Create Volumes:** Add a new volume by providing a name and (optionally) driver or labels. 7 | - **Remove Volumes:** Delete volumes you no longer need. Arcane will warn you if a volume is currently in use by a container. 8 | 9 | ## How to Use 10 | 11 | ### Viewing Volumes 12 | 13 | 1. Go to the **Volumes** section in the sidebar. 14 | 2. You’ll see a table listing all your Docker volumes. 15 | 16 | ### Creating a Volume 17 | 18 | 1. Click the **Create Volume** button. 19 | 2. Enter a name for your new volume. 20 | 3. (Optional) Choose a driver or add labels if needed. 21 | 4. Click **Create**. Your new volume will appear in the list. 22 | 23 | ### Removing a Volume 24 | 25 | 1. In the volumes list, find the volume you want to remove. 26 | 2. Click the dropdown then the **Delete** button (trash icon) in the list. 27 | 3. Confirm the deletion in the dialog. 28 | > **Note:** You cannot remove a volume that is currently used by a container. 29 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: / 4 | sidebar_label: Introduction 5 | --- 6 | 7 | # Introduction to Arcane 8 | 9 | Welcome to the documentation for Arcane! 10 | 11 | Arcane is a **Simple and Elegant Docker Management UI** written in Typescript and SvelteKit. It aims to provide an intuitive interface for managing your Docker containers, images, volumes, and networks. 12 | 13 | ## Key Features 14 | 15 | - **Modern UI Interface** - Clean, intuitive design that makes Docker management a breeze 16 | - **Real-time Monitoring** - Live updates of container status, resource usage, and logs 17 | - **Container Management** - Start, stop, restart, and inspect containers with ease 18 | - **Image Management** - Pull, and manage Docker images 19 | - **Network Configuration** - Create and configure Docker networks 20 | - **Volume Management** - Create and manage persistent data with Docker volumes 21 | - **Resource Visualization** - Visual graphs for CPU, memory, and network usage 22 | 23 | ## Getting Started 24 | 25 | To get started with Arcane, head over to the **[Quickstart](./getting-started/quickstart)** guide. 26 | 27 |
36 |
37 | 💡 38 |
39 |
40 | Pro Tip: Arcane works best with Docker version 20.10.0 or higher. Check your version with docker --version. 41 |
42 |
43 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@dipakparmar/docusaurus-plugin-umami": "^2.2.0", 19 | "@docusaurus/core": "3.7.0", 20 | "@docusaurus/preset-classic": "3.7.0", 21 | "@mdx-js/react": "^3.0.0", 22 | "clsx": "^2.0.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0", 26 | "react-feather": "^2.0.10" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "3.7.0", 30 | "@docusaurus/tsconfig": "3.7.0", 31 | "@docusaurus/types": "3.7.0", 32 | "typescript": "~5.6.2" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 3 chrome version", 42 | "last 3 firefox version", 43 | "last 5 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=18.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Sidebar configuration for Arcane documentation 5 | */ 6 | const sidebars: SidebarsConfig = { 7 | docs: [ 8 | // Introduction (homepage) 9 | 'intro', 10 | // Getting Started section 11 | { 12 | type: 'category', 13 | label: 'Getting Started', 14 | collapsible: true, 15 | collapsed: true, 16 | items: [ 17 | { 18 | type: 'autogenerated', 19 | dirName: 'getting-started' 20 | } 21 | ] 22 | }, 23 | 24 | // Features section with nested categories 25 | { 26 | type: 'category', 27 | label: 'Features', 28 | collapsible: true, 29 | collapsed: true, 30 | items: [ 31 | { 32 | type: 'autogenerated', 33 | dirName: 'features' 34 | } 35 | ] 36 | }, 37 | 38 | // How-to Guides 39 | { 40 | type: 'category', 41 | label: 'Templates', 42 | collapsible: true, 43 | collapsed: true, 44 | items: [ 45 | { 46 | type: 'autogenerated', 47 | dirName: 'templates' 48 | } 49 | ] 50 | }, 51 | 52 | // How-to Guides 53 | { 54 | type: 'category', 55 | label: 'Guides', 56 | collapsible: true, 57 | collapsed: true, 58 | items: [ 59 | { 60 | type: 'autogenerated', 61 | dirName: 'guides' 62 | } 63 | ] 64 | }, 65 | 66 | // Development/Contributing 67 | { 68 | type: 'category', 69 | label: 'Development', 70 | collapsible: true, 71 | collapsed: true, 72 | items: [ 73 | { 74 | type: 'autogenerated', 75 | dirName: 'development' 76 | } 77 | ] 78 | } 79 | ] 80 | }; 81 | 82 | export default sidebars; 83 | -------------------------------------------------------------------------------- /docs/src/components/FeatureCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './styles.module.css'; 4 | 5 | export default function FeatureCard({ title, description, icon, className, ...props }) { 6 | return ( 7 |
8 | {icon &&
{React.cloneElement(icon, { className: styles.featureIcon })}
} 9 |

{title}

10 |

{description}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /docs/src/components/FeatureCard/styles.module.css: -------------------------------------------------------------------------------- 1 | .featureCard { 2 | padding: 1.5rem; 3 | border-radius: 0.75rem; 4 | background: rgba(255, 255, 255, 0.05); 5 | backdrop-filter: blur(10px); 6 | -webkit-backdrop-filter: blur(10px); 7 | border: 1px solid rgba(139, 92, 246, 0.1); 8 | transition: all 0.3s ease; 9 | height: 100%; 10 | } 11 | 12 | [data-theme='dark'] .featureCard { 13 | background: rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | .featureCard:hover { 17 | transform: translateY(-5px); 18 | box-shadow: 19 | 0 20px 25px -5px rgba(139, 92, 246, 0.1), 20 | 0 10px 10px -5px rgba(139, 92, 246, 0.04); 21 | border-color: rgba(139, 92, 246, 0.3); 22 | } 23 | 24 | .featureIconWrapper { 25 | margin-bottom: 1.5rem; 26 | width: 60px; 27 | height: 60px; 28 | border-radius: 50%; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | background: linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(139, 92, 246, 0.1) 100%); 33 | } 34 | 35 | .featureIcon { 36 | width: 32px; 37 | height: 32px; 38 | color: var(--ifm-color-primary); 39 | } 40 | 41 | .featureTitle { 42 | margin-bottom: 1rem; 43 | font-size: 1.25rem; 44 | font-weight: 600; 45 | } 46 | 47 | .featureDescription { 48 | color: var(--ifm-color-emphasis-700); 49 | margin-bottom: 0; 50 | font-size: 0.9rem; 51 | line-height: 1.6; 52 | } 53 | -------------------------------------------------------------------------------- /docs/src/components/FeatureGrid/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.module.css'; 3 | 4 | export default function FeatureGrid({ children, columns = 3, className, ...props }) { 5 | return ( 6 |
7 | {children} 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/components/FeatureGrid/styles.module.css: -------------------------------------------------------------------------------- 1 | .featureGrid { 2 | display: grid; 3 | grid-template-columns: repeat(var(--columns, 3), 1fr); 4 | gap: 2rem; 5 | } 6 | 7 | @media screen and (max-width: 996px) { 8 | .featureGrid { 9 | grid-template-columns: 1fr; 10 | gap: 1.5rem; 11 | } 12 | } 13 | 14 | @media screen and (min-width: 997px) and (max-width: 1200px) { 15 | .featureGrid { 16 | grid-template-columns: repeat(2, 1fr); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 4rem 0; 5 | width: 100%; 6 | background-color: var(--ifm-background-color); 7 | } 8 | 9 | .featuresHeader { 10 | text-align: center; 11 | margin-bottom: 3rem; 12 | } 13 | 14 | .featuresTitle { 15 | font-size: 2.5rem; 16 | font-weight: 700; 17 | margin-bottom: 1rem; 18 | } 19 | 20 | .featuresSubtitle { 21 | font-size: 1.25rem; 22 | color: var(--ifm-color-emphasis-600); 23 | max-width: 600px; 24 | margin: 0 auto; 25 | } 26 | 27 | .featureGrid { 28 | display: grid; 29 | gap: 2rem; 30 | } 31 | 32 | .featureCard { 33 | padding: 1.5rem; 34 | border-radius: var(--ifm-card-border-radius); 35 | background: var(--ifm-background-color); 36 | border: 1px solid var(--ifm-color-emphasis-200); 37 | transition: all 0.2s ease; 38 | } 39 | 40 | .featureCard:hover { 41 | transform: translateY(-2px); 42 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 43 | } 44 | 45 | .featureIcon { 46 | width: 24px; 47 | height: 24px; 48 | color: var(--ifm-color-primary); 49 | margin-bottom: 1rem; 50 | } 51 | 52 | .featureSvg { 53 | height: 200px; 54 | width: 200px; 55 | } 56 | 57 | @media screen and (max-width: 996px) { 58 | .features { 59 | padding: 2rem 0; 60 | } 61 | 62 | .featuresTitle { 63 | font-size: 2rem; 64 | } 65 | 66 | .featureGrid { 67 | gap: 1rem; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/src/components/icons/ChartIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ChartIcon() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /docs/src/components/icons/CodeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function CodeIcon() { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | } -------------------------------------------------------------------------------- /docs/src/components/icons/ContainersIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ContainersIcon({ className }) { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/components/icons/ImagesIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ImagesIcon({ className }) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/icons/LogIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function LogIcon() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } -------------------------------------------------------------------------------- /docs/src/components/icons/MaturityIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function MaturityIcon({ className }) { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /docs/src/components/icons/NetworkIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NetworkIcon({ className }) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/components/icons/ShieldIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ShieldIcon() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } -------------------------------------------------------------------------------- /docs/src/components/icons/StackIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function StackIcon() { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | } -------------------------------------------------------------------------------- /docs/src/components/icons/VolumeIcons.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | export default function VolumeIcon() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /docs/src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ContainersIcon } from './ContainersIcon'; 2 | export { default as ImagesIcon } from './ImagesIcon'; 3 | export { default as NetworkIcon } from './NetworkIcon'; 4 | export { default as ChartIcon } from './ChartIcon'; 5 | export { default as ShieldIcon } from './ShieldIcon'; 6 | export { default as CodeIcon } from './CodeIcon'; 7 | export { default as MaturityIcon } from './MaturityIcon'; 8 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/src/theme/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from '@docusaurus/Head'; 3 | 4 | export default function Root({ children }) { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofkm/arcane/076cbf490485b5062f1320f37ac89443d8cef25c/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/arcane-dash1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofkm/arcane/076cbf490485b5062f1320f37ac89443d8cef25c/docs/static/img/arcane-dash1.png -------------------------------------------------------------------------------- /docs/static/img/arcane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofkm/arcane/076cbf490485b5062f1320f37ac89443d8cef25c/docs/static/img/arcane.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import { includeIgnoreFile } from '@eslint/compat'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import { fileURLToPath } from 'node:url'; 6 | import ts from 'typescript-eslint'; 7 | import svelteConfig from './svelte.config.js'; 8 | 9 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 10 | 11 | export default ts.config( 12 | includeIgnoreFile(gitignorePath), 13 | js.configs.recommended, 14 | ...ts.configs.recommended, 15 | ...svelte.configs.recommended, 16 | { 17 | languageOptions: { 18 | globals: { ...globals.browser, ...globals.node } 19 | }, 20 | rules: { 21 | // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. 22 | // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors 23 | 'no-undef': 'off' 24 | } 25 | }, 26 | { 27 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 28 | languageOptions: { 29 | parserOptions: { 30 | projectService: true, 31 | extraFileExtensions: ['.svelte'], 32 | parser: ts.parser, 33 | svelteConfig 34 | } 35 | } 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | const authFile = 'tests/.auth/login.json'; 4 | 5 | export default defineConfig({ 6 | testDir: './tests', 7 | fullyParallel: true, 8 | forbidOnly: !!process.env.CI, 9 | retries: process.env.CI ? 2 : 0, 10 | workers: process.env.CI ? 1 : undefined, 11 | reporter: process.env.CI ? [['html', { outputFolder: 'tests/.report' }], ['github']] : [['line'], ['html', { open: 'never', outputFolder: 'tests/.report' }]], 12 | use: { 13 | baseURL: 'http://localhost:3000', 14 | trace: 'on-first-retry', 15 | video: 'retain-on-failure' 16 | }, 17 | projects: [ 18 | { name: 'setup', testMatch: /.*\.setup\.ts/ }, 19 | { 20 | name: 'chromium', 21 | use: { ...devices['Desktop Chrome'], storageState: authFile }, 22 | dependencies: ['setup'] 23 | } 24 | ], 25 | webServer: { 26 | command: 'APP_ENV=TEST npm run dev', 27 | url: 'http://localhost:3000', 28 | reuseExistingServer: !process.env.CI, 29 | timeout: 120 * 1000, 30 | env: { 31 | APP_ENV: 'TEST' 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/setup-test-images.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import util from 'util'; 3 | 4 | const execPromise = util.promisify(exec); 5 | 6 | async function pullTestImages() { 7 | console.log('Pulling test images for e2e tests...'); 8 | 9 | try { 10 | // Pull a small test image that won't consume much space 11 | await execPromise('docker pull nginx:latest'); 12 | console.log('Successfully pulled nginx:latest'); 13 | 14 | // Optional: Pull another image for multi-image tests 15 | await execPromise('docker pull busybox:latest'); 16 | console.log('Successfully pulled busybox:latest'); 17 | 18 | console.log('Test images ready'); 19 | } catch (error) { 20 | console.error('Failed to pull test images:', error); 21 | process.exit(1); 22 | } 23 | } 24 | 25 | pullTestImages(); 26 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | import type { VolumeInspectInfo as OriginalVolumeInspectInfo } from 'dockerode'; 2 | import type { User } from '$lib/types/user.type'; 3 | 4 | declare global { 5 | namespace App { 6 | interface Error { 7 | message: string; 8 | status?: number; 9 | } 10 | interface Locals { 11 | user?: User | null; 12 | session: Session; 13 | } 14 | // interface PageData {} 15 | // interface PageState {} 16 | // interface Platform {} 17 | } 18 | } 19 | 20 | declare module 'dockerode' { 21 | // Re-declare the interface adding the missing property 22 | interface VolumeInspectInfo extends OriginalVolumeInspectInfo { 23 | CreatedAt: string; 24 | } 25 | } 26 | 27 | export {}; 28 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/badges/unused-text-badge.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#if link} 15 | 16 | 17 | {name} 18 | 19 | 20 | {:else} 21 | {name} 22 | {/if} 23 | {#if !inUse} 24 | 25 | {/if} 26 |
27 | -------------------------------------------------------------------------------- /src/lib/components/confirm-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import ConfirmDialog from './confirm-dialog.svelte'; 3 | 4 | interface ConfirmDialogStore { 5 | open: boolean; 6 | title: string; 7 | message: string; 8 | confirm: { 9 | label?: string; 10 | destructive?: boolean; 11 | action: (checkboxStates: Record) => void; 12 | }; 13 | checkboxes?: Array<{ 14 | id: string; 15 | label: string; 16 | initialState?: boolean; 17 | }>; 18 | } 19 | 20 | export const confirmDialogStore = writable({ 21 | open: false, 22 | title: '', 23 | message: '', 24 | confirm: { 25 | label: 'Confirm', 26 | destructive: false, 27 | action: () => {} 28 | } 29 | }); 30 | 31 | function openConfirmDialog({ 32 | title, 33 | message, 34 | confirm, 35 | checkboxes 36 | }: { 37 | title: string; 38 | message: string; 39 | confirm: { 40 | label?: string; 41 | destructive?: boolean; 42 | action: (checkboxStates: Record) => void; 43 | }; 44 | checkboxes?: Array<{ 45 | id: string; 46 | label: string; 47 | initialState?: boolean; 48 | }>; 49 | }) { 50 | confirmDialogStore.update((val) => ({ 51 | open: true, 52 | title, 53 | message, 54 | confirm: { 55 | ...val.confirm, 56 | ...confirm 57 | }, 58 | checkboxes 59 | })); 60 | } 61 | 62 | export { ConfirmDialog, openConfirmDialog }; 63 | -------------------------------------------------------------------------------- /src/lib/components/env-editor.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if browser} 12 |
13 | 37 |
38 | {/if} 39 | -------------------------------------------------------------------------------- /src/lib/components/stat-card.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 |
22 |

{title}

23 |

{value}

24 | {#if subtitle} 25 |

{subtitle}

26 | {/if} 27 |
28 |
29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-content.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |
10 | {@render children?.()} 11 |
12 |
13 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/accordion-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | svg]:rotate-180', className)} {...restProps}> 19 | {@render children?.()} 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/accordion/index.ts: -------------------------------------------------------------------------------- 1 | import { Accordion as AccordionPrimitive } from 'bits-ui'; 2 | import Content from './accordion-content.svelte'; 3 | import Item from './accordion-item.svelte'; 4 | import Trigger from './accordion-trigger.svelte'; 5 | const Root = AccordionPrimitive.Root; 6 | 7 | export { 8 | Root, 9 | Content, 10 | Item, 11 | Trigger, 12 | // 13 | Root as Accordion, 14 | Content as AccordionContent, 15 | Item as AccordionItem, 16 | Trigger as AccordionTrigger 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-description.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {@render children?.()} 11 |
12 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert-title.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | {@render children?.()} 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/alert.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/lib/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './alert.svelte'; 2 | import Description from './alert-description.svelte'; 3 | import Title from './alert-title.svelte'; 4 | export { alertVariants, type AlertVariant } from './alert.svelte'; 5 | 6 | export { 7 | Root, 8 | Description, 9 | Title, 10 | // 11 | Root as Alert, 12 | Description as AlertDescription, 13 | Title as AlertTitle 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | 40 | {@render children?.()} 41 | 42 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Badge } from './badge.svelte'; 2 | export { badgeVariants, type BadgeVariant } from './badge.svelte'; 3 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-item.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
  • 10 | {@render children?.()} 11 |
  • 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-link.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | {#if child} 26 | {@render child({ props: attrs })} 27 | {:else} 28 | 29 | {@render children?.()} 30 | 31 | {/if} 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-list.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
      10 | {@render children?.()} 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/breadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './breadcrumb.svelte'; 2 | import Ellipsis from './breadcrumb-ellipsis.svelte'; 3 | import Item from './breadcrumb-item.svelte'; 4 | import Separator from './breadcrumb-separator.svelte'; 5 | import Link from './breadcrumb-link.svelte'; 6 | import List from './breadcrumb-list.svelte'; 7 | import Page from './breadcrumb-page.svelte'; 8 | 9 | export { 10 | Root, 11 | Ellipsis, 12 | Item, 13 | Separator, 14 | Link, 15 | List, 16 | Page, 17 | // 18 | Root as Breadcrumb, 19 | Ellipsis as BreadcrumbEllipsis, 20 | Item as BreadcrumbItem, 21 | Separator as BreadcrumbSeparator, 22 | Link as BreadcrumbLink, 23 | List as BreadcrumbList, 24 | Page as BreadcrumbPage 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants } from './button.svelte'; 2 | 3 | export { 4 | Root, 5 | type ButtonProps as Props, 6 | // 7 | Root as Button, 8 | buttonVariants, 9 | type ButtonProps, 10 | type ButtonSize, 11 | type ButtonVariant 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | {@render children?.()} 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |

    10 | {@render children?.()} 11 |

    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | {@render children?.()} 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | {@render children?.()} 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
    18 | {@render children?.()} 19 |
    20 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | {@render children?.()} 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './card.svelte'; 2 | import Content from './card-content.svelte'; 3 | import Description from './card-description.svelte'; 4 | import Footer from './card-footer.svelte'; 5 | import Header from './card-header.svelte'; 6 | import Title from './card-title.svelte'; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | {#snippet children({ checked, indeterminate })} 21 |
    22 | {#if indeterminate} 23 | 24 | {:else} 25 | 26 | {/if} 27 |
    28 | {/snippet} 29 |
    30 | -------------------------------------------------------------------------------- /src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './checkbox.svelte'; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/data-table/flex-render.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | {#if typeof content === 'string'} 22 | {content} 23 | {:else if content instanceof Function} 24 | 25 | 26 | {@const result = content(context as any)} 27 | {#if result instanceof RenderComponentConfig} 28 | {@const { component: Component, props } = result} 29 | 30 | {:else if result instanceof RenderSnippetConfig} 31 | {@const { snippet, params } = result} 32 | {@render snippet(params)} 33 | {:else} 34 | {result} 35 | {/if} 36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/data-table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FlexRender } from './flex-render.svelte'; 2 | export { renderComponent, renderSnippet } from './render-helpers.js'; 3 | export { createSvelteTable } from './data-table.svelte.js'; 4 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-close.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 28 | {@render children?.()} 29 | 30 | 31 | Close 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | {@render children?.()} 10 |
    11 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | {@render children?.()} 10 |
    11 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-trigger.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from 'bits-ui'; 2 | 3 | import Title from './dialog-title.svelte'; 4 | import Footer from './dialog-footer.svelte'; 5 | import Header from './dialog-header.svelte'; 6 | import Overlay from './dialog-overlay.svelte'; 7 | import Content from './dialog-content.svelte'; 8 | import Description from './dialog-description.svelte'; 9 | import Trigger from './dialog-trigger.svelte'; 10 | import Close from './dialog-close.svelte'; 11 | 12 | const Root = DialogPrimitive.Root; 13 | const Portal = DialogPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | {#snippet children({ checked, indeterminate })} 22 | 23 | {#if indeterminate} 24 | 25 | {:else} 26 | 27 | {/if} 28 | 29 | {@render childrenProp?.()} 30 | {/snippet} 31 | 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
    18 | {@render children?.()} 19 |
    20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {#snippet children({ checked })} 11 | 12 | {#if checked} 13 | 14 | {/if} 15 | 16 | {@render childrenProp?.({ checked })} 17 | {/snippet} 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {@render children?.()} 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-button.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-description.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-element-field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 17 | {#snippet children({ constraints, errors, tainted, value })} 18 |
    19 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 20 |
    21 | {/snippet} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-field-errors.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {#snippet children({ errors, errorProps })} 19 | {#if childrenProp} 20 | {@render childrenProp({ errors, errorProps })} 21 | {:else} 22 | {#each errors as error (error)} 23 |
    {error}
    24 | {/each} 25 | {/if} 26 | {/snippet} 27 |
    28 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 17 | {#snippet children({ constraints, errors, tainted, value })} 18 |
    19 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 20 |
    21 | {/snippet} 22 |
    23 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-fieldset.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-label.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {#snippet child({ props })} 12 | 15 | {/snippet} 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/form-legend.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | import * as FormPrimitive from 'formsnap'; 2 | import Description from './form-description.svelte'; 3 | import Label from './form-label.svelte'; 4 | import FieldErrors from './form-field-errors.svelte'; 5 | import Field from './form-field.svelte'; 6 | import Fieldset from './form-fieldset.svelte'; 7 | import Legend from './form-legend.svelte'; 8 | import ElementField from './form-element-field.svelte'; 9 | import Button from './form-button.svelte'; 10 | 11 | const Control = FormPrimitive.Control; 12 | 13 | export { 14 | Field, 15 | Control, 16 | Label, 17 | Button, 18 | FieldErrors, 19 | Description, 20 | Fieldset, 21 | Legend, 22 | ElementField, 23 | // 24 | Field as FormField, 25 | Control as FormControl, 26 | Description as FormDescription, 27 | Label as FormLabel, 28 | FieldErrors as FormFieldErrors, 29 | Fieldset as FormFieldset, 30 | Legend as FormLegend, 31 | ElementField as FormElementField, 32 | Button as FormButton 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './input.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Input 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if type === 'file'} 14 | 22 | {:else} 23 | 31 | {/if} 32 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './label.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './pagination.svelte'; 2 | import Content from './pagination-content.svelte'; 3 | import Item from './pagination-item.svelte'; 4 | import Link from './pagination-link.svelte'; 5 | import PrevButton from './pagination-prev-button.svelte'; 6 | import NextButton from './pagination-next-button.svelte'; 7 | import Ellipsis from './pagination-ellipsis.svelte'; 8 | 9 | export { 10 | Root, 11 | Content, 12 | Item, 13 | Link, 14 | PrevButton, 15 | NextButton, 16 | Ellipsis, 17 | // 18 | Root as Pagination, 19 | Content as PaginationContent, 20 | Item as PaginationItem, 21 | Link as PaginationLink, 22 | PrevButton as PaginationPrevButton, 23 | NextButton as PaginationNextButton, 24 | Ellipsis as PaginationEllipsis 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-content.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
      10 | {@render children?.()} 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-ellipsis.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
  • 9 | {@render children?.()} 10 |
  • 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-link.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | {#snippet Fallback()} 21 | {page.value} 22 | {/snippet} 23 | 24 | 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-next-button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#snippet Fallback()} 11 | 12 | {/snippet} 13 | 14 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#snippet Fallback()} 11 | 12 | {/snippet} 13 | 14 | 26 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './progress.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Progress 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/progress.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /src/lib/components/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './radio-group.svelte'; 2 | import Item from './radio-group-item.svelte'; 3 | 4 | export { 5 | Root, 6 | Item, 7 | // 8 | Root as RadioGroup, 9 | Item as RadioGroupItem 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/radio-group/radio-group-item.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {#snippet children({ checked })} 11 |
    12 | {#if checked} 13 | 14 | {/if} 15 |
    16 | {/snippet} 17 |
    18 | -------------------------------------------------------------------------------- /src/lib/components/ui/radio-group/radio-group.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/resizable/index.ts: -------------------------------------------------------------------------------- 1 | import { Pane } from 'paneforge'; 2 | import Handle from './resizable-handle.svelte'; 3 | import PaneGroup from './resizable-pane-group.svelte'; 4 | 5 | export { 6 | PaneGroup, 7 | Pane, 8 | Handle, 9 | // 10 | PaneGroup as ResizablePaneGroup, 11 | Pane as ResizablePane, 12 | Handle as ResizableHandle 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/resizable/resizable-handle.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | div]:rotate-90', 21 | className 22 | )} 23 | {...restProps} 24 | > 25 | {#if withHandle} 26 |
    27 | 28 |
    29 | {/if} 30 |
    31 | -------------------------------------------------------------------------------- /src/lib/components/ui/resizable/resizable-pane-group.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from 'bits-ui'; 2 | 3 | import GroupHeading from './select-group-heading.svelte'; 4 | import Item from './select-item.svelte'; 5 | import Content from './select-content.svelte'; 6 | import Trigger from './select-trigger.svelte'; 7 | import Separator from './select-separator.svelte'; 8 | import ScrollDownButton from './select-scroll-down-button.svelte'; 9 | import ScrollUpButton from './select-scroll-up-button.svelte'; 10 | 11 | const Root = SelectPrimitive.Root; 12 | const Group = SelectPrimitive.Group; 13 | 14 | export { 15 | Root, 16 | Group, 17 | GroupHeading, 18 | Item, 19 | Content, 20 | Trigger, 21 | Separator, 22 | ScrollDownButton, 23 | ScrollUpButton, 24 | // 25 | Root as Select, 26 | Group as SelectGroup, 27 | GroupHeading as SelectGroupHeading, 28 | Item as SelectItem, 29 | Content as SelectContent, 30 | Trigger as SelectTrigger, 31 | Separator as SelectSeparator, 32 | ScrollDownButton as SelectScrollDownButton, 33 | ScrollUpButton as SelectScrollUpButton 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 29 | 30 | 31 | {@render children?.()} 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {#snippet children({ selected, highlighted })} 11 | 12 | {#if selected} 13 | 14 | {/if} 15 | 16 | {#if childrenProp} 17 | {@render childrenProp({ selected, highlighted })} 18 | {:else} 19 | {label || value} 20 | {/if} 21 | {/snippet} 22 | 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-scroll-down-button.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-scroll-up-button.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | span]:line-clamp-1', className)} {...restProps}> 10 | {@render children?.()} 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './separator.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './sonner.svelte'; 2 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './switch.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Switch 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/switch/switch.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './table.svelte'; 2 | import Body from './table-body.svelte'; 3 | import Caption from './table-caption.svelte'; 4 | import Cell from './table-cell.svelte'; 5 | import Footer from './table-footer.svelte'; 6 | import Head from './table-head.svelte'; 7 | import Header from './table-header.svelte'; 8 | import Row from './table-row.svelte'; 9 | 10 | export { 11 | Root, 12 | Body, 13 | Caption, 14 | Cell, 15 | Footer, 16 | Head, 17 | Header, 18 | Row, 19 | // 20 | Root as Table, 21 | Body as TableBody, 22 | Caption as TableCaption, 23 | Cell as TableCell, 24 | Footer as TableFooter, 25 | Head as TableHead, 26 | Header as TableHeader, 27 | Row as TableRow 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-body.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-caption.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-cell.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-footer.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-head.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-header.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-row.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {@render children?.()} 11 | 12 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | 11 | {@render children?.()} 12 |
    13 |
    14 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import { Tabs as TabsPrimitive } from 'bits-ui'; 2 | import Content from './tabs-content.svelte'; 3 | import List from './tabs-list.svelte'; 4 | import Trigger from './tabs-trigger.svelte'; 5 | 6 | const Root = TabsPrimitive.Root; 7 | 8 | export { 9 | Root, 10 | Content, 11 | List, 12 | Trigger, 13 | // 14 | Root as Tabs, 15 | Content as TabsContent, 16 | List as TabsList, 17 | Trigger as TabsTrigger 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-content.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-list.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/components/ui/tabs/tabs-trigger.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/textarea/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './textarea.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Textarea 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/textarea/textarea.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip as TooltipPrimitive } from 'bits-ui'; 2 | import Content from './tooltip-content.svelte'; 3 | 4 | const Root = TooltipPrimitive.Root; 5 | const Trigger = TooltipPrimitive.Trigger; 6 | const Provider = TooltipPrimitive.Provider; 7 | 8 | export { 9 | Root, 10 | Trigger, 11 | Content, 12 | Provider, 13 | // 14 | Root as Tooltip, 15 | Content as TooltipContent, 16 | Trigger as TooltipTrigger, 17 | Provider as TooltipProvider 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/tooltip/tooltip-content.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | 3 | export const isDev = dev; 4 | 5 | export const isTest = false; 6 | 7 | export const defaultEnvTemplate = `# Environment Variables 8 | # These variables will be available to your stack services 9 | # Format: VARIABLE_NAME=value 10 | 11 | NGINX_HOST=localhost 12 | NGINX_PORT=80 13 | 14 | # Example Database Configuration 15 | # DB_USER=myuser 16 | # DB_PASSWORD=mypassword 17 | # DB_NAME=mydatabase 18 | `; 19 | 20 | export const defaultComposeTemplate = `services: 21 | nginx: 22 | image: nginx:alpine 23 | container_name: nginx_service 24 | env_file: 25 | - .env 26 | ports: 27 | - "8080:80" 28 | volumes: 29 | - nginx_data:/usr/share/nginx/html 30 | restart: unless-stopped 31 | 32 | volumes: 33 | nginx_data: 34 | driver: local 35 | `; 36 | 37 | /* The line `const DEFAULT_NETWORK_NAMES = new Set(['host', 'bridge', 'none', 'ingress']);` is creating 38 | a Set named `DEFAULT_NETWORK_NAMES` that contains the default network names managed by Docker. These 39 | default network names are 'host', 'bridge', 'none', and 'ingress'. The purpose of this set is to 40 | provide a quick and efficient way to check if a given network name is one of the default networks 41 | when needed in the code. */ 42 | export const DEFAULT_NETWORK_NAMES = new Set(['host', 'bridge', 'none', 'ingress']); 43 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /src/lib/services/api/api-service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | abstract class BaseAPIService { 4 | api = axios.create({ 5 | withCredentials: true 6 | }); 7 | 8 | constructor() { 9 | this.api.defaults.baseURL = '/api'; 10 | } 11 | } 12 | 13 | export default BaseAPIService; 14 | -------------------------------------------------------------------------------- /src/lib/services/api/container-api-service.ts: -------------------------------------------------------------------------------- 1 | import BaseAPIService from './api-service'; 2 | import type Docker from 'dockerode'; 3 | 4 | export default class ContainerAPIService extends BaseAPIService { 5 | async start(id: string) { 6 | const res = await this.api.post(`/containers/${id}/start`); 7 | return res.data; 8 | } 9 | 10 | async stop(id: string) { 11 | const res = await this.api.post(`/containers/${id}/stop`); 12 | return res.data; 13 | } 14 | 15 | async restart(id: string) { 16 | const res = await this.api.post(`/containers/${id}/restart`); 17 | return res.data; 18 | } 19 | 20 | async remove(id: string) { 21 | const res = await this.api.delete(`/containers/${id}/remove`); 22 | return res.data; 23 | } 24 | 25 | async pull(id: string) { 26 | const res = await this.api.post(`/containers/${id}/pull`); 27 | return res.data; 28 | } 29 | 30 | async startAll() { 31 | const res = await this.api.post(`/containers/start-all`); 32 | return res.data; 33 | } 34 | 35 | async stopAll() { 36 | const res = await this.api.post(`/containers/stop-all`); 37 | return res.data; 38 | } 39 | 40 | async create(config: Docker.ContainerCreateOptions) { 41 | const res = await this.api.post('/containers', config); 42 | return res.data; 43 | } 44 | 45 | async list() { 46 | const res = await this.api.get(''); 47 | return res.data; 48 | } 49 | 50 | async get(id: string) { 51 | const res = await this.api.get(`/containers/${id}`); 52 | return res.data; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/services/api/image-api-service.ts: -------------------------------------------------------------------------------- 1 | import BaseAPIService from './api-service'; 2 | 3 | export default class ImageAPIService extends BaseAPIService { 4 | async remove(id: string) { 5 | const res = await this.api.delete(`/images/${id}`); 6 | return res.data; 7 | } 8 | 9 | async pull(imageRef: string, tag: string) { 10 | const encodedImageRef = encodeURIComponent(imageRef); 11 | const res = await this.api.post(`/images/pull/${encodedImageRef}`, { tag }); 12 | return res.data; 13 | } 14 | 15 | async prune() { 16 | const res = await this.api.post(`/images/prune`); 17 | return res.data; 18 | } 19 | 20 | async checkMaturity(id: string) { 21 | // Change from POST to GET 22 | const res = await this.api.get(`/images/${id}/maturity`); 23 | return res.data; 24 | } 25 | 26 | async checkMaturityBatch(imageIds: string[]) { 27 | // Use the first image ID for the endpoint URL 28 | // This is a bit unusual, but following your requirement 29 | if (!imageIds || imageIds.length === 0) { 30 | throw new Error('No image IDs provided for batch check'); 31 | } 32 | 33 | const firstId = imageIds[0]; 34 | const res = await this.api.post(`/images/${firstId}/maturity`, { imageIds }); 35 | return res.data; 36 | } 37 | 38 | async list() { 39 | const res = await this.api.get(''); 40 | return res.data; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/services/api/network-api-service.ts: -------------------------------------------------------------------------------- 1 | import type { NetworkCreateOptions } from 'dockerode'; 2 | import BaseAPIService from './api-service'; 3 | 4 | export default class NetworkAPIService extends BaseAPIService { 5 | async remove(id: string) { 6 | const res = await this.api.delete(`/networks/${id}`); 7 | return res.data; 8 | } 9 | 10 | async create(options: NetworkCreateOptions) { 11 | const res = await this.api.post(`/networks/create`, options); 12 | return res.data; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/services/api/system-api-service.ts: -------------------------------------------------------------------------------- 1 | import type { PruneType } from '$lib/types/actions.type'; 2 | import BaseAPIService from './api-service'; 3 | 4 | export default class SystemAPIService extends BaseAPIService { 5 | async prune(types: PruneType[]) { 6 | if (!types || types.length === 0) { 7 | throw new Error('No prune types specified'); 8 | } 9 | 10 | const typesParam = types.join(','); 11 | const res = await this.api.post(`/system/prune?types=${typesParam}`); 12 | return res.data; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/services/api/user-api-service.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '$lib/types/user.type'; 2 | import BaseAPIService from './api-service'; 3 | 4 | export default class UserAPIService extends BaseAPIService { 5 | async update(id: string, user: User) { 6 | const res = await this.api.put(`/users/${id}`, user); 7 | return res.data; 8 | } 9 | async create(user: User) { 10 | const res = await this.api.post(`/users`, user); 11 | return res.data; 12 | } 13 | async delete(id: string) { 14 | const res = await this.api.delete(`/users/${id}`); 15 | return res.data; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/services/api/volume-api-service.ts: -------------------------------------------------------------------------------- 1 | import type { VolumeCreateOptions } from 'dockerode'; 2 | import BaseAPIService from './api-service'; 3 | 4 | export default class VolumeAPIService extends BaseAPIService { 5 | async remove(id: string) { 6 | const res = await this.api.delete(`/volumes/${id}`); 7 | return res.data; 8 | } 9 | 10 | async create(volume: VolumeCreateOptions) { 11 | const res = await this.api.post('/volumes', volume); 12 | return res.data; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/services/paths-service.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import fs from 'node:fs/promises'; 3 | 4 | export const BASE_PATH = 'data'; 5 | 6 | export const SETTINGS_DIR = path.join(BASE_PATH, 'settings'); 7 | export const KEY_FILE = path.join(BASE_PATH, '.secret_key'); 8 | export const SESSIONS_DIR = path.join(BASE_PATH, 'sessions'); 9 | export const USER_DIR = path.join(BASE_PATH, 'users'); 10 | 11 | export const STACKS_DIR = path.join(BASE_PATH, 'stacks'); 12 | 13 | export async function ensureDirectory(dir: string, mode = 0o755): Promise { 14 | try { 15 | await fs.mkdir(dir, { recursive: true, mode }); 16 | } catch (error) { 17 | console.error(`Failed to ensure directory ${dir}:`, error); 18 | throw error; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/services/session-handler.ts: -------------------------------------------------------------------------------- 1 | import { handleSession } from 'svelte-kit-cookie-session'; 2 | import { getSettings } from './settings-service'; 3 | import { createHash } from 'node:crypto'; 4 | import { env } from '$env/dynamic/public'; 5 | import { dev } from '$app/environment'; 6 | 7 | const settings = await getSettings(); 8 | const sessionTimeout = settings.auth?.sessionTimeout || 1440; 9 | 10 | function createSecret(input: string): Uint8Array { 11 | const hash = createHash('sha256').update(input).digest(); 12 | return new Uint8Array(hash); 13 | } 14 | 15 | const secretInput = env.PUBLIC_SESSION_SECRET; 16 | 17 | if (!secretInput) { 18 | throw new Error('PUBLIC_SESSION_SECRET is missing in ENV.'); 19 | } 20 | 21 | const secret = createSecret(secretInput); 22 | 23 | const useSecureCookie = !(env.PUBLIC_ALLOW_INSECURE_COOKIES === 'true' || dev); 24 | 25 | export const sessionHandler = handleSession({ 26 | secret, 27 | expires: sessionTimeout * 60, 28 | cookie: { 29 | path: '/', 30 | httpOnly: true, 31 | sameSite: 'lax', 32 | secure: useSecureCookie 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/stores/maturity-store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { ImageMaturity } from '$lib/types/docker/image.type'; 3 | 4 | export type MaturityState = { 5 | lastChecked: Date | null; 6 | maturityData: Record; 7 | isChecking: boolean; 8 | }; 9 | 10 | const initialState: MaturityState = { 11 | lastChecked: null, 12 | maturityData: {}, 13 | isChecking: false 14 | }; 15 | 16 | export const maturityStore = writable(initialState); 17 | 18 | // Function to update maturity for a specific image 19 | export function updateImageMaturity(imageId: string, maturity: ImageMaturity | undefined): void { 20 | maturityStore.update((state) => { 21 | const newData = { ...state.maturityData }; 22 | 23 | if (maturity) { 24 | newData[imageId] = maturity; 25 | } else { 26 | delete newData[imageId]; 27 | } 28 | 29 | return { 30 | ...state, 31 | maturityData: newData 32 | }; 33 | }); 34 | } 35 | 36 | // Update the checking status 37 | export function setMaturityChecking(isChecking: boolean): void { 38 | maturityStore.update((state) => ({ 39 | ...state, 40 | isChecking, 41 | lastChecked: isChecking ? state.lastChecked : new Date() 42 | })); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/stores/table-store.ts: -------------------------------------------------------------------------------- 1 | import { PersistedState } from 'runed'; 2 | 3 | export interface PageSizes { 4 | default: number; 5 | containers: number; 6 | images: number; 7 | volumes: number; 8 | networks: number; 9 | stacks: number; 10 | [key: string]: number; // Allow additional dynamic keys 11 | } 12 | 13 | // Create default values 14 | const DEFAULT_PAGE_SIZES: PageSizes = { 15 | default: 10, 16 | containers: 10, 17 | images: 10, 18 | volumes: 10, 19 | networks: 10, 20 | stacks: 10 21 | }; 22 | 23 | // Single persisted state for all page sizes 24 | export const tablePageSizes = new PersistedState('arcane-table-page-sizes', DEFAULT_PAGE_SIZES, { 25 | storage: 'local', 26 | syncTabs: true 27 | }); 28 | 29 | // Backward compatibility helpers 30 | export const tablePersistence = { 31 | // Get a page size value for a specific table 32 | getPageSize: (tableKey: string): number => { 33 | return tablePageSizes.current[tableKey] || 10; 34 | }, 35 | 36 | // Set a page size value for a specific table 37 | setPageSize: (tableKey: string, value: number): void => { 38 | tablePageSizes.current = { 39 | ...tablePageSizes.current, 40 | [tableKey]: value 41 | }; 42 | } 43 | }; 44 | 45 | // Usage examples: 46 | // Access directly: 47 | // const containerPageSize = tablePageSizes.current.containers; 48 | // 49 | // Update directly: 50 | // tablePageSizes.current = { ...tablePageSizes.current, containers: 20 }; 51 | // 52 | // Or use the compatibility helpers: 53 | // tablePersistence.setPageSize('images', 20); 54 | -------------------------------------------------------------------------------- /src/lib/types/actions.type.ts: -------------------------------------------------------------------------------- 1 | export type StackActions = 'start' | 'stop' | 'restart' | 'redeploy' | 'import' | 'destroy' | 'pull' | 'migrate'; 2 | export type ContainerActions = 'start' | 'stop' | 'restart' | 'pull' | 'remove'; 3 | export type PruneType = 'containers' | 'images' | 'networks' | 'volumes'; 4 | -------------------------------------------------------------------------------- /src/lib/types/application-configuration.ts: -------------------------------------------------------------------------------- 1 | export interface AppVersionInformation { 2 | currentVersion: string; 3 | newestVersion?: string; 4 | updateAvailable?: boolean; 5 | releaseUrl?: string; 6 | releaseNotes?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/types/docker/connection.type.ts: -------------------------------------------------------------------------------- 1 | export interface DockerConnectionOptions { 2 | socketPath?: string; 3 | host?: string; 4 | port?: number; 5 | ca?: string; 6 | cert?: string; 7 | key?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/types/docker/image.type.ts: -------------------------------------------------------------------------------- 1 | import type Docker from 'dockerode'; 2 | 3 | /** 4 | * Represents a Docker image, extending Dockerode's ImageInfo with parsed repo and tag. 5 | * Properties like Id, RepoTags, Created, Size, etc., are inherited from Docker.ImageInfo. 6 | */ 7 | export type ServiceImage = Docker.ImageInfo & { 8 | repo: string; // Parsed repository name 9 | tag: string; // Parsed tag 10 | }; 11 | 12 | /** 13 | * Represents the maturity status of a Docker image. 14 | */ 15 | export interface ImageMaturity { 16 | version: string; 17 | date: string; 18 | status: 'Matured' | 'Not Matured' | 'Unknown'; 19 | updatesAvailable: boolean; 20 | } 21 | 22 | /** 23 | * Extends ServiceImage with application-specific information like usage status and maturity. 24 | */ 25 | export type EnhancedImageInfo = ServiceImage & { 26 | inUse: boolean; 27 | maturity?: ImageMaturity; 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/types/docker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './image.type'; 2 | export * from './connection.type'; 3 | -------------------------------------------------------------------------------- /src/lib/types/docker/prune.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for Docker prune operation results 3 | */ 4 | export interface PruneResult { 5 | ContainersDeleted?: string[]; 6 | ImagesDeleted?: any[]; 7 | NetworksDeleted?: string[]; 8 | VolumesDeleted?: string[]; 9 | SpaceReclaimed?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/types/docker/stack.type.ts: -------------------------------------------------------------------------------- 1 | export interface StackMeta { 2 | id: string; 3 | name: string; 4 | createdAt: string; 5 | updatedAt: string; 6 | autoUpdate?: boolean; 7 | dirName?: string; 8 | path: string; 9 | } 10 | 11 | export interface StackPort { 12 | PublicPort?: number; 13 | PrivatePort?: number; 14 | Type?: string; 15 | [key: string]: any; 16 | } 17 | 18 | export interface StackService { 19 | id: string; 20 | name: string; 21 | state?: { 22 | Running: boolean; 23 | Status: string; 24 | ExitCode: number; 25 | }; 26 | ports?: StackPort[]; 27 | networkSettings?: { 28 | Networks?: Record< 29 | string, 30 | { 31 | IPAddress?: string; 32 | Gateway?: string; 33 | MacAddress?: string; 34 | Driver?: string; 35 | [key: string]: any; 36 | } 37 | >; 38 | }; 39 | } 40 | 41 | export interface Stack { 42 | id: string; 43 | name: string; 44 | services?: StackService[]; 45 | serviceCount?: number; 46 | runningCount?: number; 47 | status: 'running' | 'stopped' | 'partially running' | 'unknown'; 48 | isExternal?: boolean; 49 | createdAt?: string; 50 | updatedAt?: string; 51 | composeContent?: string; 52 | envContent?: string; 53 | isLegacy?: boolean; 54 | } 55 | 56 | export interface StackUpdate { 57 | name?: string; 58 | composeContent?: string; 59 | envContent?: string; 60 | autoUpdate?: boolean; 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/types/form.type.ts: -------------------------------------------------------------------------------- 1 | export type FormInput = { 2 | value: T; 3 | valid: boolean; 4 | touched: boolean; 5 | error: string | null; 6 | errors: string[]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/types/loading-states.type.ts: -------------------------------------------------------------------------------- 1 | export type LoadingStates = { 2 | start?: boolean; 3 | stop?: boolean; 4 | restart?: boolean; 5 | pull?: boolean; 6 | deploy?: boolean; 7 | redeploy?: boolean; 8 | remove?: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/types/session.type.ts: -------------------------------------------------------------------------------- 1 | export type UserSession = { 2 | userId: string; 3 | username: string; 4 | createdAt: number; 5 | lastAccessed: number; 6 | expires?: number; 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/types/settings.type.ts: -------------------------------------------------------------------------------- 1 | export interface AuthSettings { 2 | localAuthEnabled: boolean; 3 | oidcEnabled: boolean; 4 | sessionTimeout: number; 5 | passwordPolicy: 'basic' | 'standard' | 'strong'; 6 | rbacEnabled: boolean; 7 | oidc?: OidcConfig; 8 | } 9 | 10 | export interface RegistryCredential { 11 | url: string; 12 | username: string; 13 | password: string; 14 | } 15 | 16 | export interface OidcConfig { 17 | clientId: string; 18 | clientSecret: string; 19 | redirectUri: string; 20 | authorizationEndpoint: string; 21 | tokenEndpoint: string; 22 | userinfoEndpoint: string; 23 | scopes: string; 24 | } 25 | 26 | export interface Onboarding { 27 | completed: boolean; 28 | completedAt?: string; 29 | steps?: { 30 | welcome?: boolean; 31 | password?: boolean; 32 | settings?: boolean; 33 | }; 34 | } 35 | 36 | export interface TemplateRegistryConfig { 37 | url: string; 38 | name: string; 39 | enabled: boolean; 40 | last_updated?: string; 41 | cache_ttl?: number; 42 | } 43 | 44 | export interface Settings { 45 | dockerHost: string; 46 | stacksDirectory: string; 47 | autoUpdate: boolean; 48 | autoUpdateInterval: number; 49 | pollingEnabled: boolean; 50 | pollingInterval: number; 51 | pruneMode: 'all' | 'dangling' | undefined; 52 | registryCredentials: RegistryCredential[]; 53 | templateRegistries: TemplateRegistryConfig[]; // Add this new field 54 | auth: AuthSettings; 55 | onboarding?: Onboarding; 56 | baseServerUrl?: string; 57 | maturityThresholdDays: number; 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/types/statuses.ts: -------------------------------------------------------------------------------- 1 | export const statusVariantMap: Record = { 2 | running: 'green', 3 | deployed: 'green', 4 | stopped: 'red', 5 | failed: 'red', 6 | pending: 'amber', 7 | creating: 'blue', 8 | updating: 'blue', 9 | deleting: 'purple' 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/types/table-types.ts: -------------------------------------------------------------------------------- 1 | import { type ColumnDef } from '@tanstack/table-core'; 2 | 3 | export type FeatureFlags = { 4 | sorting?: boolean; 5 | filtering?: boolean; 6 | selection?: boolean; 7 | }; 8 | 9 | export type PaginationOptions = { 10 | pageSize?: number; 11 | pageSizeOptions?: number[]; 12 | itemsPerPageLabel?: string; 13 | }; 14 | 15 | export type DisplayOptions = { 16 | filterPlaceholder?: string; 17 | noResultsMessage?: string; 18 | isDashboardTable?: boolean; 19 | class?: string; 20 | }; 21 | 22 | export type SortOptions = { 23 | defaultSort?: { id: string; desc: boolean }; 24 | }; 25 | 26 | export type UniversalTableProps = { 27 | columns: ColumnDef[]; 28 | data: TData[]; 29 | features?: FeatureFlags; 30 | display?: DisplayOptions; 31 | pagination?: PaginationOptions; 32 | sort?: SortOptions; 33 | selectedIds?: string[]; 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/types/template-registry.ts: -------------------------------------------------------------------------------- 1 | export interface TemplateRegistry { 2 | name: string; 3 | description: string; 4 | version: string; 5 | templates: RemoteTemplate[]; 6 | } 7 | 8 | export interface RemoteTemplate { 9 | id: string; 10 | name: string; 11 | description: string; 12 | version: string; 13 | author?: string; 14 | tags?: string[]; 15 | compose_url: string; 16 | env_url?: string; 17 | documentation_url?: string; 18 | icon_url?: string; 19 | updated_at: string; 20 | } 21 | 22 | export interface TemplateRegistryConfig { 23 | url: string; 24 | name: string; 25 | enabled: boolean; 26 | last_updated?: string; 27 | cache_ttl?: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/types/user.type.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | username: string; 4 | passwordHash?: string; 5 | displayName?: string; 6 | email?: string; 7 | roles: string[]; 8 | createdAt: string; 9 | lastLogin?: string; 10 | requirePasswordChange?: boolean; 11 | updatedAt?: string; 12 | oidcSubjectId?: string; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export type WithoutChild = T extends { child?: any } ? Omit : T; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export type WithoutChildren = T extends { children?: any } ? Omit : T; 12 | export type WithoutChildrenOrChild = WithoutChildren>; 13 | export type WithElementRef = T & { ref?: U | null }; 14 | 15 | export function debounced void>(func: T, delay: number) { 16 | let debounceTimeout: ReturnType; 17 | 18 | return (...args: Parameters) => { 19 | if (debounceTimeout !== undefined) { 20 | clearTimeout(debounceTimeout); 21 | } 22 | 23 | debounceTimeout = setTimeout(() => { 24 | func(...args); 25 | }, delay); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/utils/api.util.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from './try-catch'; // Assuming Result is { data?: T, error?: Error } 2 | import { toast } from 'svelte-sonner'; 3 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 4 | 5 | export function handleApiResultWithCallbacks({ result, message, setLoadingState = () => {}, onSuccess, onError = () => {} }: { result: Result; message: string; setLoadingState?: (value: boolean) => void; onSuccess: (data: T) => void; onError?: (error: Error) => void }) { 6 | setLoadingState(true); 7 | 8 | if (result.error) { 9 | const dockerMsg = extractDockerErrorMessage(result.error); 10 | console.error(`API Error: ${message}:`, result.error); 11 | toast.error(`${message}: ${dockerMsg}`); 12 | onError(result.error); 13 | setLoadingState(false); 14 | } else { 15 | onSuccess(result.data as T); 16 | setLoadingState(false); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/utils/bytes.util.ts: -------------------------------------------------------------------------------- 1 | // filepath: /Users/kylemendell/dev/ofkm/arcane/src/lib/utils/bytes.ts 2 | const units: { [key: string]: number } = { 3 | b: 1, 4 | k: 1024, 5 | m: 1024 * 1024, 6 | g: 1024 * 1024 * 1024 7 | }; 8 | 9 | export function parseBytes(input: string): number { 10 | const valueStr = input.toLowerCase().trim(); 11 | const unit = valueStr.charAt(valueStr.length - 1); 12 | const value = parseFloat(valueStr.substring(0, valueStr.length - 1)); 13 | 14 | if (isNaN(value)) { 15 | throw new Error(`Invalid numeric value in memory string: ${input}`); 16 | } 17 | 18 | if (units[unit]) { 19 | return Math.floor(value * units[unit]); 20 | } else if (!isNaN(parseFloat(valueStr))) { 21 | // Assume bytes if no unit 22 | return Math.floor(parseFloat(valueStr)); 23 | } else { 24 | throw new Error(`Invalid memory unit: ${unit}. Use b, k, m, or g.`); 25 | } 26 | } 27 | 28 | export function formatBytes(bytes: number, decimals = 2): string { 29 | if (!+bytes) return '0 Bytes'; 30 | const k = 1024; 31 | const dm = decimals < 0 ? 0 : decimals; 32 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 33 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 34 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/utils/errors.util.ts: -------------------------------------------------------------------------------- 1 | export function extractDockerErrorMessage(error: any): string { 2 | if (!error) return 'Unknown error'; 3 | 4 | if (error.response && error.response.data) { 5 | if (typeof error.response.data === 'string') return error.response.data; 6 | if (error.response.data.error) return error.response.data.error; 7 | } 8 | 9 | if (error.body && error.body.error) return error.body.error; 10 | if (error.error) return error.error; 11 | if (error.reason) return error.reason; 12 | if (error.stderr) return error.stderr; 13 | if (error.data && typeof error.data === 'string') return error.data; 14 | if (error.message) return error.message; 15 | return JSON.stringify(error); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/form.utils.ts: -------------------------------------------------------------------------------- 1 | export function preventDefault(fn: (event: Event) => any) { 2 | return function (this: any, event: Event) { 3 | event.preventDefault(); 4 | fn.call(this, event); 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils/fs.utils.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | 3 | export async function fileExists(filePath: string): Promise { 4 | try { 5 | const stats = await fs.stat(filePath); 6 | return stats.isFile(); 7 | } catch { 8 | return false; 9 | } 10 | } 11 | 12 | export async function directoryExists(dir: string): Promise { 13 | try { 14 | const stats = await fs.stat(dir); 15 | return stats.isDirectory(); 16 | } catch { 17 | return false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/utils/onboarding.utils.ts: -------------------------------------------------------------------------------- 1 | import { listUsers, saveUser, hashPassword } from '$lib/services/user-service'; 2 | 3 | // First-run check to create admin user if needed 4 | export async function checkFirstRun() { 5 | try { 6 | // getBasePath from settings-service should already handle dev vs prod 7 | const users = await listUsers(); 8 | 9 | if (users.length === 0) { 10 | console.log('No users found. Creating default admin user...'); 11 | 12 | // Create a default admin user 13 | const passwordHash = await hashPassword('arcane-admin'); // Default password 14 | 15 | await saveUser({ 16 | id: crypto.randomUUID(), 17 | username: 'arcane', 18 | passwordHash, 19 | displayName: 'Arcane Admin', 20 | email: 'arcane@local', 21 | roles: ['admin'], 22 | createdAt: new Date().toISOString() 23 | }); 24 | 25 | console.log('Default admin user created successfully'); 26 | console.log('Username: arcane'); 27 | console.log('Password: arcane-admin'); 28 | console.log('IMPORTANT: Please change this password immediately after first login!'); 29 | } 30 | } catch (error) { 31 | console.error('Error during first-run check:', error); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/utils/registry.utils.ts: -------------------------------------------------------------------------------- 1 | import { parseAll } from '@swimlane/docker-reference'; 2 | 3 | export function parseImageNameForRegistry(imageName: string): { registry: string } { 4 | try { 5 | const parsed = parseAll(imageName); 6 | return { registry: parsed.domain ?? 'docker.io' }; 7 | } catch (error) { 8 | // Fallback to Docker Hub if parsing fails 9 | console.error(`Failed to parse image name: ${imageName}`, error); 10 | return { registry: 'docker.io' }; 11 | } 12 | } 13 | 14 | export function areRegistriesEquivalent(url1: string, url2: string): boolean { 15 | const normalize = (url: string) => { 16 | let normalized = url.toLowerCase(); 17 | if (normalized.startsWith('http://')) normalized = normalized.substring(7); 18 | if (normalized.startsWith('https://')) normalized = normalized.substring(8); 19 | if (normalized.endsWith('/')) normalized = normalized.slice(0, -1); 20 | // Common Docker Hub aliases 21 | if (normalized === 'index.docker.io' || normalized === 'registry-1.docker.io' || normalized === 'auth.docker.io') { 22 | return 'docker.io'; 23 | } 24 | return normalized; 25 | }; 26 | return normalize(url1) === normalize(url2); 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/utils/string.utils.ts: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(string: string): string { 2 | if (!string) return ''; 3 | return string.charAt(0).toUpperCase() + string.slice(1); 4 | } 5 | 6 | export function shortId(id: string | undefined, length = 12): string { 7 | if (!id) return 'N/A'; 8 | return id.substring(0, length); 9 | } 10 | 11 | export function truncateString(str: string | undefined, maxLength: number): string { 12 | if (!str) return ''; 13 | if (str.length <= maxLength) { 14 | return str; 15 | } 16 | return str.substring(0, maxLength - 3) + '...'; 17 | } 18 | 19 | export function formatDate(dateString: string | undefined | null): string { 20 | if (!dateString) return 'Unknown'; 21 | try { 22 | return new Date(dateString).toLocaleString(); 23 | } catch (e) { 24 | return 'Invalid Date'; 25 | } 26 | } 27 | 28 | // Function to format logs with some basic highlighting 29 | export function formatLogLine(line: string): string { 30 | if (line.includes('ERROR') || line.includes('FATAL') || line.includes('WARN')) { 31 | return `${line}`; 32 | } 33 | if (line.includes('INFO')) { 34 | return `${line}`; 35 | } 36 | return line; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/utils/try-catch.ts: -------------------------------------------------------------------------------- 1 | type Success = { 2 | data: T; 3 | error: null; 4 | }; 5 | 6 | type Failure = { 7 | data: null; 8 | error: E; 9 | }; 10 | 11 | export type Result = Success | Failure; 12 | 13 | // Main wrapper function 14 | export async function tryCatch(promise: Promise): Promise> { 15 | try { 16 | const data = await promise; 17 | return { data, error: null }; 18 | } catch (error) { 19 | return { data: null, error: error as E }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { version as currentVersion } from '$app/environment'; 2 | import { env } from '$env/dynamic/private'; 3 | import AppConfigService from '$lib/services/app-config-service'; 4 | import type { AppVersionInformation } from '$lib/types/application-configuration'; 5 | import type { LayoutServerLoad } from './$types'; 6 | 7 | let versionInformation: AppVersionInformation; 8 | let versionInformationLastUpdated: number; 9 | 10 | export const load = (async (locals) => { 11 | // If update checks are disabled via env var, return only current version 12 | const updateCheckDisabled = env.UPDATE_CHECK_DISABLED === 'true'; 13 | const csrf = crypto.randomUUID(); 14 | 15 | if (updateCheckDisabled) { 16 | return { 17 | versionInformation: { 18 | currentVersion 19 | } as AppVersionInformation, 20 | user: locals.locals.user || null 21 | }; 22 | } 23 | 24 | const appConfigService = new AppConfigService(); 25 | 26 | // Cache the version information for 3 hours 27 | const cacheExpired = versionInformationLastUpdated && Date.now() - versionInformationLastUpdated > 1000 * 60 * 60 * 3; 28 | 29 | if (!versionInformation || cacheExpired) { 30 | try { 31 | versionInformation = await appConfigService.getVersionInformation(); 32 | versionInformationLastUpdated = Date.now(); 33 | } catch (error) { 34 | console.error('Error fetching version information:', error); 35 | versionInformation = { currentVersion } as AppVersionInformation; 36 | } 37 | } 38 | 39 | return { 40 | versionInformation, 41 | user: locals.locals.user || null, 42 | csrf 43 | }; 44 | }) satisfies LayoutServerLoad; 45 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | Arcane 22 | 23 | 24 | 25 | 26 | 27 | 28 | {#if isNavigating} 29 |
    30 |
    31 |
    32 | {/if} 33 | 34 |
    35 | 36 | {#if showSidebar} 37 |
    46 | -------------------------------------------------------------------------------- /src/routes/api/containers/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { createContainer } from '$lib/services/docker/container-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | import type { ContainerInspectInfo } from 'dockerode'; 8 | 9 | export const POST: RequestHandler = async ({ request }) => { 10 | const config = (await request.json()) as ContainerInspectInfo; 11 | 12 | if (!config.Name || !config.Image) { 13 | const response: ApiErrorResponse = { 14 | success: false, 15 | error: 'Container name and image are required', 16 | code: ApiErrorCode.BAD_REQUEST 17 | }; 18 | return json(response, { status: 400 }); 19 | } 20 | 21 | // Use name from either property 22 | const containerName = config.Name; 23 | const containerConfig = { 24 | ...config, 25 | Name: containerName // Ensure Name is set correctly for downstream code 26 | }; 27 | 28 | const result = await tryCatch(createContainer(containerConfig)); 29 | 30 | if (result.error) { 31 | console.error('Error creating container:', result.error); 32 | 33 | const response: ApiErrorResponse = { 34 | success: false, 35 | error: extractDockerErrorMessage(result.error), 36 | code: ApiErrorCode.DOCKER_API_ERROR, 37 | details: result.error 38 | }; 39 | return json(response, { status: 500 }); 40 | } 41 | 42 | return json({ 43 | success: true, 44 | container: result.data 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/routes/api/containers/[containerId]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getContainer } from '$lib/services/docker/container-service'; 4 | import { tryCatch } from '$lib/utils/try-catch'; 5 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 6 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 7 | 8 | export const GET: RequestHandler = async ({ params }) => { 9 | const containerId = params.containerId; 10 | 11 | const result = await tryCatch(getContainer(containerId)); 12 | 13 | if (result.error) { 14 | console.error(`API Error getting container ${containerId}:`, result.error); 15 | 16 | const response: ApiErrorResponse = { 17 | success: false, 18 | error: extractDockerErrorMessage(result.error), 19 | code: ApiErrorCode.DOCKER_API_ERROR, 20 | details: result.error 21 | }; 22 | 23 | return json(response, { status: 500 }); 24 | } 25 | 26 | if (!result.data) { 27 | const response: ApiErrorResponse = { 28 | success: false, 29 | error: 'Container not found', 30 | code: ApiErrorCode.NOT_FOUND 31 | }; 32 | return json(response, { status: 404 }); 33 | } 34 | 35 | return json(result.data); 36 | }; 37 | -------------------------------------------------------------------------------- /src/routes/api/containers/[containerId]/remove/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { removeContainer } from '$lib/services/docker/container-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const DELETE: RequestHandler = async ({ params, url }) => { 9 | const containerId = params.containerId; 10 | const force = url.searchParams.has('force') ? url.searchParams.get('force') === 'true' : false; 11 | 12 | const result = await tryCatch(removeContainer(containerId, force)); 13 | 14 | if (result.error) { 15 | console.error(`API Error Deleting container ${containerId}:`, result.error); 16 | 17 | const response: ApiErrorResponse = { 18 | success: false, 19 | error: extractDockerErrorMessage(result.error), 20 | code: ApiErrorCode.DOCKER_API_ERROR, 21 | details: result.error 22 | }; 23 | 24 | return json(response, { status: 500 }); 25 | } 26 | 27 | return json({ 28 | success: true, 29 | message: `Container Deleted Successfully` 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/routes/api/containers/[containerId]/restart/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { restartContainer } from '$lib/services/docker/container-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const POST: RequestHandler = async ({ params }) => { 9 | const containerId = params.containerId; 10 | 11 | const result = await tryCatch(restartContainer(containerId)); 12 | 13 | if (result.error) { 14 | console.error(`API Error restarting container ${containerId}:`, result.error); 15 | 16 | const response: ApiErrorResponse = { 17 | success: false, 18 | error: extractDockerErrorMessage(result.error), 19 | code: ApiErrorCode.DOCKER_API_ERROR, 20 | details: result.error 21 | }; 22 | 23 | return json(response, { status: 500 }); 24 | } 25 | 26 | return json({ 27 | success: true, 28 | message: `Container restarted successfully` 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/routes/api/containers/[containerId]/start/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { startContainer } from '$lib/services/docker/container-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const POST: RequestHandler = async ({ params }) => { 9 | const containerId = params.containerId; 10 | 11 | const result = await tryCatch(startContainer(containerId)); 12 | 13 | if (result.error) { 14 | console.error(`API Error starting container ${containerId}:`, result.error); 15 | 16 | const response: ApiErrorResponse = { 17 | success: false, 18 | error: extractDockerErrorMessage(result.error), 19 | code: ApiErrorCode.DOCKER_API_ERROR, 20 | details: result.error 21 | }; 22 | 23 | return json(response, { status: 500 }); 24 | } 25 | 26 | return json({ 27 | success: true, 28 | message: `Container started successfully` 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/routes/api/containers/[containerId]/stop/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { stopContainer } from '$lib/services/docker/container-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const POST: RequestHandler = async ({ params }) => { 9 | const containerId = params.containerId; 10 | 11 | const result = await tryCatch(stopContainer(containerId)); 12 | 13 | if (result.error) { 14 | console.error(`API Error stopping container ${containerId}:`, result.error); 15 | 16 | const response: ApiErrorResponse = { 17 | success: false, 18 | error: extractDockerErrorMessage(result.error), 19 | code: ApiErrorCode.DOCKER_API_ERROR, 20 | details: result.error 21 | }; 22 | 23 | return json(response, { status: 500 }); 24 | } 25 | 26 | return json({ 27 | success: true, 28 | message: `Container stopped successfully` 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/routes/api/containers/start-all/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { listContainers, startContainer } from '$lib/services/docker/container-service'; 3 | import type { RequestHandler } from './$types'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | import type { ContainerInfo } from 'dockerode'; 8 | 9 | export const POST: RequestHandler = async () => { 10 | const result = await tryCatch( 11 | (async () => { 12 | const containers: ContainerInfo[] = await listContainers(true); 13 | const stopped = containers.filter((c) => c.State === 'exited'); 14 | if (stopped.length === 0) { 15 | return { count: 0, message: 'No stopped containers to start.' }; 16 | } 17 | await Promise.all(stopped.map((c) => startContainer(c.Id))); 18 | return { count: stopped.length, message: `Successfully started ${stopped.length} container(s).` }; 19 | })() 20 | ); 21 | 22 | if (result.error) { 23 | console.error('API Error (startAllStopped):', result.error); 24 | 25 | const response: ApiErrorResponse = { 26 | success: false, 27 | error: extractDockerErrorMessage(result.error), 28 | code: ApiErrorCode.DOCKER_API_ERROR, 29 | details: result.error 30 | }; 31 | return json(response, { status: 500 }); 32 | } 33 | 34 | return json({ 35 | success: true, 36 | count: result.data.count, 37 | message: result.data.message 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/routes/api/containers/stop-all/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { listContainers, stopContainer } from '$lib/services/docker/container-service'; 3 | import type { RequestHandler } from './$types'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const POST: RequestHandler = async () => { 9 | const result = await tryCatch( 10 | (async () => { 11 | const containers = await listContainers(true); 12 | const running = containers.filter((c) => c.State === 'running'); 13 | if (running.length === 0) { 14 | return { count: 0, message: 'No running containers to stop.' }; 15 | } 16 | await Promise.all(running.map((c) => stopContainer(c.Id))); 17 | console.log(`API: Stopped ${running.length} containers.`); 18 | return { count: running.length, message: `Successfully stopped ${running.length} container(s).` }; 19 | })() 20 | ); 21 | 22 | if (result.error) { 23 | console.error('API Error (stopAllRunning):', result.error); 24 | 25 | const response: ApiErrorResponse = { 26 | success: false, 27 | error: extractDockerErrorMessage(result.error), 28 | code: ApiErrorCode.DOCKER_API_ERROR, 29 | details: result.error 30 | }; 31 | return json(response, { status: 500 }); 32 | } 33 | 34 | return json({ 35 | success: true, 36 | count: result.data.count, 37 | message: result.data.message 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/routes/api/docker/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | 4 | // Docker system info endpoint 5 | export const GET: RequestHandler = async () => { 6 | try { 7 | // Replace with actual Docker API call 8 | // const docker = new Docker(); 9 | // const info = await docker.info(); 10 | const info = { 11 | version: '25.0.0', 12 | containers: 3, 13 | images: 12, 14 | os: 'Linux', 15 | arch: 'x86_64' 16 | }; 17 | 18 | return json(info); 19 | } catch (error) { 20 | console.error('Error fetching Docker info:', error); 21 | return new Response(JSON.stringify({ error: 'Failed to fetch Docker info' }), { 22 | status: 500, 23 | headers: { 'Content-Type': 'application/json' } 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/api/docker/test-connection/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getDockerInfo } from '$lib/services/docker/core'; // Adjust path if needed 4 | 5 | export const GET: RequestHandler = async ({ url }) => { 6 | // Get the host to test from query parameters 7 | const hostToTest = url.searchParams.get('host'); 8 | 9 | if (!hostToTest) { 10 | return json( 11 | { success: false, error: 'Missing "host" query parameter.' }, 12 | { status: 400 } // Bad Request 13 | ); 14 | } 15 | 16 | try { 17 | // Pass the host from the query param to the service function 18 | await getDockerInfo(); 19 | return json({ 20 | success: true, 21 | message: `Successfully connected to Docker Engine at ${hostToTest}.` 22 | }); 23 | } catch (error: any) { 24 | console.error(`Docker connection test failed for host ${hostToTest}:`, error); 25 | return json( 26 | { 27 | success: false, 28 | error: error.message || `Failed to connect to Docker Engine at ${hostToTest}.` 29 | }, 30 | { status: 503 } 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/api/images/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { listImages } from '$lib/services/docker/image-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const GET: RequestHandler = async () => { 9 | const result = await tryCatch(listImages()); 10 | console.log('Result:', result); 11 | 12 | if (result.error) { 13 | console.error('Error fetching images:', result.error); 14 | 15 | const response: ApiErrorResponse = { 16 | success: false, 17 | error: extractDockerErrorMessage(result.error), 18 | code: ApiErrorCode.DOCKER_API_ERROR, 19 | details: result.error 20 | }; 21 | return json(response, { status: 500 }); 22 | } 23 | 24 | return json(result.data); 25 | }; 26 | -------------------------------------------------------------------------------- /src/routes/api/images/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { removeImage } from '$lib/services/docker/image-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { extractDockerErrorMessage } from '$lib/utils/errors.util'; 6 | import { tryCatch } from '$lib/utils/try-catch'; 7 | 8 | export const DELETE: RequestHandler = async ({ params, url }) => { 9 | const { id } = params; 10 | const force = url.searchParams.get('force') === 'true'; 11 | 12 | if (!id) { 13 | const response: ApiErrorResponse = { 14 | success: false, 15 | error: 'Image ID is required', 16 | code: ApiErrorCode.BAD_REQUEST 17 | }; 18 | return json(response, { status: 400 }); 19 | } 20 | 21 | const result = await tryCatch(removeImage(id, force)); 22 | 23 | if (result.error) { 24 | console.error('Error removing image:', result.error); 25 | 26 | const response: ApiErrorResponse = { 27 | success: false, 28 | error: extractDockerErrorMessage(result.error), 29 | code: ApiErrorCode.DOCKER_API_ERROR, 30 | details: result.error 31 | }; 32 | return json(response, { status: 500 }); 33 | } 34 | 35 | return json({ 36 | success: true 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /src/routes/api/stacks/[stackId]/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { updateStack } from '$lib/services/docker/stack-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const PUT: RequestHandler = async ({ params, request }) => { 8 | const { stackId } = params; 9 | const { name, composeContent, envContent } = await request.json(); 10 | 11 | const result = await tryCatch(updateStack(stackId, { name, composeContent, envContent })); 12 | 13 | if (result.error) { 14 | console.error('Error updating stack:', result.error); 15 | const response: ApiErrorResponse = { 16 | success: false, 17 | error: result.error.message || 'Failed to update stack', 18 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 19 | details: result.error 20 | }; 21 | return json(response, { status: 500 }); 22 | } 23 | 24 | return json({ 25 | success: true, 26 | message: 'Stack updated successfully' 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/routes/api/stacks/[stackId]/deploy/+server.ts: -------------------------------------------------------------------------------- 1 | import { deployStack } from '$lib/services/docker/stack-custom-service'; 2 | import type { RequestHandler } from './$types'; 3 | import { json } from '@sveltejs/kit'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const POST: RequestHandler = async ({ params }) => { 8 | const id = params.stackId; 9 | 10 | const result = await tryCatch(deployStack(id)); 11 | 12 | if (result.error) { 13 | console.error(`API Error starting stack ${id}:`, result.error); 14 | const response: ApiErrorResponse = { 15 | success: false, 16 | error: result.error.message || 'Failed to start stack', 17 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 18 | details: result.error 19 | }; 20 | return json(response, { status: 500 }); 21 | } 22 | 23 | return json({ 24 | success: true, 25 | message: `Stack started successfully` 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/routes/api/stacks/[stackId]/down/+server.ts: -------------------------------------------------------------------------------- 1 | import { stopStack } from '$lib/services/docker/stack-custom-service'; 2 | import type { RequestHandler } from './$types'; 3 | import { json } from '@sveltejs/kit'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const POST: RequestHandler = async ({ params }) => { 8 | const id = params.stackId; 9 | 10 | const result = await tryCatch(stopStack(id)); 11 | 12 | if (result.error) { 13 | console.error(`API Error starting stack ${id}:`, result.error); 14 | const response: ApiErrorResponse = { 15 | success: false, 16 | error: result.error.message || 'Failed to start stack', 17 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 18 | details: result.error 19 | }; 20 | return json(response, { status: 500 }); 21 | } 22 | 23 | return json({ 24 | success: true, 25 | message: `Stack started successfully` 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/routes/api/stacks/[stackId]/migrate/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | import { json } from '@sveltejs/kit'; 3 | import { migrateStackToNameFolder } from '$lib/services/docker/stack-migration-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const POST: RequestHandler = async ({ params }) => { 8 | const { stackId } = params; 9 | 10 | if (!stackId) { 11 | const response: ApiErrorResponse = { 12 | success: false, 13 | error: 'Missing stackId', 14 | code: ApiErrorCode.BAD_REQUEST 15 | }; 16 | return json(response, { status: 400 }); 17 | } 18 | 19 | const result = await tryCatch(migrateStackToNameFolder(stackId)); 20 | 21 | if (result.error) { 22 | console.error('Error migrating stack:', result.error); 23 | const response: ApiErrorResponse = { 24 | success: false, 25 | error: result.error.message || 'Failed to migrate stack', 26 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 27 | details: result.error 28 | }; 29 | return json(response, { status: 500 }); 30 | } 31 | 32 | return json({ 33 | success: true, 34 | message: `Stack "${stackId}" migrated successfully.` 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/routes/api/stacks/[stackId]/redeploy/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { redeployStack } from '$lib/services/docker/stack-custom-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const POST: RequestHandler = async ({ params }) => { 8 | const id = params.stackId; 9 | 10 | const result = await tryCatch(redeployStack(id)); 11 | 12 | if (result.error) { 13 | console.error(`API Error redeploying stack ${id}:`, result.error); 14 | const response: ApiErrorResponse = { 15 | success: false, 16 | error: result.error.message || 'Failed to redeploy stack', 17 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 18 | details: result.error 19 | }; 20 | return json(response, { status: 500 }); 21 | } 22 | 23 | if (result.data) { 24 | return json({ 25 | success: true, 26 | message: `Stack redeployed successfully` 27 | }); 28 | } else { 29 | const response: ApiErrorResponse = { 30 | success: false, 31 | error: 'Failed to redeploy stack', 32 | code: ApiErrorCode.INTERNAL_SERVER_ERROR 33 | }; 34 | return json(response, { status: 500 }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/routes/api/stacks/[stackId]/restart/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | import { json } from '@sveltejs/kit'; 3 | import { restartStack } from '$lib/services/docker/stack-custom-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const POST: RequestHandler = async ({ params }) => { 8 | const id = params.stackId; 9 | 10 | const result = await tryCatch(restartStack(id)); 11 | 12 | if (result.error) { 13 | console.error(`API Error restarting stack ${id}:`, result.error); 14 | const response: ApiErrorResponse = { 15 | success: false, 16 | error: result.error.message || 'Failed to restart stack', 17 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 18 | details: result.error 19 | }; 20 | return json(response, { status: 500 }); 21 | } 22 | 23 | if (result.data) { 24 | return json({ 25 | success: true, 26 | message: `Stack restarted successfully` 27 | }); 28 | } else { 29 | const response: ApiErrorResponse = { 30 | success: false, 31 | error: 'Failed to restart stack', 32 | code: ApiErrorCode.INTERNAL_SERVER_ERROR 33 | }; 34 | return json(response, { status: 500 }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/routes/api/templates/[id]/content/+server.ts: -------------------------------------------------------------------------------- 1 | import { json, error } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { TemplateService } from '$lib/services/template-service'; 4 | 5 | const templateService = new TemplateService(); 6 | 7 | export const GET: RequestHandler = async ({ params }) => { 8 | try { 9 | const { id } = params; 10 | const templates = await templateService.loadAllTemplates(); 11 | const template = templates.find((t) => t.id === id); 12 | 13 | if (!template) { 14 | return error(404, { message: 'Template not found' }); 15 | } 16 | 17 | const templateContent = await templateService.loadTemplateContent(template); 18 | 19 | return json({ 20 | id: template.id, 21 | name: template.name, 22 | description: template.description, 23 | content: templateContent.content, 24 | envContent: templateContent.envContent, 25 | isRemote: template.isRemote, 26 | metadata: template.metadata 27 | }); 28 | } catch (err) { 29 | console.error('Error fetching template content:', err); 30 | return error(500, { message: 'Failed to fetch template content' }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/routes/api/templates/registries/+server.ts: -------------------------------------------------------------------------------- 1 | import { json, error } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { TemplateService } from '$lib/services/template-service'; 4 | import { templateRegistryService } from '$lib/services/template-registry-service'; 5 | import type { TemplateRegistryConfig } from '$lib/types/settings.type'; 6 | 7 | const templateService = new TemplateService(); 8 | 9 | export const GET: RequestHandler = async () => { 10 | try { 11 | const registries = await templateService.getRegistries(); 12 | return json(registries); 13 | } catch (err) { 14 | console.error('Error fetching registries:', err); 15 | return error(500, { message: 'Failed to fetch registries' }); 16 | } 17 | }; 18 | 19 | export const POST: RequestHandler = async ({ request }) => { 20 | try { 21 | const config: TemplateRegistryConfig = await request.json(); 22 | 23 | if (!config.url || !config.name) { 24 | return error(400, { message: 'URL and name are required' }); 25 | } 26 | 27 | // Test the registry before adding 28 | const registry = await templateRegistryService.fetchRegistry(config); 29 | if (!registry) { 30 | return error(400, { message: 'Failed to fetch registry or invalid format' }); 31 | } 32 | 33 | await templateService.addRegistry(config); 34 | 35 | return json({ 36 | success: true, 37 | message: 'Registry added successfully', 38 | registry: config 39 | }); 40 | } catch (err) { 41 | console.error('Error adding registry:', err); 42 | return error(500, { message: 'Failed to add registry' }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/routes/api/templates/registries/test/+server.ts: -------------------------------------------------------------------------------- 1 | import { json, error } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { templateRegistryService } from '$lib/services/template-registry-service'; 4 | 5 | export const POST: RequestHandler = async ({ request }) => { 6 | try { 7 | const { url } = await request.json(); 8 | 9 | if (!url) { 10 | return error(400, { message: 'URL is required' }); 11 | } 12 | 13 | const config = { 14 | url, 15 | name: 'Test Registry', 16 | enabled: true 17 | }; 18 | 19 | const registry = await templateRegistryService.fetchRegistry(config); 20 | 21 | if (!registry) { 22 | return error(400, { message: 'Failed to fetch registry or invalid format' }); 23 | } 24 | 25 | return json({ 26 | success: true, 27 | message: 'Registry is valid and accessible', 28 | registry: { 29 | name: registry.name, 30 | description: registry.description, 31 | version: registry.version, 32 | templateCount: registry.templates.length 33 | } 34 | }); 35 | } catch (err) { 36 | console.error('Error testing registry:', err); 37 | return error(500, { message: 'Failed to test registry' }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/routes/api/templates/stats/+server.ts: -------------------------------------------------------------------------------- 1 | import { json, error } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { TemplateService } from '$lib/services/template-service'; 4 | 5 | const templateService = new TemplateService(); 6 | 7 | export const GET: RequestHandler = async () => { 8 | try { 9 | const [allTemplates, registries] = await Promise.all([templateService.loadAllTemplates(), templateService.getRegistries()]); 10 | 11 | const localTemplates = allTemplates.filter((t) => !t.isRemote); 12 | const remoteTemplates = allTemplates.filter((t) => t.isRemote); 13 | 14 | const stats = { 15 | total: allTemplates.length, 16 | local: localTemplates.length, 17 | remote: remoteTemplates.length, 18 | registries: registries.length, 19 | enabledRegistries: registries.filter((r) => r.enabled).length, 20 | templatesWithEnv: allTemplates.filter((t) => t.envContent || t.metadata?.envUrl).length 21 | }; 22 | 23 | return json(stats); 24 | } catch (err) { 25 | console.error('Error fetching template stats:', err); 26 | return error(500, { message: 'Failed to fetch template statistics' }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/routes/api/volumes/[name]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { removeVolume } from '$lib/services/docker/volume-service'; 4 | import { ApiErrorCode, type ApiErrorResponse } from '$lib/types/errors.type'; 5 | import { tryCatch } from '$lib/utils/try-catch'; 6 | 7 | export const DELETE: RequestHandler = async ({ params, url }) => { 8 | const { name } = params; 9 | const force = url.searchParams.get('force') === 'true'; 10 | 11 | const result = await tryCatch(removeVolume(name, force)); 12 | 13 | if (result.error) { 14 | console.error('API Error removing volume:', result.error); 15 | const response: ApiErrorResponse = { 16 | success: false, 17 | error: result.error.message || 'Failed to remove volume', 18 | code: ApiErrorCode.INTERNAL_SERVER_ERROR, 19 | details: result.error 20 | }; 21 | return json(response, { status: 500 }); 22 | } 23 | 24 | return json({ success: true }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/routes/auth/logout/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { Actions, PageServerLoad } from './$types'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | throw redirect(302, '/auth/login'); 6 | }; 7 | 8 | export const actions: Actions = { 9 | default: async ({ locals }) => { 10 | // Clear the session using the destroy method 11 | await locals.session.destroy(); 12 | throw redirect(302, '/auth/login'); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/containers/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { listContainers } from '$lib/services/docker/container-service'; 2 | import { listImages } from '$lib/services/docker/image-service'; 3 | import { listNetworks } from '$lib/services/docker/network-service'; 4 | import { listVolumes } from '$lib/services/docker/volume-service'; 5 | import type { PageServerLoad } from './$types'; 6 | 7 | export const load: PageServerLoad = async () => { 8 | try { 9 | const [containers, volumes, networks, images] = await Promise.all([listContainers(true), listVolumes(), listNetworks(), listImages()]); 10 | 11 | return { 12 | containers, 13 | volumes, 14 | networks, 15 | images 16 | }; 17 | } catch (error) { 18 | console.error('Error loading container data:', error); 19 | return { 20 | containers: [], 21 | volumes: [], 22 | networks: [], 23 | images: [], 24 | error: error instanceof Error ? error.message : String(error) 25 | }; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/routes/containers/[id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { getContainer, getContainerLogs, getContainerStats } from '$lib/services/docker/container-service'; 3 | import { error } from '@sveltejs/kit'; 4 | 5 | export const load: PageServerLoad = async ({ params }) => { 6 | const containerId = params.id; 7 | 8 | try { 9 | const [container, logs, stats] = await Promise.all([ 10 | getContainer(containerId), 11 | getContainerLogs(containerId, { tail: 100 }).catch((err) => { 12 | console.error(`Failed to retrieve logs for ${containerId}:`, err); 13 | return 'Failed to load logs. Container might not be running or logs are unavailable.'; 14 | }), 15 | getContainerStats(containerId).catch((err) => { 16 | console.error(`Failed to retrieve stats for ${containerId}:`, err); 17 | return null; 18 | }) 19 | ]); 20 | 21 | if (!container) { 22 | error(404, { 23 | message: `Container with ID "${containerId}" not found.` 24 | }); 25 | } 26 | 27 | return { 28 | container, 29 | logs, 30 | stats 31 | }; 32 | } catch (err: any) { 33 | console.error(`Failed to load container ${containerId}:`, err); 34 | if (err.name === 'NotFoundError') { 35 | error(404, { message: err.message }); 36 | } else { 37 | error(500, { 38 | message: err.message || `Failed to load container details for "${containerId}".` 39 | }); 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/routes/images/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { listImages, isImageInUse, checkImageMaturity } from '$lib/services/docker/image-service'; 3 | import type { EnhancedImageInfo, ServiceImage } from '$lib/types/docker'; 4 | import { getSettings } from '$lib/services/settings-service'; 5 | import type { Settings } from '$lib/types/settings.type'; 6 | 7 | type ImageData = { 8 | images: EnhancedImageInfo[]; 9 | error?: string; 10 | settings: Settings; 11 | }; 12 | 13 | export const load: PageServerLoad = async (): Promise => { 14 | try { 15 | const images: ServiceImage[] = await listImages(); 16 | const settings = await getSettings(); 17 | 18 | const enhancedImages = await Promise.all( 19 | images.map(async (image): Promise => { 20 | const inUse = await isImageInUse(image.Id); 21 | 22 | let maturity = undefined; 23 | try { 24 | if (image.repo !== '' && image.tag !== '') { 25 | maturity = await checkImageMaturity(image.Id); 26 | } 27 | } catch (maturityError) { 28 | console.error(`Failed to check maturity for image ${image.Id}:`, maturityError); 29 | } 30 | 31 | return { 32 | ...image, 33 | inUse, 34 | maturity 35 | }; 36 | }) 37 | ); 38 | 39 | return { 40 | images: enhancedImages, 41 | settings 42 | }; 43 | } catch (err: any) { 44 | console.error('Failed to load images:', err); 45 | const settings = await getSettings().catch(() => ({}) as Settings); 46 | return { 47 | images: [], 48 | error: err.message || 'Failed to connect to Docker or list images.', 49 | settings: settings 50 | }; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/routes/images/[imageId]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { getImage } from '$lib/services/docker/image-service'; 3 | import { error } from '@sveltejs/kit'; 4 | import { NotFoundError } from '$lib/types/errors'; 5 | 6 | export const load: PageServerLoad = async ({ params }) => { 7 | const imageId = params.imageId; 8 | 9 | try { 10 | const image = await getImage(imageId); 11 | 12 | return { 13 | image 14 | }; 15 | } catch (err: any) { 16 | console.error(`Failed to load image ${imageId}:`, err); 17 | if (err instanceof NotFoundError) { 18 | error(404, { message: err.message }); 19 | } else { 20 | error(err.status || 500, { 21 | message: err.message || `Failed to load image details for "${imageId}".` 22 | }); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/routes/networks/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { listNetworks } from '$lib/services/docker/network-service'; 3 | import type { NetworkInspectInfo } from 'dockerode'; 4 | 5 | type NetworkPageData = { 6 | networks: NetworkInspectInfo[]; 7 | error?: string; 8 | }; 9 | 10 | export const load: PageServerLoad = async (): Promise => { 11 | try { 12 | const networks = await listNetworks(); 13 | return { 14 | networks 15 | }; 16 | } catch (err: unknown) { 17 | console.error('Failed to load networks:', err); 18 | const message = err instanceof Error ? err.message : 'Failed to connect to Docker or list networks.'; 19 | return { 20 | networks: [], 21 | error: message 22 | }; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/routes/networks/[networkId]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad, Actions } from './$types'; 2 | import { getNetwork, removeNetwork } from '$lib/services/docker/network-service'; 3 | import { error, fail, redirect } from '@sveltejs/kit'; 4 | import { NotFoundError, ConflictError, DockerApiError } from '$lib/types/errors'; 5 | 6 | export const load: PageServerLoad = async ({ params }) => { 7 | const networkId = params.networkId; 8 | 9 | try { 10 | const network = await getNetwork(networkId); 11 | 12 | return { 13 | network 14 | }; 15 | } catch (err: unknown) { 16 | console.error(`Failed to load network ${networkId}:`, err); 17 | if (err instanceof NotFoundError) { 18 | error(404, { message: err.message }); 19 | } else { 20 | const statusCode = err && typeof err === 'object' && 'status' in err ? (err as { status: number }).status : 500; 21 | error(statusCode, { 22 | message: err instanceof Error ? err.message : `Failed to load network details for "${networkId}".` 23 | }); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/onboarding/complete/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 |
    8 |
    9 | 10 |
    11 |
    12 | 13 |

    Setup Complete!

    14 | 15 |
    16 |

    Congratulations! You've successfully completed the initial setup for Arcane.

    17 |

    You can now start using the application to manage your Docker containers and compose stacks.

    18 |
    19 | 20 | 21 |
    22 | -------------------------------------------------------------------------------- /src/routes/onboarding/password/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | 4 | export async function load() { 5 | const settings = await getSettings(); 6 | 7 | if (settings.onboarding?.steps?.password) { 8 | throw redirect(302, '/onboarding/settings'); 9 | } 10 | 11 | return { settings }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/onboarding/settings/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | 4 | export async function load() { 5 | const settings = await getSettings(); 6 | 7 | if (!settings.onboarding?.steps?.password) { 8 | throw redirect(302, '/onboarding/welcome'); 9 | } 10 | 11 | return { settings }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/onboarding/welcome/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | 4 | export async function load() { 5 | const settings = await getSettings(); 6 | 7 | if (settings.onboarding && settings.onboarding.completed) { 8 | throw redirect(302, '/'); 9 | } 10 | 11 | return { settings }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/settings/docker/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | try { 6 | const settings = await getSettings(); 7 | 8 | return { 9 | settings 10 | }; 11 | } catch (error) { 12 | console.error('Failed to load Docker settings:', error); 13 | return { 14 | settings: { 15 | dockerHost: 'unix:///var/run/docker.sock', 16 | registryCredentials: [], 17 | pollingEnabled: true, 18 | pollingInterval: 10, 19 | autoUpdate: false, 20 | autoUpdateInterval: 60, 21 | pruneMode: 'all' 22 | }, 23 | csrf: crypto.randomUUID() 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/settings/general/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | try { 6 | const settings = await getSettings(); 7 | 8 | return { 9 | settings 10 | }; 11 | } catch (error) { 12 | console.error('Failed to load settings:', error); 13 | return { 14 | settings: { 15 | stacksDirectory: 'data/stacks', 16 | baseServerUrl: 'localhost', 17 | maturityThresholdDays: 30 18 | } 19 | }; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/routes/settings/rbac/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | const settings = await getSettings(); 6 | 7 | return { 8 | settings 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/routes/settings/security/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | import { env } from '$env/dynamic/private'; 4 | 5 | export const load: PageServerLoad = async () => { 6 | const settings = await getSettings(); 7 | 8 | const oidcEnvVarsConfigured = !!env.OIDC_CLIENT_ID && !!env.OIDC_CLIENT_SECRET && !!env.OIDC_REDIRECT_URI && !!env.OIDC_AUTHORIZATION_ENDPOINT && !!env.OIDC_TOKEN_ENDPOINT && !!env.OIDC_USERINFO_ENDPOINT; 9 | 10 | return { 11 | settings, 12 | oidcEnvVarsConfigured 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/settings/templates/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { TemplateService } from '$lib/services/template-service'; 2 | import { getSettings } from '$lib/services/settings-service'; 3 | import { templateRegistryService } from '$lib/services/template-registry-service'; 4 | import { fail } from '@sveltejs/kit'; 5 | import type { Actions, PageServerLoad } from './$types'; 6 | import type { TemplateRegistryConfig } from '$lib/types/settings.type'; 7 | 8 | export const load: PageServerLoad = async () => { 9 | try { 10 | const templateService = new TemplateService(); 11 | const settings = await getSettings(); 12 | 13 | const templates = await templateService.loadAllTemplates(); 14 | 15 | const localTemplateCount = templates.filter((t) => !t.isRemote).length; 16 | const remoteTemplateCount = templates.filter((t) => t.isRemote).length; 17 | 18 | return { 19 | settings, 20 | localTemplateCount, 21 | remoteTemplateCount 22 | }; 23 | } catch (error) { 24 | console.error('Error loading template settings:', error); 25 | const fallbackSettings = await getSettings(); 26 | return { 27 | settings: fallbackSettings, 28 | localTemplateCount: 0, 29 | remoteTemplateCount: 0, 30 | error: error instanceof Error ? error.message : 'Failed to load template data' 31 | }; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/settings/users/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import UserAPIService from '$lib/services/api/user-api-service'; 3 | import { listUsers } from '$lib/services/user-service'; 4 | 5 | export const load: PageServerLoad = async ({ cookies }) => { 6 | try { 7 | const userApi = new UserAPIService(); 8 | 9 | const users = await listUsers(); 10 | 11 | const sanitizedUsers = users.map((user) => { 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | const { passwordHash: _passwordHash, ...rest } = user; 14 | return rest; 15 | }); 16 | 17 | return { 18 | users: users || [] 19 | }; 20 | } catch (error) { 21 | console.error('Failed to load users:', error); 22 | return { 23 | users: [] 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/stacks/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { loadComposeStacks, discoverExternalStacks } from '$lib/services/docker/stack-service'; 2 | import type { PageServerLoad } from './$types'; 3 | import { tryCatch } from '$lib/utils/try-catch'; 4 | 5 | export const load: PageServerLoad = async () => { 6 | const [managedResult, externalResult] = await Promise.all([tryCatch(loadComposeStacks()), tryCatch(discoverExternalStacks())]); 7 | 8 | if (managedResult.error || externalResult.error) { 9 | console.error('Failed to load stacks:', managedResult.error || externalResult.error); 10 | const errorMessage = (managedResult.error?.message || externalResult.error?.message) ?? 'Unknown error'; 11 | return { 12 | stacks: [], 13 | error: 'Failed to load Docker Compose stacks: ' + errorMessage 14 | }; 15 | } 16 | 17 | const managedStacks = managedResult.data; 18 | const externalStacks = externalResult.data; 19 | const combinedStacks = [...managedStacks]; 20 | 21 | for (const externalStack of externalStacks) { 22 | if (!combinedStacks.some((stack) => stack.id === externalStack.id)) { 23 | combinedStacks.push(externalStack); 24 | } 25 | } 26 | 27 | return { 28 | stacks: combinedStacks 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/routes/stacks/new/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { TemplateService } from '$lib/services/template-service'; 2 | import { defaultComposeTemplate } from '$lib/constants'; 3 | import type { PageServerLoad } from './$types'; 4 | 5 | export const load = (async () => { 6 | try { 7 | const templateService = new TemplateService(); 8 | 9 | const [allTemplates, envTemplate] = await Promise.all([templateService.loadAllTemplates(), TemplateService.getEnvTemplate()]); 10 | 11 | return { 12 | composeTemplates: allTemplates, 13 | envTemplate, 14 | defaultTemplate: defaultComposeTemplate 15 | }; 16 | } catch (error) { 17 | console.error('Error loading templates:', error); 18 | 19 | // Return fallback data 20 | return { 21 | composeTemplates: [], 22 | envTemplate: defaultComposeTemplate, 23 | defaultTemplate: defaultComposeTemplate 24 | }; 25 | } 26 | }) satisfies PageServerLoad; 27 | -------------------------------------------------------------------------------- /src/routes/volumes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { listVolumes, isVolumeInUse } from '$lib/services/docker/volume-service'; 3 | import type { VolumeInspectInfo } from 'dockerode'; 4 | 5 | type EnhancedVolumeInfo = VolumeInspectInfo & { 6 | inUse: boolean; 7 | }; 8 | 9 | type VolumePageData = { 10 | volumes: EnhancedVolumeInfo[]; 11 | error?: string; 12 | }; 13 | 14 | export const load: PageServerLoad = async (): Promise => { 15 | try { 16 | const volumesData = await listVolumes(); 17 | 18 | const enhancedVolumes = await Promise.all( 19 | volumesData.map(async (volume): Promise => { 20 | const inUse = await isVolumeInUse(volume.Name); 21 | return { 22 | ...volume, 23 | inUse 24 | }; 25 | }) 26 | ); 27 | 28 | return { 29 | volumes: enhancedVolumes 30 | }; 31 | } catch (err: unknown) { 32 | console.error('Failed to load volumes:', err); 33 | const message = err instanceof Error ? err.message : 'Failed to connect to Docker or list volumes.'; 34 | return { 35 | volumes: [], 36 | error: message 37 | }; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofkm/arcane/076cbf490485b5062f1320f37ac89443d8cef25c/static/favicon.png -------------------------------------------------------------------------------- /static/img/arcane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ofkm/arcane/076cbf490485b5062f1320f37ac89443d8cef25c/static/img/arcane.png -------------------------------------------------------------------------------- /static/img/arcane.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import packageJson from './package.json' with { type: 'json' }; 4 | 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | kit: { 8 | adapter: adapter({ 9 | out: 'build', 10 | precompress: false, 11 | polyfill: true 12 | }), 13 | csrf: { 14 | checkOrigin: process.env.NODE_ENV === 'production' 15 | }, 16 | version: { 17 | name: packageJson.version 18 | } 19 | } 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /template-registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Community Docker Templates", 3 | "description": "A collection of popular Docker Compose templates", 4 | "version": "1.0.0", 5 | "templates": [ 6 | { 7 | "id": "wordpress-mysql", 8 | "name": "WordPress + MySQL", 9 | "description": "WordPress blog with MySQL database", 10 | "version": "1.0.0", 11 | "author": "Community", 12 | "tags": ["cms", "blog", "php", "mysql"], 13 | "compose_url": "https://raw.githubusercontent.com/example/templates/main/wordpress/docker-compose.yml", 14 | "env_url": "https://raw.githubusercontent.com/example/templates/main/wordpress/.env.example", 15 | "documentation_url": "https://github.com/example/templates/tree/main/wordpress", 16 | "icon_url": "https://example.com/icons/wordpress.svg", 17 | "updated_at": "2025-05-28T10:00:00Z" 18 | }, 19 | { 20 | "id": "nextjs-postgres", 21 | "name": "Next.js + PostgreSQL", 22 | "description": "Next.js application with PostgreSQL database", 23 | "version": "1.0.0", 24 | "author": "Community", 25 | "tags": ["react", "nextjs", "postgresql", "frontend"], 26 | "compose_url": "https://raw.githubusercontent.com/example/templates/main/nextjs/docker-compose.yml", 27 | "env_url": "https://raw.githubusercontent.com/example/templates/main/nextjs/.env.example", 28 | "documentation_url": "https://github.com/example/templates/tree/main/nextjs", 29 | "updated_at": "2025-05-28T09:30:00Z" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/.auth/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookies": [ 3 | { 4 | "name": "kit.session", 5 | "value": "51e972dc20999f886ad5304cc98ca9d304f8c07a97dfef48a600949ded39cffcdf58fa8c19cd35fc2f0ae018a4358203efd2a524e0f9ef6c8deabb3ac9f68e5efaa4529f9cc9aa99d48605624925fa2bf248a3979294ba44a27ff97a259a3214ab580619bc6492eb5899591b6977742e2116329e701d186956f8c7827afeae4f06066bfa5af4a4c2f70d218dff7f3aadcbac7ad4bdf2bf40ad4a0830ab4fc0204bc7be1d9c11320a4488e5d16d3ec8700f477456e7c7684eab174590a7%26id%3D1", 6 | "domain": "localhost", 7 | "path": "/", 8 | "expires": 1782709844.331035, 9 | "httpOnly": true, 10 | "secure": false, 11 | "sameSite": "Lax" 12 | } 13 | ], 14 | "origins": [ 15 | { 16 | "origin": "http://localhost:3000", 17 | "localStorage": [ 18 | { 19 | "name": "mode-watcher-theme", 20 | "value": "" 21 | }, 22 | { 23 | "name": "mode-watcher-mode", 24 | "value": "system" 25 | } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /tests/auth.setup.ts: -------------------------------------------------------------------------------- 1 | import { test as setup, expect } from '@playwright/test'; 2 | 3 | const authFile = 'tests/.auth/login.json'; 4 | 5 | setup('authenticate', async ({ page }) => { 6 | await page.goto('/auth/login'); 7 | await page.getByLabel('Username').fill('arcane'); 8 | await page.getByLabel('Password').fill('arcane-admin'); 9 | await page.getByRole('button', { name: 'Sign in', exact: true }).click(); 10 | 11 | await expect(page).toHaveURL('/'); 12 | 13 | await page.context().storageState({ path: authFile }); 14 | console.log(`Authentication state saved to ${authFile}`); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()], 7 | optimizeDeps: { 8 | exclude: ['ssh2', 'cpu-features'] // Exclude problematic dependencies 9 | }, 10 | ssr: { 11 | noExternal: ['ssh2', 'cpu-features'] // Exclude from SSR bundling 12 | }, 13 | build: { 14 | rollupOptions: { 15 | external: [/\.node$/] // Explicitly mark .node files as external 16 | } 17 | } 18 | }); 19 | --------------------------------------------------------------------------------