Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
Dedy Kurniawan
/
pxe-utbksvr
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 6c19868e
authored
Feb 25, 2026
by
Dedy Kurniawan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update
1 parent
e4019636
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
399 additions
and
15 deletions
README.md
app.js
public/css/style.css
routes/monitor.js
views/monitor.ejs
views/partials/nav.ejs
README.md
View file @
6c19868
...
...
@@ -4,9 +4,9 @@ Dashboard manajemen PXE Server untuk lab komputer, dibangun dengan Node.js + Exp
## Fitur
-
✅ Login dengan username & password
-
📝 Editor teks untuk file
`boot.
uefi
`
(dengan backup otomatis)
-
📝 Editor teks untuk file
`boot.
ipxe
`
(dengan backup otomatis)
-
📤 Upload/replace file:
`vmlinuz`
,
`initrd.img`
,
`filesystem.squashfs`
-
⚙️ Monitor & kontrol service:
tftpd-hpa, nginx, isc-dhcp-server, nfs-kernel-server
-
⚙️ Monitor & kontrol service:
dnsmaq, nginx, ipxe
-
🔄 Auto-refresh status service tiap 10 detik
## Struktur Direktori
...
...
@@ -51,18 +51,15 @@ npm install
### 3. Konfigurasi sudo (untuk kontrol service)
Tambahkan ke
`/etc/sudoers`
(gunakan
`visudo`
):
```
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start
tftpd-hpa
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop
tftpd-hpa
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart
tftpd-hpa
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start
dnsmaq
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop
dnsmaq
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart
dnsmaq
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start nginx
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop nginx
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start isc-dhcp-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop isc-dhcp-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart isc-dhcp-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start nfs-kernel-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop nfs-kernel-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart nfs-kernel-server
www-data ALL=(ALL) NOPASSWD: /bin/systemctl start ipxe
www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop ipxe
www-data ALL=(ALL) NOPASSWD: /bin/systemctl restart ipxe
```
### 4. Pastikan direktori PXE ada
...
...
@@ -96,7 +93,7 @@ Buka browser: **http://localhost:3000**
## Environment Variables
```
bash
PORT
=
3000
# Port server (default: 3000)
PXE_ROOT
=
/var/www/
html
# Root direktori PXE (default: /var/www/html
)
PXE_ROOT
=
/var/www/
pxe-dashboard
# Root direktori PXE (default: /var/www/pxe-dashboard
)
```
## Menjalankan sebagai Service (systemd)
...
...
@@ -113,7 +110,7 @@ WorkingDirectory=/path/to/pxe-dashboard
ExecStart
=
/usr/bin/node app.js
Restart
=
on-failure
Environment
=
PORT=3000
Environment
=
PXE_ROOT=/var/www/
html
Environment
=
PXE_ROOT=/var/www/
pxe-dashboard
[Install]
WantedBy
=
multi-user.target
...
...
@@ -128,7 +125,7 @@ sudo systemctl start pxe-dashboard
## Jalur File yang Dikelola
| File | Path |
|------|------|
| boot.uefi |
`/var/www/html/boot.
uefi
`
|
| boot.uefi |
`/var/www/html/boot.
ipxe
`
|
| vmlinuz |
`/var/www/html/debian12/live/vmlinuz`
|
| initrd.img |
`/var/www/html/debian12/live/initrd.img`
|
| filesystem.squashfs |
`/var/www/html/debian12/live/filesystem.squashfs`
|
app.js
View file @
6c19868
...
...
@@ -39,12 +39,14 @@ const authRoutes = require('./routes/auth');
const
dashboardRoutes
=
require
(
'./routes/dashboard'
);
const
fileRoutes
=
require
(
'./routes/files'
);
const
serviceRoutes
=
require
(
'./routes/service'
);
const
monitorRoutes
=
require
(
'./routes/monitor'
);
app
.
use
(
'/'
,
authRoutes
);
app
.
use
(
'/dashboard'
,
dashboardRoutes
);
app
.
use
(
'/files'
,
fileRoutes
);
app
.
use
(
'/service'
,
serviceRoutes
);
app
.
use
(
'/monitor'
,
monitorRoutes
);
app
.
listen
(
PORT
,
()
=>
{
console
.
log
(
`PXE Dashboard berjalan di http://localhost:
${
PORT
}
`
);
});
});
\ No newline at end of file
public/css/style.css
View file @
6c19868
...
...
@@ -1026,3 +1026,225 @@ body {
opacity
:
0.3
;
font-size
:
10px
;
}
/* ===== MONITOR PAGE ===== */
.mon-stat-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
180px
,
1
fr
));
gap
:
12px
;
margin-bottom
:
4px
;
}
.mon-stat-card
{
background
:
var
(
--card-bg
);
border
:
1px
solid
var
(
--border
);
border-radius
:
6px
;
padding
:
18px
20px
;
}
.msc-label
{
font-size
:
9px
;
letter-spacing
:
2px
;
color
:
var
(
--text3
);
font-family
:
var
(
--font-mono
);
margin-bottom
:
8px
;
}
.msc-value
{
font-size
:
26px
;
font-weight
:
700
;
font-family
:
var
(
--font-mono
);
color
:
#fff
;
}
.msc-sep
{
color
:
var
(
--text3
);
margin
:
0
4px
;
font-size
:
18px
;
}
.accent-green
{
color
:
var
(
--accent3
);
}
.accent-blue
{
color
:
var
(
--accent
);
}
.accent-red
{
color
:
var
(
--danger
);
}
/* Bandwidth */
.bw-container
{
background
:
var
(
--card-bg
);
border
:
1px
solid
var
(
--border
);
border-radius
:
6px
;
padding
:
20px
;
}
.bw-iface-selector
{
display
:
flex
;
gap
:
8px
;
margin-bottom
:
16px
;
flex-wrap
:
wrap
;
}
.bw-iface-btn
{
padding
:
5px
14px
;
border
:
1px
solid
var
(
--border2
);
border-radius
:
4px
;
background
:
var
(
--bg3
);
color
:
var
(
--text2
);
font-family
:
var
(
--font-mono
);
font-size
:
12px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.bw-iface-btn.active
,
.bw-iface-btn
:hover
{
border-color
:
var
(
--accent
);
color
:
var
(
--accent
);
background
:
rgba
(
0
,
212
,
255
,
0.08
);
}
.bw-stats-row
{
display
:
flex
;
gap
:
32px
;
margin-bottom
:
16px
;
flex-wrap
:
wrap
;
}
.bw-stat
{
display
:
flex
;
flex-direction
:
column
;
gap
:
4px
;
}
.bw-stat-label
{
font-size
:
9px
;
letter-spacing
:
2px
;
color
:
var
(
--text3
);
font-family
:
var
(
--font-mono
);
}
.bw-stat-val
{
font-size
:
20px
;
font-weight
:
700
;
font-family
:
var
(
--font-mono
);
}
.chart-wrap
{
position
:
relative
;
height
:
160px
;
}
/* Two col layout */
.two-col
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
24px
;
}
@media
(
max-width
:
768px
)
{
.two-col
{
grid-template-columns
:
1
fr
;
}
}
/* Top lists */
.top-list
{
background
:
var
(
--card-bg
);
border
:
1px
solid
var
(
--border
);
border-radius
:
6px
;
overflow
:
hidden
;
}
.top-item
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
padding
:
10px
16px
;
border-bottom
:
1px
solid
var
(
--border
);
transition
:
background
0.15s
;
}
.top-item
:last-child
{
border-bottom
:
none
;
}
.top-item
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.02
);
}
.top-rank
{
font-size
:
10px
;
color
:
var
(
--text3
);
font-family
:
var
(
--font-mono
);
width
:
24px
;
flex-shrink
:
0
;
}
.top-name
{
flex
:
1
;
font-size
:
12px
;
color
:
var
(
--text
);
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.top-count
{
font-size
:
11px
;
font-family
:
var
(
--font-mono
);
color
:
var
(
--accent
);
flex-shrink
:
0
;
}
/* Log Table */
.log-filter-wrap
{
margin-left
:
auto
;
}
.log-filter-input
{
background
:
var
(
--bg3
);
border
:
1px
solid
var
(
--border2
);
border-radius
:
4px
;
padding
:
4px
12px
;
color
:
var
(
--text
);
font-family
:
var
(
--font-mono
);
font-size
:
11px
;
outline
:
none
;
width
:
200px
;
transition
:
border-color
0.2s
;
}
.log-filter-input
:focus
{
border-color
:
var
(
--accent
);
}
.log-filter-input
::placeholder
{
color
:
var
(
--text3
);
}
.section-title
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
}
.log-table-wrap
{
background
:
var
(
--card-bg
);
border
:
1px
solid
var
(
--border
);
border-radius
:
6px
;
overflow
:
auto
;
max-height
:
400px
;
}
.log-table
{
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
12px
;
}
.log-table
thead
{
position
:
sticky
;
top
:
0
;
background
:
var
(
--bg2
);
z-index
:
1
;
}
.log-table
th
{
padding
:
10px
14px
;
text-align
:
left
;
font-size
:
9px
;
letter-spacing
:
2px
;
color
:
var
(
--text3
);
font-family
:
var
(
--font-mono
);
border-bottom
:
1px
solid
var
(
--border
);
font-weight
:
400
;
}
.log-table
td
{
padding
:
8px
14px
;
border-bottom
:
1px
solid
rgba
(
30
,
45
,
61
,
0.5
);
font-size
:
11px
;
color
:
var
(
--text2
);
}
.log-row
:hover
td
{
background
:
rgba
(
255
,
255
,
255
,
0.02
);
}
.log-row.status-err
td
{
color
:
rgba
(
255
,
61
,
90
,
0.7
);
}
.log-time
{
color
:
var
(
--text3
)
!important
;
font-size
:
10px
!important
;
}
.log-ip
{
color
:
var
(
--accent
)
!important
;
}
.log-file
{
color
:
var
(
--text
)
!important
;
max-width
:
200px
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.status-badge
{
display
:
inline-block
;
padding
:
2px
8px
;
border-radius
:
3px
;
font-size
:
10px
;
font-family
:
var
(
--font-mono
);
font-weight
:
700
;
}
.status-badge.status-ok
{
background
:
rgba
(
0
,
255
,
157
,
0.1
);
color
:
var
(
--accent3
);
border
:
1px
solid
rgba
(
0
,
255
,
157
,
0.2
);
}
.status-badge.status-err
{
background
:
rgba
(
255
,
61
,
90
,
0.1
);
color
:
var
(
--danger
);
border
:
1px
solid
rgba
(
255
,
61
,
90
,
0.2
);
}
.empty-state
{
text-align
:
center
;
padding
:
32px
;
color
:
var
(
--text3
);
font-family
:
var
(
--font-mono
);
font-size
:
12px
;
}
routes/monitor.js
0 → 100644
View file @
6c19868
const
express
=
require
(
'express'
);
const
fs
=
require
(
'fs'
);
const
readline
=
require
(
'readline'
);
const
{
exec
}
=
require
(
'child_process'
);
const
{
isAuthenticated
}
=
require
(
'../middleware/auth'
);
const
router
=
express
.
Router
();
const
NGINX_LOG
=
process
.
env
.
NGINX_LOG
||
'/var/log/nginx/access.log'
;
// ─── Bandwidth dari /proc/net/dev ───────────────────────────────────────────
function
getNetStats
()
{
try
{
const
raw
=
fs
.
readFileSync
(
'/proc/net/dev'
,
'utf8'
);
const
lines
=
raw
.
trim
().
split
(
'\n'
).
slice
(
2
);
const
result
=
{};
lines
.
forEach
(
line
=>
{
const
parts
=
line
.
trim
().
split
(
/
\s
+/
);
const
iface
=
parts
[
0
].
replace
(
':'
,
''
);
if
(
iface
===
'lo'
)
return
;
result
[
iface
]
=
{
rxBytes
:
parseInt
(
parts
[
1
]),
txBytes
:
parseInt
(
parts
[
9
]),
};
});
return
result
;
}
catch
(
e
)
{
return
{};
}
}
// Cache untuk hitung delta bps
let
prevStats
=
{};
let
prevTime
=
Date
.
now
();
function
getBandwidth
()
{
const
now
=
Date
.
now
();
const
current
=
getNetStats
();
const
elapsed
=
(
now
-
prevTime
)
/
1000
;
// detik
const
bandwidth
=
{};
Object
.
keys
(
current
).
forEach
(
iface
=>
{
const
prev
=
prevStats
[
iface
];
if
(
prev
&&
elapsed
>
0
)
{
bandwidth
[
iface
]
=
{
rxBps
:
Math
.
max
(
0
,
(
current
[
iface
].
rxBytes
-
prev
.
rxBytes
)
/
elapsed
),
txBps
:
Math
.
max
(
0
,
(
current
[
iface
].
txBytes
-
prev
.
txBytes
)
/
elapsed
),
rxBytes
:
current
[
iface
].
rxBytes
,
txBytes
:
current
[
iface
].
txBytes
,
};
}
else
{
bandwidth
[
iface
]
=
{
rxBps
:
0
,
txBps
:
0
,
...
current
[
iface
]
};
}
});
prevStats
=
current
;
prevTime
=
now
;
return
bandwidth
;
}
// ─── Parse nginx access log ──────────────────────────────────────────────────
// Format nginx default: IP - - [date] "METHOD /path HTTP/x.x" status bytes "ref" "ua"
function
parseNginxLog
(
limit
=
100
)
{
return
new
Promise
((
resolve
)
=>
{
const
entries
=
[];
try
{
if
(
!
fs
.
existsSync
(
NGINX_LOG
))
return
resolve
([]);
const
content
=
fs
.
readFileSync
(
NGINX_LOG
,
'utf8'
);
const
lines
=
content
.
trim
().
split
(
'\n'
).
filter
(
Boolean
);
// Ambil dari bawah (terbaru)
const
recent
=
lines
.
slice
(
-
limit
).
reverse
();
recent
.
forEach
(
line
=>
{
// Hanya tampilkan request ke /debian12/live/
if
(
!
line
.
includes
(
'/debian12/live/'
))
return
;
// Parse format nginx combined log
const
m
=
line
.
match
(
/^
(\S
+
)\s
+-
\s
+-
\s
+
\[([^\]]
+
)\]\s
+"
(\S
+
)\s
+
(\S
+
)\s
+
\S
+"
\s
+
(\d
+
)\s
+
(\d
+
)
/
);
if
(
!
m
)
return
;
const
[,
ip
,
time
,
method
,
path
,
status
,
bytes
]
=
m
;
const
filename
=
path
.
split
(
'/'
).
pop
().
split
(
'?'
)[
0
]
||
path
;
entries
.
push
({
ip
,
time
,
method
,
path
,
filename
,
status
:
parseInt
(
status
),
bytes
:
parseInt
(
bytes
),
});
});
}
catch
(
e
)
{
console
.
error
(
'Gagal baca nginx log:'
,
e
.
message
);
}
resolve
(
entries
);
});
}
// ─── Routes ──────────────────────────────────────────────────────────────────
// Halaman monitoring
router
.
get
(
'/'
,
isAuthenticated
,
async
(
req
,
res
)
=>
{
const
logs
=
await
parseNginxLog
(
100
);
const
bandwidth
=
getBandwidth
();
// Statistik ringkasan dari log
const
stats
=
{
totalRequests
:
logs
.
length
,
uniqueClients
:
new
Set
(
logs
.
map
(
l
=>
l
.
ip
)).
size
,
totalBytes
:
logs
.
reduce
((
s
,
l
)
=>
s
+
(
l
.
bytes
||
0
),
0
),
success
:
logs
.
filter
(
l
=>
l
.
status
>=
200
&&
l
.
status
<
300
).
length
,
errors
:
logs
.
filter
(
l
=>
l
.
status
>=
400
).
length
,
};
// Top file yang paling sering diakses
const
fileCounts
=
{};
logs
.
forEach
(
l
=>
{
fileCounts
[
l
.
filename
]
=
(
fileCounts
[
l
.
filename
]
||
0
)
+
1
;
});
const
topFiles
=
Object
.
entries
(
fileCounts
)
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
])
.
slice
(
0
,
5
)
.
map
(([
name
,
count
])
=>
({
name
,
count
}));
// Top client IP
const
ipCounts
=
{};
logs
.
forEach
(
l
=>
{
ipCounts
[
l
.
ip
]
=
(
ipCounts
[
l
.
ip
]
||
0
)
+
1
;
});
const
topClients
=
Object
.
entries
(
ipCounts
)
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
])
.
slice
(
0
,
10
)
.
map
(([
ip
,
count
])
=>
({
ip
,
count
}));
res
.
render
(
'monitor'
,
{
title
:
'Traffic Monitor'
,
logs
:
logs
.
slice
(
0
,
50
),
bandwidth
,
stats
,
topFiles
,
topClients
,
nginxLog
:
NGINX_LOG
,
});
});
// ─── API endpoint untuk polling realtime ─────────────────────────────────────
router
.
get
(
'/api/bandwidth'
,
isAuthenticated
,
(
req
,
res
)
=>
{
res
.
json
({
bandwidth
:
getBandwidth
(),
timestamp
:
Date
.
now
()
});
});
router
.
get
(
'/api/logs'
,
isAuthenticated
,
async
(
req
,
res
)
=>
{
const
logs
=
await
parseNginxLog
(
50
);
const
stats
=
{
totalRequests
:
logs
.
length
,
uniqueClients
:
new
Set
(
logs
.
map
(
l
=>
l
.
ip
)).
size
,
totalBytes
:
logs
.
reduce
((
s
,
l
)
=>
s
+
(
l
.
bytes
||
0
),
0
),
success
:
logs
.
filter
(
l
=>
l
.
status
>=
200
&&
l
.
status
<
300
).
length
,
errors
:
logs
.
filter
(
l
=>
l
.
status
>=
400
).
length
,
};
res
.
json
({
logs
:
logs
.
slice
(
0
,
50
),
stats
,
timestamp
:
Date
.
now
()
});
});
module
.
exports
=
router
;
\ No newline at end of file
views/monitor.ejs
0 → 100644
View file @
6c19868
This diff is collapsed.
Click to expand it.
views/partials/nav.ejs
View file @
6c19868
...
...
@@ -19,6 +19,9 @@
<li><a href="/service/status" class="nav-link <%= title === 'Status Service' ? 'active' : '' %>">
<span class="nav-icon">◈</span> Services
</a></li>
<li><a href="/monitor" class="nav-link <%= title === 'Traffic Monitor' ? 'active' : '' %>">
<span class="nav-icon">◈</span> Monitor
</a></li>
</ul>
<div class="nav-user">
<span class="user-badge"><%= user.username %></span>
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment