saymon-syswatch-linux 1.0.0 → 1.0.2
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/bin/gen-cert.js +4 -3
- package/package.json +22 -3
- package/public/login.html +3 -6
- package/src/config.js +22 -7
- package/src/server.js +1 -4
- package/src/setup.js +65 -42
- package/syswatch.config.example.json +2 -2
package/bin/gen-cert.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// bin/gen-cert.js
|
|
4
4
|
// Generates a self-signed TLS cert for HTTPS mode.
|
|
5
5
|
// Uses openssl — must be installed on the system.
|
|
6
|
-
// Saves to: certs/key.pem +
|
|
6
|
+
// Saves to: <configDir>/certs/key.pem + cert.pem
|
|
7
7
|
// ─────────────────────────────────────────────────────
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
@@ -11,7 +11,8 @@ const { execSync } = require('child_process');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const { CONFIG_DIR } = require('../src/config');
|
|
15
|
+
const certsDir = path.join(CONFIG_DIR, 'certs');
|
|
15
16
|
|
|
16
17
|
function generateCert() {
|
|
17
18
|
if (!fs.existsSync(certsDir)) fs.mkdirSync(certsDir, { recursive: true });
|
|
@@ -33,7 +34,7 @@ function generateCert() {
|
|
|
33
34
|
-subj "/CN=syswatch/O=SysWatch/C=US"`,
|
|
34
35
|
{ stdio: 'pipe' }
|
|
35
36
|
);
|
|
36
|
-
console.log(
|
|
37
|
+
console.log(`✓ Certificates saved to ${certsDir}/`);
|
|
37
38
|
console.log(' Note: Browser will warn "Not secure" for self-signed certs — that is normal.');
|
|
38
39
|
console.log(' For production, replace with Let\'s Encrypt certs.');
|
|
39
40
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saymon-syswatch-linux",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Linux system monitor & learning dashboard — HTTP/HTTPS, journal logs, services, processes",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,9 +11,28 @@
|
|
|
11
11
|
"dev": "node src/server.js --dev",
|
|
12
12
|
"setup-https": "node bin/gen-cert.js"
|
|
13
13
|
},
|
|
14
|
-
"
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"src/",
|
|
17
|
+
"public/",
|
|
18
|
+
"syswatch.config.example.json",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"linux",
|
|
23
|
+
"monitor",
|
|
24
|
+
"systemd",
|
|
25
|
+
"journalctl",
|
|
26
|
+
"sysadmin",
|
|
27
|
+
"dashboard",
|
|
28
|
+
"learning"
|
|
29
|
+
],
|
|
15
30
|
"author": "Saymon",
|
|
16
31
|
"license": "MIT",
|
|
32
|
+
"preferGlobal": true,
|
|
33
|
+
"os": [
|
|
34
|
+
"linux"
|
|
35
|
+
],
|
|
17
36
|
"dependencies": {
|
|
18
37
|
"express": "^4.18.2",
|
|
19
38
|
"cors": "^2.8.5",
|
|
@@ -25,4 +44,4 @@
|
|
|
25
44
|
"engines": {
|
|
26
45
|
"node": ">=18.0.0"
|
|
27
46
|
}
|
|
28
|
-
}
|
|
47
|
+
}
|
package/public/login.html
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
onkeydown="if(event.key==='Enter')login()">
|
|
32
32
|
<button onclick="login()">Sign In →</button>
|
|
33
33
|
<div class="err" id="err">Invalid password. Try again.</div>
|
|
34
|
-
<div class="note"
|
|
35
|
-
|
|
36
|
-
To
|
|
34
|
+
<div class="note">
|
|
35
|
+
Enter the password you set during <code>syswatch --setup</code>.<br>
|
|
36
|
+
To re-configure, run <code>sudo syswatch --setup</code> in the terminal.
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
39
39
|
<script>
|
|
@@ -51,11 +51,8 @@ async function login() {
|
|
|
51
51
|
document.getElementById('pw').focus();
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
// check if auth is even enabled
|
|
55
54
|
fetch('/auth/status').then(r=>r.json()).then(d=>{
|
|
56
55
|
if (!d.authEnabled) window.location.href = '/';
|
|
57
|
-
if (d.passwordHash === null)
|
|
58
|
-
document.getElementById('note').style.display='block';
|
|
59
56
|
});
|
|
60
57
|
</script>
|
|
61
58
|
</body>
|
package/src/config.js
CHANGED
|
@@ -3,13 +3,26 @@
|
|
|
3
3
|
// Reads config from (priority order):
|
|
4
4
|
// 1. CLI arguments (--port 9000)
|
|
5
5
|
// 2. ENV variables (SYSWATCH_PORT=9000)
|
|
6
|
-
// 3.
|
|
6
|
+
// 3. Config file at fixed path (see CONFIG_PATH below)
|
|
7
7
|
// 4. Defaults below
|
|
8
8
|
// ─────────────────────────────────────────────────────
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// ── fixed config path (same regardless of cwd) ────
|
|
16
|
+
// root/sudo → /etc/syswatch/ non-root → ~/.config/syswatch/
|
|
17
|
+
function getConfigDir() {
|
|
18
|
+
try {
|
|
19
|
+
if (process.getuid && process.getuid() === 0) return '/etc/syswatch';
|
|
20
|
+
} catch {}
|
|
21
|
+
return path.join(os.homedir(), '.config', 'syswatch');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CONFIG_DIR = process.env.SYSWATCH_CONFIG_DIR || getConfigDir();
|
|
25
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'syswatch.config.json');
|
|
13
26
|
|
|
14
27
|
// ── parse CLI args ─────────────────────────────────
|
|
15
28
|
const argv = process.argv.slice(2);
|
|
@@ -21,10 +34,9 @@ const flag = (f) => argv.includes(f);
|
|
|
21
34
|
|
|
22
35
|
// ── load optional config file ──────────────────────
|
|
23
36
|
let fileConf = {};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
catch (e) { console.warn('[syswatch] Warning: invalid syswatch.config.json'); }
|
|
37
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
38
|
+
try { fileConf = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); }
|
|
39
|
+
catch (e) { console.warn('[syswatch] Warning: invalid syswatch.config.json at ' + CONFIG_PATH); }
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
// ── merge config ───────────────────────────────────
|
|
@@ -46,8 +58,8 @@ const config = {
|
|
|
46
58
|
sessionHours: fileConf.sessionHours || 8,
|
|
47
59
|
|
|
48
60
|
// TLS cert paths (used when httpsEnabled)
|
|
49
|
-
certPath: fileConf.certPath || path.join(
|
|
50
|
-
keyPath: fileConf.keyPath || path.join(
|
|
61
|
+
certPath: fileConf.certPath || path.join(CONFIG_DIR, 'certs', 'cert.pem'),
|
|
62
|
+
keyPath: fileConf.keyPath || path.join(CONFIG_DIR, 'certs', 'key.pem'),
|
|
51
63
|
|
|
52
64
|
// Features
|
|
53
65
|
learnMode: fileConf.learnMode !== false, // default on
|
|
@@ -62,4 +74,7 @@ function _randomSecret() {
|
|
|
62
74
|
catch { return 'syswatch-change-this-secret'; }
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
config.CONFIG_PATH = CONFIG_PATH;
|
|
78
|
+
config.CONFIG_DIR = CONFIG_DIR;
|
|
79
|
+
|
|
65
80
|
module.exports = config;
|
package/src/server.js
CHANGED
|
@@ -98,12 +98,9 @@ function banner() {
|
|
|
98
98
|
function afterBanner() {
|
|
99
99
|
console.log(`
|
|
100
100
|
📖 Learn mode: ${config.learnMode ? 'ON (toggle in UI)' : 'OFF'}
|
|
101
|
-
🔐 Auth: ${config.authEnabled ? 'ON
|
|
101
|
+
🔐 Auth: ${config.authEnabled ? 'ON' : 'OFF (--no-auth)'}
|
|
102
102
|
🖥 Host: ${config.host}
|
|
103
103
|
`);
|
|
104
|
-
if (config.authEnabled && !config.passwordHash) {
|
|
105
|
-
console.log(' ℹ No password set yet — you will create one on first login.\n');
|
|
106
|
-
}
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
function displayHost() {
|
package/src/setup.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────
|
|
2
2
|
// src/setup.js
|
|
3
3
|
// First-run interactive setup wizard.
|
|
4
|
-
// Runs automatically if
|
|
5
|
-
// Also callable via:
|
|
4
|
+
// Runs automatically if config is missing.
|
|
5
|
+
// Also callable via: syswatch --setup
|
|
6
6
|
// ─────────────────────────────────────────────────────
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
@@ -10,35 +10,57 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const readline = require('readline');
|
|
12
12
|
const crypto = require('crypto');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// ── config path (mirrors src/config.js logic) ─────
|
|
16
|
+
function getConfigDir() {
|
|
17
|
+
try {
|
|
18
|
+
if (process.getuid && process.getuid() === 0) return '/etc/syswatch';
|
|
19
|
+
} catch {}
|
|
20
|
+
return path.join(os.homedir(), '.config', 'syswatch');
|
|
21
|
+
}
|
|
13
22
|
|
|
14
|
-
const
|
|
23
|
+
const CONFIG_DIR = process.env.SYSWATCH_CONFIG_DIR || getConfigDir();
|
|
24
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'syswatch.config.json');
|
|
15
25
|
|
|
16
26
|
// ── helpers ───────────────────────────────────────
|
|
17
27
|
function ask(rl, question) {
|
|
18
28
|
return new Promise(resolve => rl.question(question, resolve));
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
// Hide password input — uses raw stdin mode WITHOUT creating a readline interface
|
|
32
|
+
// so there is no conflict with the rl instance used for other questions.
|
|
21
33
|
function askHidden(question) {
|
|
22
|
-
// hide password input on linux/mac terminals
|
|
23
34
|
return new Promise(resolve => {
|
|
24
35
|
process.stdout.write(question);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
|
|
37
|
+
const isTTY = process.stdin.isTTY;
|
|
38
|
+
if (isTTY) {
|
|
39
|
+
try { process.stdin.setRawMode(true); } catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
let input = '';
|
|
29
|
-
|
|
30
|
-
process.stdin.setEncoding('utf8');
|
|
43
|
+
|
|
31
44
|
const onData = (ch) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
const s = ch.toString();
|
|
46
|
+
|
|
47
|
+
if (s === '\n' || s === '\r' || s === '\u0004') {
|
|
48
|
+
// Enter / Ctrl-D → done
|
|
49
|
+
if (isTTY) {
|
|
50
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
51
|
+
}
|
|
36
52
|
process.stdin.removeListener('data', onData);
|
|
53
|
+
process.stdin.pause();
|
|
37
54
|
process.stdout.write('\n');
|
|
38
55
|
resolve(input);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
|
|
57
|
+
} else if (s === '\u0003') {
|
|
58
|
+
// Ctrl-C → exit
|
|
59
|
+
process.stdout.write('\n');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
|
|
62
|
+
} else if (s === '\u007f' || s === '\b') {
|
|
63
|
+
// Backspace
|
|
42
64
|
if (input.length > 0) {
|
|
43
65
|
input = input.slice(0, -1);
|
|
44
66
|
process.stdout.clearLine(0);
|
|
@@ -46,10 +68,13 @@ function askHidden(question) {
|
|
|
46
68
|
process.stdout.write(question + '*'.repeat(input.length));
|
|
47
69
|
}
|
|
48
70
|
} else {
|
|
49
|
-
input +=
|
|
71
|
+
input += s;
|
|
50
72
|
process.stdout.write('*');
|
|
51
73
|
}
|
|
52
74
|
};
|
|
75
|
+
|
|
76
|
+
process.stdin.resume();
|
|
77
|
+
process.stdin.setEncoding('utf8');
|
|
53
78
|
process.stdin.on('data', onData);
|
|
54
79
|
});
|
|
55
80
|
}
|
|
@@ -86,27 +111,20 @@ async function run(autoMode = false) {
|
|
|
86
111
|
]);
|
|
87
112
|
console.log();
|
|
88
113
|
|
|
89
|
-
const rl = readline.createInterface({
|
|
90
|
-
input: process.stdin,
|
|
91
|
-
output: process.stdout,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
114
|
const config = {};
|
|
95
115
|
|
|
96
|
-
// ── Step 1: Password
|
|
116
|
+
// ── Step 1: Password (raw stdin — no readline yet) ─
|
|
97
117
|
print('Step 1/4 — Set your login password', 'bold');
|
|
98
118
|
print(' (This protects your dashboard from others on the network)\n', 'dim');
|
|
99
119
|
|
|
100
120
|
let password = '';
|
|
101
|
-
let confirmed = '';
|
|
102
|
-
|
|
103
121
|
while (true) {
|
|
104
|
-
password
|
|
122
|
+
password = await askHidden(' Enter password: ');
|
|
105
123
|
if (password.length < 4) {
|
|
106
124
|
print(' ✗ Password must be at least 4 characters. Try again.\n', 'red');
|
|
107
125
|
continue;
|
|
108
126
|
}
|
|
109
|
-
confirmed
|
|
127
|
+
const confirmed = await askHidden(' Confirm password: ');
|
|
110
128
|
if (password !== confirmed) {
|
|
111
129
|
print(' ✗ Passwords do not match. Try again.\n', 'red');
|
|
112
130
|
continue;
|
|
@@ -119,6 +137,12 @@ async function run(autoMode = false) {
|
|
|
119
137
|
config.passwordHash = await bcrypt.hash(password, 10);
|
|
120
138
|
print(' ✓ Password set!\n', 'green');
|
|
121
139
|
|
|
140
|
+
// ── readline for remaining non-secret questions ──
|
|
141
|
+
const rl = readline.createInterface({
|
|
142
|
+
input: process.stdin,
|
|
143
|
+
output: process.stdout,
|
|
144
|
+
});
|
|
145
|
+
|
|
122
146
|
// ── Step 2: Port ────────────────────────────────
|
|
123
147
|
print('Step 2/4 — HTTP Port', 'bold');
|
|
124
148
|
print(' Default is 8080. Press Enter to keep it.\n', 'dim');
|
|
@@ -147,15 +171,18 @@ async function run(autoMode = false) {
|
|
|
147
171
|
config.httpsPort = 8443;
|
|
148
172
|
print(` ✓ HTTPS: ${config.https ? 'Enabled on port 8443' : 'Disabled'}\n`, 'green');
|
|
149
173
|
|
|
150
|
-
|
|
151
|
-
config.jwtSecret = crypto.randomBytes(32).toString('hex');
|
|
174
|
+
config.jwtSecret = crypto.randomBytes(32).toString('hex');
|
|
152
175
|
config.sessionHours = 8;
|
|
153
|
-
config.learnMode
|
|
154
|
-
config.maxLogLines
|
|
176
|
+
config.learnMode = true;
|
|
177
|
+
config.maxLogLines = 200;
|
|
155
178
|
|
|
156
179
|
rl.close();
|
|
157
180
|
|
|
158
181
|
// ── Write config file ───────────────────────────
|
|
182
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
183
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
159
186
|
const finalConfig = {
|
|
160
187
|
_note: "Auto-generated by syswatch --setup. Do NOT commit this file to git.",
|
|
161
188
|
host: config.host,
|
|
@@ -177,23 +204,19 @@ async function run(autoMode = false) {
|
|
|
177
204
|
box([
|
|
178
205
|
'\x1b[1m\x1b[32m ✓ Setup complete!\x1b[0m',
|
|
179
206
|
'',
|
|
180
|
-
` Config saved →
|
|
207
|
+
` Config saved → ${CONFIG_PATH}`,
|
|
181
208
|
'',
|
|
182
209
|
` 🌐 http://${config.host === '0.0.0.0' ? 'YOUR_IP' : 'localhost'}:${config.port}`,
|
|
183
210
|
config.https ? ` 🔒 https://${config.host === '0.0.0.0' ? 'YOUR_IP' : 'localhost'}:${config.httpsPort}` : '',
|
|
184
211
|
'',
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'',
|
|
188
|
-
' Or if installed globally:',
|
|
189
|
-
' sudo syswatch',
|
|
212
|
+
' To start: sudo syswatch',
|
|
213
|
+
' Re-setup: sudo syswatch --setup',
|
|
190
214
|
].filter(l => l !== ''));
|
|
191
215
|
console.log();
|
|
192
216
|
|
|
193
217
|
if (autoMode) {
|
|
194
|
-
// auto-start after setup
|
|
195
218
|
print(' Starting SysWatch now...\n', 'cyan');
|
|
196
|
-
|
|
219
|
+
// server is already chained via .then(startServer) in server.js
|
|
197
220
|
} else {
|
|
198
221
|
process.exit(0);
|
|
199
222
|
}
|
|
@@ -202,10 +225,10 @@ async function run(autoMode = false) {
|
|
|
202
225
|
// ── check if config exists, if not → auto-run setup ─
|
|
203
226
|
function checkAndSetup() {
|
|
204
227
|
if (!fs.existsSync(CONFIG_PATH)) {
|
|
205
|
-
print(
|
|
206
|
-
return run(true);
|
|
228
|
+
print(`\n ⚠ No config found at ${CONFIG_PATH} — running first-time setup...\n`, 'yellow');
|
|
229
|
+
return run(true);
|
|
207
230
|
}
|
|
208
231
|
return Promise.resolve();
|
|
209
232
|
}
|
|
210
233
|
|
|
211
|
-
module.exports = { run, checkAndSetup };
|
|
234
|
+
module.exports = { run, checkAndSetup, CONFIG_PATH, CONFIG_DIR };
|