From ffc9ccd77ed80309a35372d2441ca4b226cb11df Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Mon, 3 Aug 2026 23:56:42 +0100 Subject: [PATCH] feat: onboarding wizard, PWA support, Oracle Cloud free tier deploy, mode nav update --- index.html | 40 +++++++++++++- public/icon-192.png | Bin 0 -> 3064 bytes public/icon-512.png | Bin 0 -> 10224 bytes public/manifest.json | 13 +++++ public/sw.js | 40 ++++++++++++++ src/generators/oracle-cloud.js | 98 +++++++++++++++++++++++++++++++++ src/main.js | 79 +++++++++++++++++++++++++- src/style.css | 36 ++++++++++++ 8 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 public/icon-192.png create mode 100644 public/icon-512.png create mode 100644 public/manifest.json create mode 100644 public/sw.js create mode 100644 src/generators/oracle-cloud.js diff --git a/index.html b/index.html index decf62c..be89f71 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,9 @@ + + +
@@ -16,8 +19,9 @@

Build your self-hosted Docker stack in minutes

@@ -125,6 +129,7 @@ +
Select apps above to see preview...
@@ -181,6 +186,36 @@ + + + + diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..c56d6f5fce3fbdb579a4e6422c6491aa4bd484c2 GIT binary patch literal 3064 zcmb7GeN+=y7JtKp43Ll@AO?XU9xAvNHTa1zfe~4e) zSn0Nkt{{Q4weIR-tm|qIt_)eN?pA@G#ezL3LzK!YN~l`1f@JSYQq*cG{bOEU-X!n7 zd++b#ZBI^0@bnn#fe`XcoFBgkK6(7OiD6vSo{(b=0KB6e| z&RG1*V1_jl^d?TbyRairFq!{A&1d**nb)*V8M!=Vzm~gRB~vjqbhm^o*?7+XtynZw$gy}%nLT(PVDQ5eM=QgTX?{~OiURyz>k)kYz z$@O3M5_e)CTK+~Z(z`F0D^ds-$aXCpV*eU^l3DO>;cOV{WARMQ=JgPv2`%L2bmXmi zbDii*lju*Hi5J+=>jw0GwMdfgJ~3J1nc_YvrAl{Xrpd2Ir;g+jf|NO2d7!YyMUEgw zIebMlES*rmaEqv+w1;5uf>(G2L8hlJqR55^(lYuL`8_=`pI1G4w?{Kfu)>Rp-JiQg zr^{u+RZ=rR3x5@NR|uD=Z8T42EZQ&Zvd`8_y*kn%D~7{?TSzU}J64kuuWlYAR?gm@ z=83!HQ&#Y26*+{E@dw#S(>3G&rtzE^p3#uhR!CYKiCXgmM6*(_w70oNtD2*%`-rG8 z3n~DZ@0%FlYbQUigEaM4MaQulY#BTN_*uj<38B^1R#|@?+UQKnE+85-U&pU56+w>1IVx#E{A2NTN-*fG zG&$&%-bKu+pOX~72duVoxRNsOz|z7BR>3JKhaW`S_9n@HXa_=U=|w}?Ax`(glfve?!tP6-B$ zeJ#*wQo{l`DXtvUe3{(N>aF$?ti?fMow=+=rM8bi`Jnn-05q8Y7ADJN#@X0}3&H+= z5r6;iZQMA-Z0i*QOX@6zlDUg-09=PcspW9dn;aR80WGxcrMfF{yPN%Ox2ZO#VZ@wi z8eXIJWp)-ht}C>?7z^RJfO=Aw4Kd)V5y5Idvq$%})!AQl7u)*9Z9z^y%6ibz)-ap~ z?)$j;EeTGA50L_7z}O7a2D1YU@4Lt?--9&uM)H?LwD2i_R>*;7U40z}Ej+0?oF#?q z+3CC=oi`5?CB{AN9p|NMC&MZ#wcL$hVPc-#(nR4VFqfce1A{OyT`;s~!uH_m0+AVu zFBR34+X{w%hvMBCeg&9_?I;x)=*%x!v8Aubdg-~|zCjX;?a=A%tAd3!BDv)@scT~R z&44~(GJwqzNe)~mPQS$+vZ(oM(yZ?wCc8QwjQJ@3$pKQ=8Ymoj;=bY+dgb+JMY%$E zCyGZ*JNP2N1}(JERvPFep&E-&gOuCC>dWd|CjK@D1$ z$+Bkrvqa_8-s)BROybI6LJj#%?GX^8Btu>tCLB1Obc%k-DC5 z)s$P+hO@mOU{$nW>uSx6V68oC?*ZkQ?@{b%&Ez~!@M18EVBiH-kijd+mg~4-{37`6 r_ZNpqVnxHC2Eu~%{y5%==555~lkut@yB52{jUFZbHYvU~PE+uI959!X literal 0 HcmV?d00001 diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..0a0fffdae632daa38e38f9a1ea23c7de7f0508e3 GIT binary patch literal 10224 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 literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..7059ed8 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "StackPilot", + "short_name": "StackPilot", + "description": "Build your self-hosted Docker stack in minutes", + "start_url": "/", + "display": "standalone", + "background_color": "#09090b", + "theme_color": "#6366f1", + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..5a1eaaa --- /dev/null +++ b/public/sw.js @@ -0,0 +1,40 @@ +const CACHE_NAME = 'stackpilot-v1'; +const STATIC_ASSETS = ['/', '/index.html']; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + // Don't cache API calls + if (request.url.includes('/api/')) { + event.respondWith(fetch(request)); + return; + } + // Cache-first for static assets + event.respondWith( + caches.match(request).then((cached) => { + const fetched = fetch(request).then((response) => { + if (response.ok) { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + } + return response; + }).catch(() => cached); + return cached || fetched; + }) + ); +}); diff --git a/src/generators/oracle-cloud.js b/src/generators/oracle-cloud.js new file mode 100644 index 0000000..8bf7ef0 --- /dev/null +++ b/src/generators/oracle-cloud.js @@ -0,0 +1,98 @@ +/** + * Generate a cloud-init script for Oracle Cloud Always Free tier. + * Oracle free tier: 4 ARM Ampere A1 cores, 24GB RAM, 200GB storage. + * This produces a user-data script that installs Docker + Compose, + * sets up the stack, and runs the auto-linker. + */ +export function generateOracleCloudInit(selectedApps, config, composYaml, envContent, setupScript) { + const lines = [ + '#!/bin/bash', + '# StackPilot — Oracle Cloud Free Tier Deployment Script', + '# Usage: Paste this as "Cloud-Init Script" when creating your free instance.', + '# Instance shape: VM.Standard.A1.Flex (4 OCPUs, 24GB RAM)', + `# Generated: ${new Date().toISOString()}`, + '', + 'set -euo pipefail', + 'exec > >(tee /var/log/stackpilot-init.log) 2>&1', + '', + 'echo "=== StackPilot Cloud-Init Starting ==="', + '', + '# Update system', + 'apt-get update && apt-get upgrade -y', + '', + '# Install Docker', + 'curl -fsSL https://get.docker.com | sh', + 'systemctl enable docker && systemctl start docker', + '', + '# Install Docker Compose plugin', + 'apt-get install -y docker-compose-plugin', + '', + '# Create stackpilot user', + 'useradd -m -s /bin/bash -G docker stackpilot || true', + '', + '# Create directories', + `mkdir -p ${config.configPath}`, + `mkdir -p ${config.mediaPath}`, + `mkdir -p ${config.downloadsPath}`, + `chown -R stackpilot:stackpilot ${config.configPath} ${config.mediaPath} ${config.downloadsPath}`, + '', + '# Write docker-compose.yml', + 'mkdir -p /home/stackpilot/stack', + 'cat > /home/stackpilot/stack/docker-compose.yml << \'COMPOSE_EOF\'', + composYaml, + 'COMPOSE_EOF', + '', + '# Write .env', + 'cat > /home/stackpilot/stack/.env << \'ENV_EOF\'', + envContent, + 'ENV_EOF', + '', + '# Write setup.sh', + 'cat > /home/stackpilot/stack/setup.sh << \'SETUP_EOF\'', + setupScript, + 'SETUP_EOF', + 'chmod +x /home/stackpilot/stack/setup.sh', + '', + '# Set ownership', + 'chown -R stackpilot:stackpilot /home/stackpilot/stack', + '', + '# Open firewall ports (Oracle Linux / Ubuntu iptables)', + 'iptables -I INPUT -p tcp --match multiport --dports 80,443,8080,8096,8989,7878,9696,8112,32400 -j ACCEPT || true', + 'iptables-save > /etc/iptables/rules.v4 2>/dev/null || true', + '', + '# Start the stack', + 'cd /home/stackpilot/stack', + 'docker compose up -d', + '', + '# Wait and run auto-linker', + 'echo "Waiting 60s for services to start..."', + 'sleep 60', + 'su - stackpilot -c "cd /home/stackpilot/stack && ./setup.sh" || true', + '', + '# Setup auto-start on reboot', + 'cat > /etc/systemd/system/stackpilot.service << \'SERVICE_EOF\'', + '[Unit]', + 'Description=StackPilot Docker Stack', + 'After=docker.service', + 'Requires=docker.service', + '', + '[Service]', + 'Type=oneshot', + 'RemainAfterExit=yes', + 'WorkingDirectory=/home/stackpilot/stack', + 'ExecStart=/usr/bin/docker compose up -d', + 'ExecStop=/usr/bin/docker compose down', + 'User=stackpilot', + '', + '[Install]', + 'WantedBy=multi-user.target', + 'SERVICE_EOF', + 'systemctl enable stackpilot.service', + '', + 'echo "=== StackPilot Cloud-Init Complete ==="', + `echo "Stack is running! Services: ${selectedApps.filter(a => a.ports?.length > 0).map(a => a.name).join(', ')}"`, + 'echo "Check logs: /var/log/stackpilot-init.log"', + ]; + + return lines.join('\n'); +} diff --git a/src/main.js b/src/main.js index d3ad181..d3d2621 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ import { apps, categories } from './catalog/apps.js'; import { generateCompose, generateEnv } from './generators/compose.js'; import { generateSetupScript } from './generators/setup.js'; import { analyseCompose, fixCompose } from './generators/analyser.js'; +import { generateOracleCloudInit } from './generators/oracle-cloud.js'; import { trackDownload, fetchPopularity } from './utils/analytics.js'; import { initAuth } from './utils/auth.js'; import { initGallery } from './utils/gallery.js'; @@ -254,6 +255,14 @@ function updatePreview() { case 'setup': content.textContent = generateSetupScript(selectedApps, config); break; + case 'oracle': + content.textContent = generateOracleCloudInit( + selectedApps, config, + generateCompose(selectedApps, config), + generateEnv(selectedApps, config), + generateSetupScript(selectedApps, config) + ); + break; } } @@ -387,6 +396,7 @@ initSearch(); initPresets(); initAuth(); initGallery(() => [...state.selected], getConfig); +initWizard(); document.getElementById('download-btn').addEventListener('click', downloadBundle); // Listen for "Use This Stack" from gallery @@ -408,6 +418,72 @@ fetchPopularity().then(data => { } }); +// Wizard +const wizardGoalMap = { + movies: ['jellyfin', 'sonarr', 'radarr', 'prowlarr', 'bazarr', 'jellyseerr', 'qbittorrent', 'flaresolverr'], + music: ['navidrome', 'lidarr', 'prowlarr', 'qbittorrent'], + books: ['calibre-web', 'readarr', 'audiobookshelf', 'kavita', 'prowlarr', 'qbittorrent'], + photos: ['immich', 'postgres', 'redis'], + downloads: ['qbittorrent', 'prowlarr', 'sonarr', 'radarr', 'flaresolverr', 'gluetun'], + ai: ['ollama', 'open-webui', 'comfyui', 'flowise'], + gaming: ['minecraft', 'crafty', 'pterodactyl'], + 'smart-home': ['homeassistant', 'mosquitto', 'zigbee2mqtt', 'nodered', 'esphome'], + development: ['gitea', 'code-server', 'drone', 'registry', 'postgres', 'redis'], + 'cloud-storage': ['nextcloud', 'syncthing', 'filebrowser'], + privacy: ['adguard-home', 'gluetun', 'searxng', 'vaultwarden', 'tailscale'], + monitoring: ['portainer', 'uptime-kuma', 'grafana', 'prometheus', 'netdata', 'watchtower', 'dozzle'], + 'remote-access': ['tailscale', 'nginx-proxy-manager', 'authelia'], + 'team-chat': ['matrix-synapse', 'element', 'ntfy'], + notes: ['trilium', 'outline', 'hedgedoc', 'memos', 'postgres'], + finance: ['actual-budget', 'firefly-iii', 'ghostfolio'], +}; + +function initWizard() { + document.getElementById('wizard-build-btn').addEventListener('click', () => { + const checked = [...document.querySelectorAll('#wizard-goals input:checked')].map(el => el.value); + if (checked.length === 0) return; + + const recommended = new Set(); + for (const goal of checked) { + const goalApps = wizardGoalMap[goal] || []; + goalApps.forEach(id => recommended.add(id)); + } + // Always add essentials if multiple goals + if (checked.length >= 2) { + recommended.add('homepage'); + recommended.add('watchtower'); + } + + const appsList = [...recommended].filter(id => apps.some(a => a.id === id)); + const resultEl = document.getElementById('wizard-result'); + const appsEl = document.getElementById('wizard-apps'); + + appsEl.innerHTML = appsList.map(id => { + const app = apps.find(a => a.id === id); + return `${app ? app.name : id}`; + }).join(''); + + resultEl.classList.remove('hidden'); + resultEl.dataset.apps = JSON.stringify(appsList); + }); + + document.getElementById('wizard-use-btn').addEventListener('click', () => { + const resultEl = document.getElementById('wizard-result'); + const appsList = JSON.parse(resultEl.dataset.apps || '[]'); + state.selected = new Set(appsList); + renderAppGrid(); + checkDependencies(); + checkPortConflicts(); + updatePreview(); + updateUrlState(); + // Switch to builder + document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); + document.querySelector('.mode-btn[data-mode="builder"]').classList.add('active'); + document.querySelector('.container').style.display = 'block'; + document.getElementById('step-wizard').style.display = 'none'; + }); +} + // Search function initSearch() { const input = document.getElementById('app-search'); @@ -444,7 +520,7 @@ function initPresets() { }); } -// Mode switching (builder vs analyser vs gallery) +// Mode switching (builder vs wizard vs analyser vs gallery) function initModeNav() { document.querySelectorAll('.mode-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -452,6 +528,7 @@ function initModeNav() { btn.classList.add('active'); const mode = btn.dataset.mode; document.querySelector('.container').style.display = mode === 'builder' ? 'block' : 'none'; + document.getElementById('step-wizard').style.display = mode === 'wizard' ? 'block' : 'none'; document.getElementById('step-analyser').style.display = mode === 'analyser' ? 'block' : 'none'; document.getElementById('step-gallery').style.display = mode === 'gallery' ? 'block' : 'none'; }); diff --git a/src/style.css b/src/style.css index c10c7c4..ec8ff71 100644 --- a/src/style.css +++ b/src/style.css @@ -648,6 +648,42 @@ body { /* Gallery */ .gallery-section { max-width: 1280px; margin: 0 auto; padding: 2.5rem 1.5rem; } + +/* Wizard */ +.wizard-section { max-width: 1280px; margin: 0 auto; padding: 2.5rem 1.5rem; } +.wizard-desc { color: var(--text-dim); margin-bottom: 1.5rem; font-size: 0.95rem; } +.wizard-goals { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.6rem; + margin-bottom: 1.5rem; +} +.wizard-goal { + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.8rem 1rem; + cursor: pointer; + transition: all 0.15s ease; + font-size: 0.9rem; +} +.wizard-goal:hover { border-color: var(--accent); background: var(--surface-hover); } +.wizard-goal input:checked + span { color: var(--accent-hover); } +.wizard-goal input { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; } +.wizard-result { margin-top: 1.5rem; } +.wizard-result h3 { margin-bottom: 1rem; font-size: 1rem; color: var(--success); } +.wizard-apps { display: flex; flex-wrap: wrap; gap: 0.4rem; } +.wizard-app-chip { + background: var(--accent-glow); + color: var(--accent-hover); + padding: 0.35rem 0.75rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 500; +} .gallery-controls { display: flex; gap: 0.75rem;