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 +15 -0
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/bin/deploymaster.js +52 -0
- package/package.json +44 -4
- package/public/app.js +186 -0
- package/public/docs.html +188 -0
- package/public/index.html +152 -0
- package/scripts/install-ubuntu.sh +8 -0
- package/scripts/run-windows.bat +6 -0
- package/src/config.js +32 -0
- package/src/deployer.js +203 -0
- package/src/detector.js +126 -0
- package/src/proc.js +116 -0
- package/src/server.js +272 -0
- package/src/store.js +40 -0
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
|
-
#
|
|
1
|
+
# DeployMaster — Panel Deployment Node.js & Next.js (On-Premise, Windows & Ubuntu)
|
|
2
2
|
|
|
3
|
-
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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://<IP>:${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://<IP>:${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();
|
package/public/docs.html
ADDED
|
@@ -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/<nama>/releases/<tanggal>/ ← 5 versi terakhir (rollback)
|
|
24
|
+
├── apps/<nama>/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/<timestamp></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 <repo> 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>
|