From 1d941b148c45542d8e3718bb8d76b2673e34cc89 Mon Sep 17 00:00:00 2001 From: tonyaellie Date: Fri, 11 Apr 2025 15:25:44 +0000 Subject: [PATCH] feat: split into separate files --- docs/.vitepress/components/BasicConfig.vue | 105 +++ docs/.vitepress/components/ConfigEditor.vue | 844 ++---------------- docs/.vitepress/components/ConfigPreview.vue | 81 ++ docs/.vitepress/components/DatabaseConfig.vue | 112 +++ docs/.vitepress/components/HttpsConfig.vue | 179 ++++ docs/.vitepress/components/StorageConfig.vue | 76 ++ .../components/StorageTypeSelector.vue | 95 ++ docs/.vitepress/components/common.css | 150 ++++ .../components/dockerComposeGenerator.ts | 369 ++++++++ docs/.vitepress/components/types.ts | 90 ++ 10 files changed, 1347 insertions(+), 754 deletions(-) create mode 100644 docs/.vitepress/components/BasicConfig.vue create mode 100644 docs/.vitepress/components/ConfigPreview.vue create mode 100644 docs/.vitepress/components/DatabaseConfig.vue create mode 100644 docs/.vitepress/components/HttpsConfig.vue create mode 100644 docs/.vitepress/components/StorageConfig.vue create mode 100644 docs/.vitepress/components/StorageTypeSelector.vue create mode 100644 docs/.vitepress/components/common.css create mode 100644 docs/.vitepress/components/dockerComposeGenerator.ts create mode 100644 docs/.vitepress/components/types.ts diff --git a/docs/.vitepress/components/BasicConfig.vue b/docs/.vitepress/components/BasicConfig.vue new file mode 100644 index 00000000..b6bf21d3 --- /dev/null +++ b/docs/.vitepress/components/BasicConfig.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/ConfigEditor.vue b/docs/.vitepress/components/ConfigEditor.vue index 82e40792..9c64a2db 100644 --- a/docs/.vitepress/components/ConfigEditor.vue +++ b/docs/.vitepress/components/ConfigEditor.vue @@ -15,408 +15,48 @@ - -
-
-
-

Basic Configuration

-

Configure the basic settings for your Homebox instance.

-
-
-
- -
- - -
-
- -
- - -

Only used if HTTPS with Traefik is not enabled

-
- -
- - -
- -
- -
- - -
-
- -
- -
- -
- - -
-
- -
- -
- - -
-
- -
- -
- - -
-
-
-
-
- - -
-
-
-

Database Configuration

-

Configure the database for your Homebox instance.

-
-
-
- - -
- -
-
- - -
- -
- - -
- -
- - -
- -
- -
- - - -
-
- -
- - -
-
-
-
-
- - -
-
-
-

Proxy Configuration with Traefik

-

Configure Traefik as a reverse proxy with automatic HTTPS for your Homebox instance.

-
-
-
- -
- - -
-
- -
-
- - -

The domain name must be pointed to your server's IP address

-
- -
- - -

Required for Let's Encrypt certificate notifications

-
-
-
-
-
- - -
-
-
-

Storage Configuration

-

Configure storage options for your Homebox instance and related services.

-
-
- -
-

Homebox Data Storage

-

Store Homebox data in a Docker volume or host directory

- -
-
- - -
-
- - -
-
- -
- - -
-
- - -

Absolute path recommended (e.g., /home/user/homebox-data)

-
-
- - -
-

PostgreSQL Data Storage

-

Store PostgreSQL data in a Docker volume or host directory

- -
-
- - -
-
- - -
-
- -
- - -
-
- - -

Absolute path recommended (e.g., /home/user/postgres-data)

-
-
- - -
-

Traefik Data Storage

-

Store Traefik certificates in a Docker volume or host directory

- -
-
- - -
-
- - -
-
- -
- - -
-
- - -

Absolute path recommended (e.g., /home/user/traefik-data)

-
-
-
-
-
+ + + + + + + -
-
-
-
-

Docker Compose Configuration

-
- - -
-
-

This configuration will be saved as docker-compose.yml

