deploymaster-panel 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,15 @@
1
+ PORT=9000
2
+ HOST=0.0.0.0
3
+ # HTTP selalu aktif untuk testing (http://IP:9000)
4
+ # HTTPS aktif otomatis jika cert tersedia. Untuk testing bisa auto-generate self-signed:
5
+ HTTPS_PORT=9443
6
+ HTTPS_ENABLED=true
7
+ AUTO_SELF_SIGNED=true
8
+ # Jika punya cert asli, isi path ini (relatif atau absolut):
9
+ # TLS_CERT_PATH=./certs/cert.pem
10
+ # TLS_KEY_PATH=./certs/key.pem
11
+ TLS_CERT_PATH=
12
+ TLS_KEY_PATH=
13
+ DATA_DIR=./data
14
+ APPS_DIR=./apps
15
+ UPLOAD_DIR=./uploads
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeployMaster Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # DeployMaster — Panel Deployment Node.js & Next.js (On-Premise, Windows & Ubuntu)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Self-hosted deployment service: upload project `.zip` via **HTTP (testing)** / **HTTPS (produksi)**, manifest otomatis, build, jalan, rollback.
4
+
5
+ ## Fitur
6
+ - ✅ Deploy **Node.js & Next.js** (Express, Nest, Vite, Nuxt terdeteksi otomatis)
7
+ - ✅ Jalan di **Windows & Ubuntu** (murni Node.js, tanpa Docker)
8
+ - ✅ Upload via **HTTP :9000** (testing) + **HTTPS :9443** (produksi, self-signed / cert asli)
9
+ - ✅ **Auto-manifest** (`deployment-manifest.json`) + checklist syarat deploy
10
+ - ✅ **Rollback**: 5 release terakhir, redeploy gagal = auto-rollback, rollback manual 1 klik
11
+ - ✅ UI menarik + **Dokumentasi HTML** di `/docs.html`
12
+ - ✅ Start/stop/restart, log live, env editor, port manager
13
+
14
+ ## Cepat mulai (dari source)
15
+ ```bash
16
+ cp .env.example .env
17
+ npm install
18
+ npm start
19
+ # HTTP http://localhost:9000 (testing/upload)
20
+ # HTTPS https://localhost:9443 (self-signed auto)
21
+ ```
22
+
23
+ ## Pakai via npm (setelah publish)
24
+ ```bash
25
+ npx deploymaster
26
+ # atau global:
27
+ npm i -g deploymaster
28
+ deploymaster --port 9000 --https-port 9443
29
+ ```
30
+ Data client tersimpan di `./deploymaster-data`, `./deploymaster-apps`
31
+ (bisa diubah via ENV `DATA_DIR`, `APPS_DIR`, `UPLOAD_DIR`).
32
+
33
+ ## Publish ke npm
34
+ ```bash
35
+ npm login
36
+ npm publish --access public
37
+ ```
38
+
39
+ ## Syarat project agar bisa deploy
40
+ 1. `package.json` di root ZIP
41
+ 2. `scripts.start` (cth. `next start` / `node server.js`)
42
+ 3. `scripts.build` untuk Next/Nuxt/Vite/Nest (cth. `next build`)
43
+ 4. Support `process.env.PORT` (Next otomatis; Express wajib `app.listen(process.env.PORT || 3000)`)
44
+ 5. Jangan sertakan `node_modules` / `.next` di ZIP
45
+
46
+ Detail lengkap: buka `/docs.html` → bagian **Syarat Project**.
47
+
48
+ ## Rollback
49
+ - Tiap deploy = `apps/<nama>/releases/<timestamp>/` (maks 5).
50
+ - Redeploy gagal → otomatis balik ke versi sebelumnya.
51
+ - Manual: kartu app → Manifest & Rollback → tab Rollback → pilih versi.
52
+
53
+ ## Struktur
54
+ ```
55
+ src/server.js HTTP+HTTPS + API
56
+ src/detector.js deteksi framework + checklist syarat
57
+ src/deployer.js deploy/redeploy/rollback + build
58
+ src/proc.js process manager Win/Ubuntu
59
+ src/store.js JSON DB
60
+ public/index.html UI panel
61
+ public/docs.html dokumentasi HTML
62
+ ```
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ // DeployMaster CLI — jalan via `npx deploymaster` / global install.
3
+ // Bisa dijalankan dari folder mana saja; data disimpan di folder kerja saat ini
4
+ // atau sesuai ENV (DATA_DIR, APPS_DIR, UPLOAD_DIR).
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const args = process.argv.slice(2);
9
+ if (args.includes('--help') || args.includes('-h')) {
10
+ console.log(`
11
+ DeployMaster v1.0 — panel deployment Node.js & Next.js (Windows & Ubuntu)
12
+
13
+ Pakai:
14
+ npx deploymaster # jalan di http://localhost:9000 + https://localhost:9443
15
+ npx deploymaster --port 9000 --https-port 9443
16
+ deploymaster # bila install global (npm i -g deploymaster)
17
+
18
+ Opsi:
19
+ --port <n> HTTP panel (default 9000 / ENV PORT)
20
+ --https-port <n> HTTPS panel (default 9443 / ENV HTTPS_PORT)
21
+ --no-https matikan HTTPS
22
+ --help tampilkan ini
23
+
24
+ Env (.env / environment):
25
+ PORT, HTTPS_PORT, HTTPS_ENABLED, AUTO_SELF_SIGNED,
26
+ DATA_DIR, APPS_DIR, UPLOAD_DIR, TLS_CERT_PATH, TLS_KEY_PATH
27
+
28
+ Lalu buka http://localhost:9000 (testing) atau https://localhost:9443.
29
+ Dokumentasi: https://localhost:9000/docs.html
30
+ `);
31
+ process.exit(0);
32
+ }
33
+
34
+ for (let i = 0; i < args.length; i++) {
35
+ if (args[i] === '--port' && args[i + 1]) process.env.PORT = args[i + 1];
36
+ if (args[i] === '--https-port' && args[i + 1]) process.env.HTTPS_PORT = args[i + 1];
37
+ if (args[i] === '--no-https') process.env.HTTPS_ENABLED = 'false';
38
+ }
39
+
40
+ // Default folder data relatif ke CWD (agar npx dari mana saja tetap jalan),
41
+ // bukan relatif ke lokasi paket. Buat dulu sebelum config dimuat.
42
+ for (const k of ['DATA_DIR', 'APPS_DIR', 'UPLOAD_DIR']) {
43
+ if (!process.env[k]) process.env[k] = path.join(process.cwd(), k === 'DATA_DIR' ? 'deploymaster-data' : k === 'APPS_DIR' ? 'deploymaster-apps' : 'deploymaster-uploads');
44
+ }
45
+
46
+ // Resolve server.js relatif ke paket ini
47
+ const server = path.join(__dirname, '..', 'src', 'server.js');
48
+ if (!fs.existsSync(server)) {
49
+ console.error('server.js tidak ditemukan di paket:', server);
50
+ process.exit(1);
51
+ }
52
+ require(server);
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "deploymaster-panel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "DeployMaster - Self-hosted deployment service untuk Node.js & Next.js (Windows & Ubuntu, HTTP/HTTPS, auto-manifest)",
5
+ "main": "src/server.js",
6
+ "bin": {
7
+ "deploymaster": "bin/deploymaster.js"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "public/",
12
+ "bin/",
13
+ "scripts/",
14
+ "README.md",
15
+ "LICENSE",
16
+ ".env.example"
17
+ ],
18
+ "type": "commonjs",
19
+ "license": "MIT",
20
+ "keywords": [
21
+ "deployment",
22
+ "nodejs",
23
+ "nextjs",
24
+ "on-premise",
25
+ "self-hosted",
26
+ "windows",
27
+ "ubuntu",
28
+ "rollback"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "preferGlobal": true,
34
+ "scripts": {
35
+ "start": "node src/server.js",
36
+ "dev": "node --watch src/server.js"
37
+ },
38
+ "dependencies": {
39
+ "adm-zip": "^0.5.16",
40
+ "cors": "^2.8.5",
41
+ "dotenv": "^16.4.5",
42
+ "express": "^4.19.2",
43
+ "multer": "^1.4.5-lts.1",
44
+ "selfsigned": "^2.4.1"
45
+ }
46
+ }
package/public/app.js ADDED
@@ -0,0 +1,186 @@
1
+ let APPS = [], SYS = null, curId = null, analyzedManifest = null;
2
+
3
+ document.querySelectorAll('.navbtn').forEach(b => b.onclick = () => {
4
+ document.querySelectorAll('.navbtn').forEach(x => x.className = 'navbtn btn bg-slate-800 text-left');
5
+ b.className = 'navbtn btn bg-sky-500 text-slate-950 text-left';
6
+ const v = b.dataset.nav;
7
+ document.getElementById('title').textContent = v === 'dash' ? 'Dashboard' : v === 'deploy' ? 'Deploy Baru (Upload ZIP via HTTP/HTTPS)' : 'Sistem & HTTPS';
8
+ for (const s of ['dash', 'deploy', 'system']) document.getElementById('view-' + s).classList.toggle('hidden', s !== v);
9
+ });
10
+
11
+ // drop zone
12
+ const drop = document.getElementById('drop'), fFile = document.getElementById('fFile');
13
+ drop.onclick = () => fFile.click();
14
+ drop.ondragover = e => { e.preventDefault(); drop.style.background = '#0f2743'; };
15
+ drop.ondragleave = () => drop.style.background = '';
16
+ drop.ondrop = e => { e.preventDefault(); drop.style.background = ''; if (e.dataTransfer.files[0]) { fFile.files = e.dataTransfer.files; showF(); } };
17
+ fFile.onchange = showF;
18
+ function showF() { document.getElementById('fName2').textContent = fFile.files[0] ? '📦 ' + fFile.files[0].name : ''; if (!document.getElementById('fName').value && fFile.files[0]) document.getElementById('fName').value = fFile.files[0].name.replace(/\.zip$/i, '').toLowerCase().replace(/[^a-z0-9]+/g, '-'); }
19
+
20
+ function gotoStep(n) {
21
+ for (const i of [1, 2, 3]) {
22
+ document.getElementById('step' + i).classList.toggle('hidden', i !== n);
23
+ document.getElementById('d' + i).classList.toggle('step-on', i <= n);
24
+ }
25
+ }
26
+ function resetWizard() { gotoStep(1); document.getElementById('deployResult').innerHTML = ''; }
27
+
28
+ async function api(u, o) { const r = await fetch(u, o); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || 'Error ' + r.status); return j; }
29
+
30
+ async function loadAll() {
31
+ SYS = await api('/api/system');
32
+ document.getElementById('httpBadge').textContent = `🌐 HTTP :${SYS.httpPort} (testing)`;
33
+ document.getElementById('httpsBadge').textContent = SYS.tlsReady ? `🔒 HTTPS :${SYS.httpsPort} aktif` : '🔓 HTTPS belum aktif';
34
+ document.getElementById('srvInfo').innerHTML = `${SYS.os}<br>Node ${SYS.node}<br>HTTP :${SYS.httpPort} • HTTPS :${SYS.httpsPort}`;
35
+ document.getElementById('sys').innerHTML = `<div class="grid md:grid-cols-2 gap-2 text-sm">
36
+ <div>OS: <b>${SYS.os}</b></div><div>Node: <b>${SYS.node}</b></div>
37
+ <div>CPU: <b>${SYS.cpus} core</b></div><div>RAM: <b>${SYS.freeMemMB}/${SYS.totalMemMB} MB free</b></div>
38
+ <div>HTTP: <b class="text-emerald-300">http://&lt;IP&gt;:${SYS.httpPort}</b></div>
39
+ <div>HTTPS: <b class="text-sky-300">${SYS.tlsReady ? 'https://<IP>:' + SYS.httpsPort + ' (' + SYS.tlsInfo + ')' : 'belum aktif'}</b></div>
40
+ <div>Saran port app: <b>${SYS.suggestPort}</b></div><div>Riwayat release: <b>${SYS.maxReleases}x (rollback)</b></div></div>`;
41
+ APPS = await api('/api/apps');
42
+ renderStats(); renderApps();
43
+ }
44
+ function renderStats() {
45
+ const run = APPS.filter(a => a.running).length;
46
+ document.getElementById('stats').innerHTML = `
47
+ <div class="card p-4"><div class="text-xs text-slate-400">Total Aplikasi</div><div class="text-3xl font-black">${APPS.length}</div></div>
48
+ <div class="card p-4"><div class="text-xs text-slate-400">Running</div><div class="text-3xl font-black text-emerald-400">${run}</div></div>
49
+ <div class="card p-4"><div class="text-xs text-slate-400">CPU / RAM free</div><div class="text-xl font-black">${SYS.cpus} core • ${SYS.freeMemMB}MB</div></div>
50
+ <div class="card p-4"><div class="text-xs text-slate-400">Akses Panel</div><div class="text-sm font-bold text-sky-300">HTTP :${SYS.httpPort} testing<br>HTTPS :${SYS.httpsPort} produksi</div></div>`;
51
+ }
52
+ function statusBadge(a) {
53
+ if (a.status === 'running') return '<span class="badge bg-emerald-500/20 text-emerald-300">● RUNNING</span>';
54
+ if (a.status === 'running-unhealthy') return '<span class="badge bg-amber-500/20 text-amber-300">● RUNNING (unhealthy)</span>';
55
+ if (a.status === 'building') return '<span class="badge bg-sky-500/20 text-sky-300">◌ BUILDING</span>';
56
+ return '<span class="badge bg-slate-600/40 text-slate-300">■ STOPPED</span>';
57
+ }
58
+ function renderApps() {
59
+ const q = (document.getElementById('q').value || '').toLowerCase();
60
+ const el = document.getElementById('apps');
61
+ const list = APPS.filter(a => a.name.toLowerCase().includes(q));
62
+ el.innerHTML = list.length ? '' : '<div class="text-slate-400 text-sm">Belum ada aplikasi. Klik <b>+ Deploy Baru</b>.</div>';
63
+ for (const a of list) {
64
+ const d = document.createElement('div');
65
+ d.className = 'card p-4';
66
+ const isW = (a.serviceType === 'worker' || a.manifest?.serviceType === 'worker' || !a.port);
67
+ d.innerHTML = `<div class="flex items-center gap-2 mb-1"><b>${a.name}</b>${statusBadge(a)}
68
+ <span class="text-xs text-slate-400 ml-auto">${isW ? '⚙️ worker • tanpa port' : ':' + a.port} • ${a.manifest?.framework || '-'}</span></div>
69
+ <div class="text-xs text-slate-400 mb-2">release <code>${a.currentRelease || '-'}</code> • ${(a.releases || []).length} versi • pid ${a.pid || '-'}</div>
70
+ <div class="flex flex-wrap gap-1.5 text-xs">
71
+ <button class="btn bg-emerald-600 text-xs" onclick="act('${a.id}','start')">▶ Start</button>
72
+ <button class="btn bg-slate-700 text-xs" onclick="act('${a.id}','stop')">■ Stop</button>
73
+ <button class="btn bg-sky-600 text-xs" onclick="act('${a.id}','restart')">↻ Restart</button>
74
+ <button class="btn bg-slate-700 text-xs" onclick="openLogs('${a.id}')">📜 Log</button>
75
+ <button class="btn bg-violet-600 text-xs" onclick="openMan('${a.id}')">📄 Manifest & Rollback</button>
76
+ <button class="btn bg-red-700 text-xs" onclick="del('${a.id}')">🗑</button>
77
+ </div>
78
+ <a class="text-xs text-sky-300 mt-2 inline-block" target="_blank" ${isW ? 'style="display:none"' : ''} href="http://${location.hostname}:${a.port}">↗ buka http://${location.hostname}:${a.port}</a>${isW ? '<div class="text-xs text-slate-400 mt-2">⚙️ Background service — cek output di 📜 Log</div>' : ''}`;
79
+ el.appendChild(d);
80
+ }
81
+ }
82
+ async function act(id, k) { try { await api(`/api/apps/${id}/${k}`, { method: 'POST' }); await loadAll(); } catch (e) { alert(e.message); } }
83
+ async function del(id) { if (!confirm('Hapus aplikasi + semua release?')) return; await api('/api/apps/' + id, { method: 'DELETE' }); await loadAll(); }
84
+
85
+ async function analyze() {
86
+ if (!fFile.files[0]) return alert('Pilih file .zip dulu');
87
+ const svc = document.getElementById('fType').value || 'web';
88
+ const fd = new FormData();
89
+ fd.append('file', fFile.files[0]);
90
+ fd.append('name', document.getElementById('fName').value || 'my-app');
91
+ fd.append('serviceType', svc);
92
+ try {
93
+ const r = await (await fetch('/api/apps/analyze', { method: 'POST', body: fd })).json();
94
+ if (r.error) throw new Error(r.error);
95
+ analyzedManifest = r.manifest;
96
+ if (svc === 'worker') { analyzedManifest.serviceType = 'worker'; analyzedManifest.port = null; if (analyzedManifest.env) delete analyzedManifest.env.PORT; }
97
+ else {
98
+ if (document.getElementById('fPort').value) analyzedManifest.port = parseInt(document.getElementById('fPort').value, 10);
99
+ else document.getElementById('fPort').value = analyzedManifest.port;
100
+ analyzedManifest.env = { ...(analyzedManifest.env || {}), PORT: String(analyzedManifest.port) };
101
+ }
102
+ document.getElementById('fManifest').value = JSON.stringify(analyzedManifest, null, 2);
103
+ const cl = r.analysis.checks.map(c => `<div class="text-sm"> ${c.ok ? '<span class="check-ok">✅</span>' : (c.required ? '<span class="check-bad">❌</span>' : '<span>⚠️</span>')} <b>${c.label}</b><div class="text-xs text-slate-400 ml-6">${c.hint}</div></div>`).join('');
104
+ document.getElementById('checklist').innerHTML = `<div class="card p-3" style="background:#0b1220">
105
+ <div class="font-bold mb-1">Framework: <span class="text-sky-300">${r.analysis.framework}</span> • ${r.analysis.canDeploy ? '<span class="check-ok">Siap deploy ✅</span>' : '<span class="check-bad">Belum bisa deploy ❌ — perbaiki checklist merah</span>'}</div>${cl}
106
+ <div class="text-xs mt-2"><a class="text-sky-300" href="/docs.html#syarat">📖 Lihat syarat lengkap di Dokumentasi</a></div></div>`;
107
+ gotoStep(2);
108
+ } catch (e) { alert(e.message); }
109
+ }
110
+ async function doDeploy() {
111
+ let manifest = null, env = {};
112
+ try { manifest = JSON.parse(document.getElementById('fManifest').value); } catch { return alert('Manifest bukan JSON valid'); }
113
+ try { env = JSON.parse(document.getElementById('fEnv').value || '{}'); } catch { return alert('Env bukan JSON valid'); }
114
+ const fd = new FormData();
115
+ fd.append('file', fFile.files[0]);
116
+ fd.append('name', document.getElementById('fName').value || manifest.name || 'my-app');
117
+ fd.append('serviceType', manifest.serviceType || 'web');
118
+ if (manifest.port) fd.append('port', manifest.port);
119
+ fd.append('manifest', JSON.stringify(manifest));
120
+ fd.append('env', JSON.stringify(env));
121
+ gotoStep(3);
122
+ document.getElementById('deployResult').innerHTML = '<div class="text-sky-300">⏳ Installing → building → starting… (bisa 1–5 menit)</div>';
123
+ try {
124
+ const r = await (await fetch('/api/apps/deploy', { method: 'POST', body: fd })).json();
125
+ if (r.error) throw new Error(r.error + (r.analysis ? '\n\nChecklist:\n' + r.analysis.checks.map(c => (c.ok ? '✅ ' : '❌ ') + c.label + ' — ' + c.hint).join('\n') : ''));
126
+ document.getElementById('deployResult').innerHTML = `<div class="card p-4" style="background:#052e16;border-color:#16a34a">
127
+ <div class="font-black text-emerald-300 text-lg">✅ Deploy sukses!</div>
128
+ <div class="text-sm">App <b>${r.name}</b> ${r.port ? `live di <b>http://&lt;IP&gt;:${r.port}</b>` : 'running sebagai <b>worker (tanpa port)</b> — cek 📜 Log'} (release <code>${r.currentRelease}</code>)</div>
129
+ <div class="text-xs text-slate-300 mt-1">Rollback tersedia: setiap redeploy menyimpan ${SYS.maxReleases} versi. Lihat Manifest & Rollback.</div></div>`;
130
+ await loadAll();
131
+ } catch (e) { document.getElementById('deployResult').innerHTML = `<div class="card p-4" style="background:#3f1d1d"><b class="text-red-300">❌ Deploy gagal</b><pre class="text-xs whitespace-pre-wrap">${e.message}</pre><a class="text-sky-300 text-xs" href="/docs.html#trouble">📖 Troubleshooting</a></div>`; }
132
+ }
133
+
134
+ // logs & manifest
135
+ async function openLogs(id) {
136
+ curId = id; const a = APPS.find(x => x.id === id);
137
+ document.getElementById('logTitle').textContent = '📜 Log — ' + (a ? a.name : id);
138
+ document.getElementById('logModal').showModal(); refreshLogs();
139
+ }
140
+ async function refreshLogs() {
141
+ const r = await api(`/api/apps/${curId}/logs?lines=300`);
142
+ document.getElementById('logBody').textContent = (r.mem ? '── live ──\n' + r.mem : '') + '\n── file ──\n' + r.file;
143
+ document.getElementById('logBody').scrollTop = 1e9;
144
+ }
145
+ function showTab(t) {
146
+ document.getElementById('tab-man').classList.toggle('hidden', t !== 'man');
147
+ document.getElementById('tab-rel').classList.toggle('hidden', t !== 'rel');
148
+ }
149
+ async function openMan(id) {
150
+ curId = id; showTab('man');
151
+ const m = await api(`/api/apps/${id}/manifest`);
152
+ document.getElementById('manEdit').value = JSON.stringify(m, null, 2);
153
+ const rels = await api(`/api/apps/${id}/releases`);
154
+ document.getElementById('relList').innerHTML = rels.map(r => `<div class="flex items-center gap-2 text-sm py-1 border-b border-slate-800">
155
+ <code class="text-xs">${r.release}</code>${r.current ? '<span class="badge bg-emerald-500/20 text-emerald-300">AKTIF</span>' : ''}
156
+ <span class="text-xs text-slate-400">${r.manifest?.framework || ''}</span>
157
+ ${r.current ? '' : `<button class="btn bg-amber-500 text-slate-950 text-xs ml-auto" onclick="doRollback('${r.release}')">↩ Rollback ke sini</button>`}</div>`).join('')
158
+ + `<button class="btn bg-amber-500 text-slate-950 text-xs mt-3" onclick="doRollback('')">↩ Rollback ke versi sebelumnya</button>
159
+ <div class="text-xs text-slate-400 mt-2">Rollback = stop → pindah release → start. Tanpa build ulang, jadi cepat. Maks ${SYS.maxReleases} versi disimpan.</div>`;
160
+ document.getElementById('manModal').showModal();
161
+ }
162
+ async function saveManifest() {
163
+ try { const m = JSON.parse(document.getElementById('manEdit').value); await api(`/api/apps/${curId}/manifest`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(m) }); alert('Manifest tersimpan. Restart app agar berlaku.'); await loadAll(); }
164
+ catch (e) { alert(e.message); }
165
+ }
166
+ function redeployUp() { document.getElementById('reFile').click(); }
167
+ document.getElementById('reFile').onchange = async (e) => {
168
+ const f = e.target.files[0]; if (!f) return;
169
+ if (!confirm('Redeploy ' + f.name + '? Versi lama tetap tersimpan untuk rollback.')) return;
170
+ const fd = new FormData(); fd.append('file', f);
171
+ try {
172
+ const r = await (await fetch(`/api/apps/${curId}/redeploy`, { method: 'POST', body: fd })).json();
173
+ if (r.error) throw new Error(r.error);
174
+ alert('✅ Redeploy sukses → ' + r.currentRelease); document.getElementById('manModal').close(); await loadAll();
175
+ } catch (err) { alert('❌ ' + err.message); }
176
+ };
177
+ async function doRollback(rel) {
178
+ if (!confirm('Rollback ke ' + (rel || 'versi sebelumnya') + '?')) return;
179
+ try { const r = await api(`/api/apps/${curId}/rollback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ release: rel || undefined }) }); alert('✅ Rollback sukses → ' + r.currentRelease); document.getElementById('manModal').close(); await loadAll(); }
180
+ catch (e) { alert(e.message); }
181
+ }
182
+ async function genTLS() {
183
+ try { const r = await api('/api/tls/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cn: location.hostname }) }); document.getElementById('tlsMsg').textContent = '✅ ' + r.note; }
184
+ catch (e) { document.getElementById('tlsMsg').textContent = '❌ ' + e.message; }
185
+ }
186
+ loadAll();
@@ -0,0 +1,188 @@
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <title>Dokumentasi DeployMaster</title>
6
+ <script src="https://cdn.tailwindcss.com"></script>
7
+ <style>body{background:#0b1220;color:#e2e8f0;font-family:Inter,system-ui,sans-serif} .card{background:#111c33;border:1px solid #1e3a5f;border-radius:14px} code{background:#020617;padding:2px 6px;border-radius:6px;color:#7dd3fc} pre{background:#020617;border:1px solid #1e293b;border-radius:12px;padding:12px;overflow:auto;font-size:12px} a{color:#38bdf8} h2{font-size:20px;font-weight:800;margin:28px 0 8px} h3{font-weight:700;margin:14px 0 6px} li{margin:4px 0}</style>
8
+ </head>
9
+ <body class="max-w-4xl mx-auto p-6">
10
+ <a href="/">← Kembali ke Panel</a>
11
+ <h1 class="text-3xl font-black mt-2">📖 Dokumentasi DeployMaster</h1>
12
+ <p class="text-slate-400">Self-hosted deployment service untuk <b>Node.js & Next.js</b> di PC client (on-premise / lokal). Jalan di <b>Windows & Ubuntu</b>. Upload project via <b>HTTP (testing)</b> & <b>HTTPS (produksi)</b>. Manifest otomatis + rollback.</p>
13
+
14
+ <div class="card p-4 mt-4 text-sm">
15
+ <b>Daftar isi:</b> <a href="#arsitektur">Arsitektur</a> • <a href="#syarat">Syarat Project</a> • <a href="#zip">Cara ZIP & Upload</a> • <a href="#manifest">Manifest</a> • <a href="#rollback">Rollback</a> • <a href="#https">HTTP/HTTPS</a> • <a href="#install">Install Win/Ubuntu</a> • <a href="#trouble">Troubleshooting</a>
16
+ </div>
17
+
18
+ <h2 id="arsitektur">1. Arsitektur — kenapa bisa Windows & Ubuntu?</h2>
19
+ <div class="card p-4 text-sm">
20
+ Panel ini <b>100% Node.js</b>, jadi cross-platform. Satu folder <code>DEPLOYMENT-MASTER</code> dijalankan di PC client (Windows / Ubuntu), lalu browser akses panelnya.<br><br>
21
+ <pre>PC Client (Windows / Ubuntu)
22
+ ├── Panel DeployMaster :9000 (HTTP, testing) + :9443 (HTTPS)
23
+ ├── apps/&lt;nama&gt;/releases/&lt;tanggal&gt;/ ← 5 versi terakhir (rollback)
24
+ ├── apps/&lt;nama&gt;/current.txt ← penunjuk versi aktif
25
+ └── tiap app jalan di port sendiri :3001, :3002, ...</pre>
26
+ <ul class="list-disc ml-5">
27
+ <li><b>Windows:</b> proses via <code>cmd.exe</code>, kill via <code>taskkill /T /F</code>.</li>
28
+ <li><b>Ubuntu:</b> proses via <code>sh</code>, kill via group signal.</li>
29
+ <li>Tidak butuh Docker. Hanya butuh <b>Node.js 18+</b>.</li>
30
+ </ul></div>
31
+
32
+ <h2 id="syarat">2. Apa yang dibutuhkan agar project bisa deploy?</h2>
33
+ <div class="card p-4 text-sm">
34
+ <p>Panel otomatis cek ini saat <b>Analisis</b>. Yang <b>❌ merah = wajib</b>, ⚠️ = opsional.</p>
35
+ <h3>✅ Wajib (Web)</h3>
36
+ <ul class="list-disc ml-5">
37
+ <li><b>package.json di root ZIP</b> — berisi <code>name</code>, <code>dependencies</code>, <code>scripts</code>.</li>
38
+ <li><b>scripts.start</b> — perintah jalan produksi. Contoh:
39
+ <pre>"scripts": {
40
+ "start": "next start", // Next.js
41
+ "start": "node server.js", // Express polos
42
+ "start": "node dist/main.js"// NestJS
43
+ }</pre></li>
44
+ <li><b>scripts.build (untuk Next/Nuxt/Vite/Nest)</b> — contoh <code>"build": "next build"</code>. Node polos boleh kosong.</li>
45
+ <li><b>Mendukung env PORT</b> — Panel meng-inject <code>PORT=3001,3002,…</code> tiap app.
46
+ <ul class="list-disc ml-5">
47
+ <li>Next.js/Nuxt: <b>otomatis</b> (tidak perlu ubah apa-apa).</li>
48
+ <li>Express: wajib <code>app.listen(process.env.PORT || 3000)</code>. ❌ Jangan hardcode <code>app.listen(3000)</code> saja.</li>
49
+ </ul></li>
50
+ </ul>
51
+ <h3>⚙️ Khusus Worker (background, tanpa port)</h3>
52
+ <ul class="list-disc ml-5">
53
+ <li>Pilih tipe <b>Worker</b> saat deploy — <b>port tidak wajib</b>, healthcheck = proses hidup (tanpa HTTP).</li>
54
+ <li>Syarat hanya: <code>package.json</code> + <code>scripts.start</code> yang jalan terus (cth. <code>"start": "node worker.js"</code>). Contoh worker olah data:</li>
55
+ </ul>
56
+ <pre>const { setTimeout: sleep } = require('timers/promises');
57
+ (async () => { while (true) { console.log('proses data...', new Date().toISOString()); await sleep(5000); } })();</pre>
58
+ <h3>⚠️ Opsional (disarankan)</h3>
59
+ <ul class="list-disc ml-5">
60
+ <li><b>Lockfile</b> (<code>package-lock.json</code> / <code>pnpm-lock.yaml</code> / <code>yarn.lock</code>) agar install reproducible.</li>
61
+ <li><b>engines.node</b> — contoh <code>"engines": {"node": "20.x"}</code> sebagai penanda versi.</li>
62
+ <li><b>next.config.*</b> — boleh tidak ada (pakai default).</li>
63
+ <li><b>.env.example</b> — sebagai referensi env, Panel akan meminta isi env saat deploy.</li>
64
+ </ul>
65
+ <h3>Contoh minimal Express (server.js)</h3>
66
+ <pre>const express = require('express');
67
+ const app = express();
68
+ app.get('/', (req,res) => res.send('ok'));
69
+ const PORT = process.env.PORT || 3000;
70
+ app.listen(PORT, () => console.log('live :' + PORT));</pre>
71
+ <h3>Contoh package.json Next.js</h3>
72
+ <pre>{
73
+ "name": "toko-online",
74
+ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" },
75
+ "dependencies": { "next": "14.2.0", "react": "18.3.1", "react-dom": "18.3.1" }
76
+ }</pre>
77
+ </div>
78
+
79
+ <h2 id="zip">3. Cara ZIP & upload (HTTP dulu, HTTPS nanti)</h2>
80
+ <div class="card p-4 text-sm">
81
+ <ol class="list-decimal ml-5">
82
+ <li>Di folder project, pastikan <code>package.json</code> terlihat langsung (bukan di dalam subfolder).</li>
83
+ <li>ZIP <b>isi folder</b>, bukan foldernya. Di Windows: blok semua file → klik kanan → Send to → Compressed. Di Ubuntu: <code>zip -r app.zip . -x "node_modules/*" ".next/*" ".git/*"</code>.</li>
84
+ <li>Jangan sertakan <code>node_modules</code> & <code>.next</code> (Panel akan build ulang).</li>
85
+ <li>Buka panel: testing via <code>http://IP-PC:9000</code> → <b>Deploy Baru</b> → upload ZIP → <b>Analisis</b> → <b>Deploy</b>.</li>
86
+ <li>Produksi: sama persis, tapi buka <code>https://IP-PC:9443</code>.</li>
87
+ </ol>
88
+ <pre>✅ BENAR (buka ZIP langsung ketemu package.json):
89
+ app.zip
90
+ ├── package.json
91
+ ├── server.js / src / app / pages
92
+ └── next.config.js (opsional)
93
+
94
+ ❌ SALAH (double folder — panel tetap toleransi tapi jangan dibiasakan):
95
+ app.zip
96
+ └── my-app/package.json ...</pre>
97
+ </div>
98
+
99
+ <h2 id="manifest">4. Manifest — otomatis dibuatkan</h2>
100
+ <div class="card p-4 text-sm">
101
+ <p>Setiap deploy menghasilkan <code>deployment-manifest.json</code> di folder release. Bisa diedit di Panel → <b>Manifest & Rollback</b>.</p>
102
+ <pre>{
103
+ "version": 1,
104
+ "name": "toko-online",
105
+ "framework": "nextjs",
106
+ "nodeVersion": "20.x",
107
+ "port": 3001,
108
+ "installCommand": "npm install",
109
+ "buildCommand": "npm run build",
110
+ "startCommand": "npm start",
111
+ "healthCheck": { "path": "/", "timeoutSec": 30 },
112
+ "env": { "NODE_ENV": "production", "PORT": "3001" },
113
+ "release": "2026-10-15T10-00-00",
114
+ "createdAt": "..."
115
+ }</pre>
116
+ <ul class="list-disc ml-5">
117
+ <li><b>framework</b> terdeteksi dari dependencies: <code>next</code>→nextjs, <code>@nestjs/core</code>→nestjs, <code>express/fastify/koa</code>→nodejs-express, <code>vite</code>→vite.</li>
118
+ <li><b>installCommand</b> menyesuaikan <code>packageManager</code> (npm/pnpm/yarn).</li>
119
+ <li>Edit manifest → <b>Simpan</b> → <b>Restart</b> agar berlaku.</li>
120
+ </ul></div>
121
+
122
+ <h2 id="rollback">5. Rollback build (jangan lupa!)</h2>
123
+ <div class="card p-4 text-sm">
124
+ <ul class="list-disc ml-5">
125
+ <li>Setiap <b>Deploy / Redeploy</b> = release baru <code>releases/&lt;timestamp&gt;</code>. Maks <b>5 versi</b> disimpan (terbaru).</li>
126
+ <li><b>Redeploy gagal → otomatis rollback</b> ke versi sebelumnya (tanpa klik apa-apa).</li>
127
+ <li><b>Rollback manual:</b> Panel → kartu app → <b>📄 Manifest & Rollback → tab Rollback</b> → pilih versi → <b>↩ Rollback ke sini</b>. Prosesnya stop → pindah pointer <code>current.txt</code> → start. Cepat karena tanpa build ulang.</li>
128
+ <li>API: <code>POST /api/apps/:id/rollback {"release": "2026-..."}</code> (kosongkan = versi sebelumnya). Daftar: <code>GET /api/apps/:id/releases</code>.</li>
129
+ </ul>
130
+ <pre>releases/
131
+ ├── 2026-10-15T10-00-00/ ← v1 (bisa rollback ke sini)
132
+ └── 2026-10-15T11-20-00/ ← v2 AKTIF (current.txt menunjuk sini)</pre>
133
+ </div>
134
+
135
+ <h2 id="https">6. HTTP untuk testing, HTTPS untuk produksi</h2>
136
+ <div class="card p-4 text-sm">
137
+ <ul class="list-disc ml-5">
138
+ <li><b>HTTP :9000</b> selalu aktif → untuk testing/upload lokal.</li>
139
+ <li><b>HTTPS :9443</b> aktif jika ada cert. Tanpa cert asli, Panel auto-generate <b>self-signed</b> (browser warning merah = wajar untuk testing).</li>
140
+ <li>Panel → <b>Sistem & HTTPS → ⚡ Generate Self-Signed</b>, lalu restart panel, akses <code>https://IP:9443</code>.</li>
141
+ <li>Cert asli (produksi): isi <code>.env</code> → <code>TLS_CERT_PATH</code> & <code>TLS_KEY_PATH</code> (mis. dari Let's Encrypt / kantor), restart.</li>
142
+ </ul>
143
+ <pre># .env
144
+ HTTPS_ENABLED=true
145
+ HTTPS_PORT=9443
146
+ AUTO_SELF_SIGNED=true # false bila sudah punya cert asli
147
+ TLS_CERT_PATH=./certs/cert.pem
148
+ TLS_KEY_PATH=./certs/key.pem</pre>
149
+ </div>
150
+
151
+ <h2 id="install">7. Install di Windows & Ubuntu</h2>
152
+ <div class="card p-4 text-sm">
153
+ <h3>Butuh: Node.js 18+ saja</h3>
154
+ <pre># Ubuntu
155
+ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
156
+ sudo apt install -y nodejs zip
157
+ git clone &lt;repo&gt; DEPLOYMENT-MASTER && cd DEPLOYMENT-MASTER
158
+ cp .env.example .env
159
+ npm install
160
+ npm start
161
+ # akses http://IP:9000
162
+
163
+ # Windows (PowerShell)
164
+ node --version # pastikan 18+
165
+ cd DEPLOYMENT-MASTER
166
+ copy .env.example .env
167
+ npm install
168
+ npm start
169
+ # akses http://localhost:9000</pre>
170
+ <h3>Autostart (opsional)</h3>
171
+ <ul class="list-disc ml-5">
172
+ <li>Ubuntu: buat systemd service <code>deploymaster.service</code> → <code>ExecStart=/usr/bin/node /opt/DEPLOYMENT-MASTER/src/server.js</code>.</li>
173
+ <li>Windows: Task Scheduler → jalankan <code>npm start</code> saat logon, atau bungkus dengan NSSM/pm2-windows.</li>
174
+ </ul></div>
175
+
176
+ <h2 id="trouble">8. Troubleshooting</h2>
177
+ <div class="card p-4 text-sm"><ul class="list-disc ml-5">
178
+ <li><b>"Syarat deploy belum terpenuhi"</b> → baca checklist merah, perbaiki <code>package.json</code>, ZIP ulang.</li>
179
+ <li><b>"Proses langsung exit"</b> → cek <b>📜 Log</b>. Umumnya salah <code>startCommand</code> atau crash karena env kurang.</li>
180
+ <li><b>"Port sudah dipakai"</b> → ganti port atau stop app lama.</li>
181
+ <li><b>"running-unhealthy"</b> → app jalan tapi <code>/</code> tidak balas 200 dalam 30 detik. Cek route <code>/</code> & <code>PORT</code>.</li>
182
+ <li><b>ZIP double folder</b> → ZIP isinya, bukan foldernya.</li>
183
+ <li><b>HTTPS warning merah</b> → wajar untuk self-signed. Klik Advanced → Proceed. Hilang bila pakai cert asli.</li>
184
+ </ul></div>
185
+
186
+ <p class="text-center text-slate-500 text-xs mt-8">DeployMaster v1.0 — Windows & Ubuntu • HTTP/HTTPS • Auto-manifest • Rollback</p>
187
+ </body>
188
+ </html>