-
-
- -
-
-
+ - \ No newline at end of file diff --git a/docs/.vitepress/components/ConfigPreview.vue b/docs/.vitepress/components/ConfigPreview.vue new file mode 100644 index 00000000..5f0ea1c5 --- /dev/null +++ b/docs/.vitepress/components/ConfigPreview.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/DatabaseConfig.vue b/docs/.vitepress/components/DatabaseConfig.vue new file mode 100644 index 00000000..79d476d8 --- /dev/null +++ b/docs/.vitepress/components/DatabaseConfig.vue @@ -0,0 +1,112 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/HttpsConfig.vue b/docs/.vitepress/components/HttpsConfig.vue new file mode 100644 index 00000000..a73b94bd --- /dev/null +++ b/docs/.vitepress/components/HttpsConfig.vue @@ -0,0 +1,179 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/StorageConfig.vue b/docs/.vitepress/components/StorageConfig.vue new file mode 100644 index 00000000..18b37a42 --- /dev/null +++ b/docs/.vitepress/components/StorageConfig.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/StorageTypeSelector.vue b/docs/.vitepress/components/StorageTypeSelector.vue new file mode 100644 index 00000000..9e7886d0 --- /dev/null +++ b/docs/.vitepress/components/StorageTypeSelector.vue @@ -0,0 +1,95 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/common.css b/docs/.vitepress/components/common.css new file mode 100644 index 00000000..d0f3f155 --- /dev/null +++ b/docs/.vitepress/components/common.css @@ -0,0 +1,150 @@ +.card { + background-color: var(--vp-c-bg-soft); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; +} + +.card-header { + padding: 1.5rem; + border-bottom: 1px solid var(--vp-c-divider); +} + +.card-title { + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 0.5rem; + + border-top: 0px; + padding-top: 0px; +} + +.card-description { + color: var(--vp-c-text-2); + font-size: 0.875rem; + margin: 0; +} + +.card-content { + padding: 1.5rem; +} + +.tab-content { + width: 100%; +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-group label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.5rem; + border: 1px soli var(--vp-c-divider); + border-radius: 4px; + background-color: var(--vp-c-bg); + color: var(--vp-c-text-1); + font-size: 0.875rem; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--vp-c-brand); + box-shadow: 0 0 0 2px rgba(var(--vp-c-brand-rgb), 0.1); +} + +.form-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.25rem; +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 40px; + height: 20px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-switch label { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--vp-c-divider); + transition: .4s; + border-radius: 20px; +} + +.toggle-switch label:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +.toggle-switch input:checked + label { + background-color: var(--vp-c-brand); +} + +.toggle-switch input:checked + label:before { + transform: translateX(20px); +} + +.help-text { + font-size: 0.75rem; + color: var(--vp-c-text-2); + margin-top: 0.25rem; +} + +.separator { + height: 1px; + background-color: var(--vp-c-divider); + margin: 1.5rem 0; +} + +.nested-form { + margin-top: 1rem; + padding: 1rem; + border: 1px solid var(--vp-c-divider); + border-radius: 4px; +} + +.icon-button { + padding: 0.5rem; + background-color: var(--vp-c-bg-mute); + border: 1px solid var(--vp-c-divider); + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.icon-button:hover { + background-color: var(--vp-c-bg-alt); +} \ No newline at end of file diff --git a/docs/.vitepress/components/dockerComposeGenerator.ts b/docs/.vitepress/components/dockerComposeGenerator.ts new file mode 100644 index 00000000..72ee64ec --- /dev/null +++ b/docs/.vitepress/components/dockerComposeGenerator.ts @@ -0,0 +1,369 @@ +import type { AppConfig, DockerServices } from "./types" // Assuming types are in a separate file + +export function generateDockerCompose(config: AppConfig): string { + const services: DockerServices = { + homebox: { + image: config.rootless + ? "ghcr.io/sysadminsmedia/homebox:latest-rootless" + : "ghcr.io/sysadminsmedia/homebox:latest", + container_name: "homebox", + restart: "always", + environment: [ + `HBOX_LOG_LEVEL=${config.logLevel}`, + `HBOX_LOG_FORMAT=${config.logFormat}`, + `HBOX_WEB_MAX_FILE_UPLOAD=${config.maxFileUpload}`, + `HBOX_OPTIONS_ALLOW_ANALYTICS=${config.allowAnalytics}`, + `HBOX_OPTIONS_ALLOW_REGISTRATION=${config.allowRegistration}`, + `HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID=${config.autoIncrementAssetId}`, + `HBOX_OPTIONS_CHECK_GITHUB_RELEASE=${config.checkGithubRelease}`, + ], + volumes: [], + }, + } + + // Configure homebox volumes based on storage type + if (config.storageConfig.homeboxStorage.type === "volume") { + services.homebox.volumes.push( + `${config.storageConfig.homeboxStorage.volumeName}:/data/`, + ) + } else { + services.homebox.volumes.push( + `${config.storageConfig.homeboxStorage.directory}:/data/`, + ) + } + + // Configure ports based on HTTPS option + if (config.httpsOption === "none") { + services.homebox.ports = [`${config.port}:7745`] + } else { + // For HTTPS options, the proxy will handle the ports + services.homebox.expose = ["7745"] + } + + // Add database configuration if PostgreSQL is selected + if (config.databaseType === "postgres") { + // Ensure environment array exists before pushing + if (!services.homebox.environment) { + services.homebox.environment = [] + } + services.homebox.environment.push( + "HBOX_DATABASE_DRIVER=postgres", + `HBOX_DATABASE_HOST=${config.postgresConfig.host}`, + `HBOX_DATABASE_PORT=${config.postgresConfig.port}`, + `HBOX_DATABASE_USERNAME=${config.postgresConfig.username}`, + `HBOX_DATABASE_PASSWORD=${config.postgresConfig.password}`, + `HBOX_DATABASE_DATABASE=${config.postgresConfig.database}`, + ) + + // Add PostgreSQL service + services["postgres"] = { + image: "postgres:14", + container_name: "homebox-postgres", + restart: "always", + environment: [ + `POSTGRES_USER=${config.postgresConfig.username}`, + `POSTGRES_PASSWORD=${config.postgresConfig.password}`, + `POSTGRES_DB=${config.postgresConfig.database}`, + ], + volumes: [], + } + + // Configure postgres volumes based on storage type + if (config.storageConfig.postgresStorage.type === "volume") { + services.postgres.volumes.push( + `${config.storageConfig.postgresStorage.volumeName}:/var/lib/postgresql/data`, + ) + } else { + services.postgres.volumes.push( + `${config.storageConfig.postgresStorage.directory}:/var/lib/postgresql/data`, + ) + } + } + + // Add HTTPS configuration based on selected option + switch (config.httpsOption) { + case "traefik": + addTraefikConfig(services, config) + break + case "nginx": + addNginxConfig(services, config) + break + case "caddy": + addCaddyConfig(services, config) + break + case "cloudflared": + addCloudflaredConfig(services, config) + break + } + + // Format the Docker Compose YAML + let dockerCompose = "# generated by homebox config generator v0.0.1\n\nservices:\n" + + // Add services + Object.entries(services).forEach(([serviceName, serviceConfig]) => { + dockerCompose += ` ${serviceName}:\n` + Object.entries(serviceConfig).forEach(([key, value]) => { + if (Array.isArray(value)) { + dockerCompose += ` ${key}:\n` + value.forEach((item: string) => { + // Added type assertion for item + dockerCompose += ` - ${item}\n` + }) + } else if (value !== undefined) { + // Check for undefined before adding + dockerCompose += ` ${key}: ${value}\n` + } + }) + }) + + // Add volumes section if needed + const volumeNames: string[] = [] + + // Only add volumes that are configured as Docker volumes, not directories + if (config.storageConfig.homeboxStorage.type === "volume") { + volumeNames.push(config.storageConfig.homeboxStorage.volumeName) + } + + if ( + config.databaseType === "postgres" && + config.storageConfig.postgresStorage.type === "volume" + ) { + volumeNames.push(config.storageConfig.postgresStorage.volumeName) + } + + // Add HTTPS-related volumes + if ( + config.httpsOption === "traefik" && + config.storageConfig.traefikStorage.type === "volume" + ) { + volumeNames.push(config.storageConfig.traefikStorage.volumeName) + } + + if ( + config.httpsOption === "nginx" && + config.storageConfig.nginxStorage.type === "volume" + ) { + volumeNames.push(config.storageConfig.nginxStorage.volumeName) + } + + if ( + config.httpsOption === "caddy" && + config.storageConfig.caddyStorage.type === "volume" + ) { + volumeNames.push(config.storageConfig.caddyStorage.volumeName) + } + + if ( + config.httpsOption === "cloudflared" && + config.storageConfig.cloudflaredStorage.type === "volume" + ) { + volumeNames.push(config.storageConfig.cloudflaredStorage.volumeName) + } + + if (volumeNames.length > 0) { + dockerCompose += "\nvolumes:\n" + volumeNames.forEach((volumeName: string) => { + dockerCompose += ` ${volumeName}:\n driver: local\n` + }) + } + + return dockerCompose +} + +function addTraefikConfig(services: DockerServices, config: AppConfig): void { + // Add Traefik labels to Homebox + services.homebox.labels = [ + "traefik.enable=true", + `traefik.http.routers.homebox.rule=Host(\`${config.traefikConfig.domain}\`)`, + "traefik.http.routers.homebox.entrypoints=websecure", + "traefik.http.routers.homebox.tls.certresolver=letsencrypt", + "traefik.http.services.homebox.loadbalancer.server.port=7745", + ] + + // Add Traefik service + services["traefik"] = { + image: "traefik:v2.10", + container_name: "homebox-traefik", + restart: "always", + ports: ["80:80", "443:443"], + command: [ + "--api.insecure=false", + "--providers.docker=true", + "--providers.docker.exposedbydefault=false", + "--entrypoints.web.address=:80", + "--entrypoints.web.http.redirections.entrypoint.to=websecure", + "--entrypoints.web.http.redirections.entrypoint.scheme=https", + "--entrypoints.websecure.address=:443", + "--certificatesresolvers.letsencrypt.acme.tlschallenge=true", + `--certificatesresolvers.letsencrypt.acme.email=${config.traefikConfig.email}`, + "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json", + ], + volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"], + } + + // Configure traefik volumes based on storage type + if (config.storageConfig.traefikStorage.type === "volume") { + services.traefik.volumes.push( + `${config.storageConfig.traefikStorage.volumeName}:/letsencrypt`, + ) + } else { + services.traefik.volumes.push( + `${config.storageConfig.traefikStorage.directory}:/letsencrypt`, + ) + } +} + +function addNginxConfig(services: DockerServices, config: AppConfig): void { + // Add Nginx service + services["nginx"] = { + image: "nginx:latest", + container_name: "homebox-nginx", + restart: "always", + ports: [`${config.nginxConfig.port}:443`, "80:80"], + volumes: [], + depends_on: ["homebox"], + } + + // Configure nginx volumes based on storage type + if (config.storageConfig.nginxStorage.type === "volume") { + services.nginx.volumes.push( + `${config.storageConfig.nginxStorage.volumeName}/conf.d:/etc/nginx/conf.d`, + ) + services.nginx.volumes.push( + `${config.storageConfig.nginxStorage.volumeName}/ssl:/etc/nginx/ssl`, + ) + } else { + services.nginx.volumes.push( + `${config.storageConfig.nginxStorage.directory}/conf.d:/etc/nginx/conf.d`, + ) + services.nginx.volumes.push( + `${config.storageConfig.nginxStorage.directory}/ssl:/etc/nginx/ssl`, + ) + } + + // Add default Nginx configuration path (assuming the file exists) + const nginxConfVolume = + config.storageConfig.nginxStorage.type === "volume" + ? `${config.storageConfig.nginxStorage.volumeName}/conf.d/default.conf:/etc/nginx/conf.d/default.conf` + : `${config.storageConfig.nginxStorage.directory}/conf.d/default.conf:/etc/nginx/conf.d/default.conf` + + services.nginx.volumes.push(nginxConfVolume) + + // Add comments via environment variables (Docker Compose doesn't support comments directly in YAML this way) + services.nginx.environment = [ + "# You need to create SSL certificates and place them in the SSL directory", + `# Certificate path: ${config.nginxConfig.sslCertPath}`, + `# Key path: ${config.nginxConfig.sslKeyPath}`, + "# Then create a default.conf file in the conf.d directory with the following content:", + "# server {", + "# listen 80;", + `# server_name ${config.nginxConfig.domain};`, + "# return 301 https://$host$request_uri;", + "# }", + "# server {", + "# listen 443 ssl;", + `# server_name ${config.nginxConfig.domain};`, + `# ssl_certificate ${config.nginxConfig.sslCertPath};`, + `# ssl_certificate_key ${config.nginxConfig.sslKeyPath};`, + "# location / {", + "# proxy_pass http://homebox:7745;", + "# proxy_set_header Host $host;", + "# proxy_set_header X-Real-IP $remote_addr;", + "# }", + "# }", + ] +} + +function addCaddyConfig(services: DockerServices, config: AppConfig): void { + // Add Caddy service + services["caddy"] = { + image: "caddy:latest", + container_name: "homebox-caddy", + restart: "always", + ports: ["80:80", "443:443"], + volumes: [], + depends_on: ["homebox"], + } + + // Configure caddy volumes based on storage type + if (config.storageConfig.caddyStorage.type === "volume") { + services.caddy.volumes.push( + `${config.storageConfig.caddyStorage.volumeName}/data:/data`, + ) + services.caddy.volumes.push( + `${config.storageConfig.caddyStorage.volumeName}/config:/config`, + ) + services.caddy.volumes.push( + `${config.storageConfig.caddyStorage.volumeName}/Caddyfile:/etc/caddy/Caddyfile`, + ) + } else { + services.caddy.volumes.push( + `${config.storageConfig.caddyStorage.directory}/data:/data`, + ) + services.caddy.volumes.push( + `${config.storageConfig.caddyStorage.directory}/config:/config`, + ) + services.caddy.volumes.push( + `${config.storageConfig.caddyStorage.directory}/Caddyfile:/etc/caddy/Caddyfile`, + ) + } + + // Add environment variables for Caddy comments and potential ACME config + services.caddy.environment = [ + `# Create a Caddyfile in ${config.storageConfig.caddyStorage.type === "volume" ? config.storageConfig.caddyStorage.volumeName : config.storageConfig.caddyStorage.directory} with the following content:`, + `# ${config.caddyConfig.domain} {`, + "# reverse_proxy homebox:7745", + "# }", + ] + + // Add email if provided for ACME + if (config.caddyConfig.email) { + // Ensure environment array exists + if (!services.caddy.environment) { + services.caddy.environment = [] + } + services.caddy.environment.push(`ACME_AGREE=true`) // Note: Caddy v2 doesn't use ACME_AGREE env var, email is set in Caddyfile + services.caddy.environment.push(`EMAIL=${config.caddyConfig.email}`) // This might be useful for scripting but Caddy reads email from Caddyfile + services.caddy.environment.push( + `# Add 'email ${config.caddyConfig.email}' to your Caddyfile for automatic HTTPS`, + ) + } +} + +function addCloudflaredConfig( + services: DockerServices, + config: AppConfig, +): void { + // Add Cloudflared service + services["cloudflared"] = { + image: "cloudflare/cloudflared:latest", + container_name: "homebox-cloudflared", + restart: "always", + command: ["tunnel", "--no-autoupdate", "run"], + volumes: [], + environment: [`TUNNEL_TOKEN=${config.cloudflaredConfig.token}`], + depends_on: ["homebox"], + } + + // Configure cloudflared volumes based on storage type + if (config.storageConfig.cloudflaredStorage.type === "volume") { + services.cloudflared.volumes.push( + `${config.storageConfig.cloudflaredStorage.volumeName}:/etc/cloudflared`, + ) + } else { + services.cloudflared.volumes.push( + `${config.storageConfig.cloudflaredStorage.directory}:/etc/cloudflared`, + ) + } + + // Add comments via environment variables + // Ensure environment array exists + if (!services.cloudflared.environment) { + services.cloudflared.environment = [] + } + services.cloudflared.environment.push( + "# Create a tunnel in the Cloudflare Zero Trust dashboard", + `# Configure DNS for ${config.cloudflaredConfig.domain} to point to your tunnel`, + "# Add a public hostname in the tunnel configuration pointing to http://homebox:7745", + ) +} diff --git a/docs/.vitepress/components/types.ts b/docs/.vitepress/components/types.ts new file mode 100644 index 00000000..7614556a --- /dev/null +++ b/docs/.vitepress/components/types.ts @@ -0,0 +1,90 @@ +// types.ts + +export type StorageType = "volume" | "directory" +export type HttpsOption = "none" | "traefik" | "nginx" | "caddy" | "cloudflared" +export type DatabaseType = "sqlite" | "postgres" + +export interface StorageDetail { + type: StorageType + directory: string + volumeName: string +} + +export interface StorageConfig { + homeboxStorage: StorageDetail + postgresStorage: StorageDetail + traefikStorage: StorageDetail + nginxStorage: StorageDetail + caddyStorage: StorageDetail + cloudflaredStorage: StorageDetail +} + +export interface PostgresConfig { + host: string + port: string + username: string + password: string + database: string +} + +export interface TraefikConfig { + domain: string + email: string +} + +export interface NginxConfig { + domain: string + port: string + sslCertPath: string + sslKeyPath: string +} + +export interface CaddyConfig { + domain: string + email: string +} + +export interface CloudflaredConfig { + tunnel: string // Note: This wasn't used in the generator function, but kept for completeness + domain: string + token: string +} + +export interface AppConfig { + image: string // Not directly used in generator, but part of the config + rootless: boolean + port: string + logLevel: string + logFormat: string + maxFileUpload: string + allowAnalytics: boolean + httpsOption: HttpsOption + traefikConfig: TraefikConfig + nginxConfig: NginxConfig + caddyConfig: CaddyConfig + cloudflaredConfig: CloudflaredConfig + databaseType: DatabaseType + postgresConfig: PostgresConfig + allowRegistration: boolean + autoIncrementAssetId: boolean + checkGithubRelease: boolean + storageConfig: StorageConfig +} + +// Types for the generated Docker Compose structure +export interface DockerService { + image: string + container_name: string + restart: string + environment?: string[] + volumes: string[] + ports?: string[] + expose?: string[] + labels?: string[] + command?: string[] + depends_on?: string[] +} + +export interface DockerServices { + [key: string]: DockerService +}