249 Commits
v1.0 ... dev

Author SHA1 Message Date
dc6dfc38ad usr/local/bin/custom/poe.sh aktualisiert 2025-10-12 19:33:11 +02:00
7561791a0b usr/local/bin/custom/poe.sh aktualisiert 2025-10-12 19:29:39 +02:00
c7266f73f3 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:21:06 +02:00
1a742c56c9 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:20:40 +02:00
9cdcc59038 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:20:11 +02:00
6523fdf675 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:19:42 +02:00
d846856cb4 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:19:02 +02:00
94142f9ead srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:17:25 +02:00
8c93eab3a5 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:17:09 +02:00
ab6ecd773d srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:16:51 +02:00
970f7dbeb5 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:16:34 +02:00
19f6390e22 srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:16:08 +02:00
268800d31a srv/poe_manager/static/css/style.css aktualisiert 2025-10-12 19:14:41 +02:00
7b0c847e67 srv/poe_manager/app.py aktualisiert 2025-10-12 18:25:08 +02:00
007d9f919f revert 7c045a65b2
revert srv/poe_manager/templates/index.html aktualisiert
2025-10-12 18:24:13 +02:00
7c045a65b2 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:23:57 +02:00
98023092a2 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:23:16 +02:00
28fa9087a4 srv/poe_manager/app.py aktualisiert 2025-10-12 18:22:37 +02:00
804753dde8 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:20:53 +02:00
eaf2e2f78e srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:18:45 +02:00
50d5f58af4 srv/poe_manager/app.py aktualisiert 2025-10-12 18:18:08 +02:00
12d2511695 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:13:29 +02:00
3bd2aca4f5 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:11:26 +02:00
b6e9ff3f98 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:09:58 +02:00
398629eaaa srv/poe_manager/templates/index.html aktualisiert 2025-10-12 18:09:04 +02:00
3eb78b46a7 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:51:21 +02:00
681888a36a DB 2025-10-12 17:45:24 +02:00
e8df4f937b srv/poe_manager/templates/users.html aktualisiert 2025-10-12 17:43:58 +02:00
29823bb4ef srv/poe_manager/templates/switche.html aktualisiert 2025-10-12 17:43:46 +02:00
a2920a98bd srv/poe_manager/templates/devices.html aktualisiert 2025-10-12 17:43:12 +02:00
7f0871fa64 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:33:30 +02:00
5ae38a9e20 srv/poe_manager/app.py aktualisiert 2025-10-12 17:33:05 +02:00
de23b132ed srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:31:41 +02:00
746972e321 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:31:08 +02:00
dfba8789a7 srv/poe_manager/app.py aktualisiert 2025-10-12 17:28:05 +02:00
194e3cfbf9 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:22:31 +02:00
4e10fe8ac0 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:16:23 +02:00
7ae131b5f4 srv/poe_manager/app.py aktualisiert 2025-10-12 17:12:40 +02:00
09ee8c245e srv/poe_manager/app.py aktualisiert 2025-10-12 17:09:43 +02:00
bb07a598d7 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:06:38 +02:00
4ef10fc39d srv/poe_manager/app.py aktualisiert 2025-10-12 17:06:04 +02:00
d73fda1935 revert aa2895ccf5
revert srv/poe_manager/templates/index.html aktualisiert
2025-10-12 17:03:52 +02:00
ad9dfcee7a revert 4ae255b67f
revert srv/poe_manager/templates/index.html aktualisiert
2025-10-12 17:03:00 +02:00
4ae255b67f srv/poe_manager/templates/index.html aktualisiert 2025-10-12 17:00:36 +02:00
aa2895ccf5 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 16:53:23 +02:00
7c81f3a299 srv/poe_manager/app.py aktualisiert 2025-10-12 16:52:24 +02:00
cef96cd7cd Devices DB 2025-10-12 16:39:42 +02:00
b4cc41c272 v1.2 2025-10-12 13:35:39 +00:00
0d784a0530 srv/poe_manager/app.py aktualisiert 2025-10-12 15:22:08 +02:00
34ef2c4e2a srv/poe_manager/app.py aktualisiert 2025-10-12 15:20:21 +02:00
a26a91ec5a srv/poe_manager/templates/index.html aktualisiert 2025-10-12 15:17:21 +02:00
ef548b9532 srv/poe_manager/templates/index.html aktualisiert 2025-10-12 15:14:10 +02:00
466f3615ed usr/local/bin/custom/poe.sh aktualisiert 2025-10-12 15:09:22 +02:00
3dc554e165 srv/poe_manager/app.py aktualisiert 2025-10-12 14:58:56 +02:00
78ed058d69 srv/poe_manager/generate_ips.py aktualisiert 2025-10-12 14:53:59 +02:00
d3f209941b srv/poe_manager/generate_ips.py aktualisiert 2025-10-12 14:51:08 +02:00
9f98d240ca srv/poe_manager/app.py aktualisiert 2025-10-12 14:43:58 +02:00
d88135fc12 srv/poe_manager/app.py aktualisiert 2025-10-12 14:37:34 +02:00
34eae3ca9d srv/poe_manager/app.py aktualisiert 2025-10-12 14:35:18 +02:00
f05936226d srv/poe_manager/app.py aktualisiert 2025-10-12 14:33:49 +02:00
d6c15d3d49 srv/poe_manager/app.py aktualisiert 2025-10-12 14:32:37 +02:00
7a1e9d88e5 srv/poe_manager/app.py aktualisiert 2025-10-12 14:29:02 +02:00
2f1c7b5653 srv/poe_manager/app.py aktualisiert 2025-10-12 14:10:10 +02:00
d1e43a1220 srv/poe_manager/app.py aktualisiert 2025-10-12 14:03:06 +02:00
fe921254c8 srv/poe_manager/app.py aktualisiert 2025-10-12 13:58:25 +02:00
f27e73ae72 srv/poe_manager/app.py aktualisiert 2025-10-12 13:55:40 +02:00
63b8e33434 srv/poe_manager/app.py aktualisiert 2025-10-12 13:50:22 +02:00
ad09b4f6a2 srv/poe_manager/templates/devices.html aktualisiert 2025-10-12 13:48:59 +02:00
59a9000de5 Table changes 2025-10-12 11:39:31 +00:00
20b38194a5 srv/poe_manager/create_db.py aktualisiert 2025-10-12 13:38:01 +02:00
772d190369 srv/poe_manager/generate_ips.py aktualisiert 2025-10-12 13:35:48 +02:00
41f244cc59 usr/local/bin/custom/poe.sh aktualisiert 2025-10-12 13:34:55 +02:00
f6ce3763a5 v1.1 2025-10-12 12:32:24 +02:00
5df7a13780 DB 2025-10-08 22:02:40 +02:00
cbce015edd DB 2025-10-08 22:01:03 +02:00
9965d8d4fb srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 21:59:22 +02:00
1e94ac6dde srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 21:57:43 +02:00
25d778086d srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 21:54:08 +02:00
b1eaecf3f2 revert 4258ff07eb
revert srv/poe_manager/templates/logs.html aktualisiert
2025-10-08 21:42:28 +02:00
56ed90f560 revert 861c019324
revert srv/poe_manager/templates/logs.html aktualisiert
2025-10-08 21:41:23 +02:00
861c019324 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 21:40:45 +02:00
459aeb22cf revert fbfafd0055
revert srv/poe_manager/templates/logs.html aktualisiert
2025-10-08 21:40:22 +02:00
fbfafd0055 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 21:39:25 +02:00
91885932c8 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 21:38:00 +02:00
4258ff07eb srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 21:37:27 +02:00
af689c20b8 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 21:33:16 +02:00
57ce7d0c6f srv/poe_manager/app.py aktualisiert 2025-10-08 21:30:45 +02:00
cea081585f srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 21:30:08 +02:00
b2f9b701fe Index.html 2025-10-08 21:27:05 +02:00
1a67dca394 revert 349710e0d3
revert srv/poe_manager/sqlite.db gelöscht
2025-10-08 21:14:19 +02:00
349710e0d3 srv/poe_manager/sqlite.db gelöscht 2025-10-08 21:13:35 +02:00
45ba78c374 .gitignore aktualisiert 2025-10-08 21:12:50 +02:00
a444185933 revert 8a5745e337
revert srv/poe_manager/templates/index.html aktualisiert
2025-10-08 20:47:37 +02:00
8a5745e337 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:46:00 +02:00
e244092667 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:39:06 +02:00
ad0e48b979 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:38:31 +02:00
3544675975 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:37:12 +02:00
580cda1acc srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:35:09 +02:00
61b0137233 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:34:17 +02:00
2b92c328e1 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:32:15 +02:00
831da1479e revert 122a4bd4a4
revert srv/poe_manager/templates/index.html aktualisiert
2025-10-08 20:28:46 +02:00
122a4bd4a4 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:27:40 +02:00
cd654cf9a0 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:24:39 +02:00
8ca32bf7a9 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:20:35 +02:00
3e49f433aa srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:18:59 +02:00
c2a74e54cb srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:16:25 +02:00
847c720055 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 20:14:59 +02:00
250a131542 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:57:10 +02:00
e7458aca5f srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:47:17 +02:00
64e9ebc3ea srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:45:22 +02:00
3ab5e023fe srv/poe_manager/app.py aktualisiert 2025-10-08 19:43:55 +02:00
f479de8508 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:41:01 +02:00
2d735fd4b4 srv/poe_manager/static/css/style.css aktualisiert 2025-10-08 19:38:43 +02:00
9d50113c20 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:38:13 +02:00
18a2b8facf srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:34:18 +02:00
f8042669b4 srv/poe_manager/templates/index.html aktualisiert 2025-10-08 19:33:50 +02:00
0cc9d2ea0b srv/poe_manager/templates/users.html aktualisiert 2025-10-08 19:32:09 +02:00
b7c6264082 srv/poe_manager/app.py aktualisiert 2025-10-08 19:25:11 +02:00
8ff4515a17 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 19:03:14 +02:00
225750f6bc srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 19:01:40 +02:00
de6b5c7fe9 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 18:59:51 +02:00
2c9a715d37 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 18:57:12 +02:00
c957a32a86 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 18:51:47 +02:00
4fd22bd3ec srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 18:50:58 +02:00
44b7a9e0b0 srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 18:49:11 +02:00
9c25d532ec srv/poe_manager/templates/logs.html aktualisiert 2025-10-08 18:46:26 +02:00
207eed1f00 srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 18:31:51 +02:00
a2c2f6464f srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 18:29:39 +02:00
404f12e5fe srv/poe_manager/app.py aktualisiert 2025-10-08 18:27:50 +02:00
7eec728796 srv/poe_manager/app.py aktualisiert 2025-10-08 18:25:24 +02:00
93a93e2289 srv/poe_manager/templates/switche.html aktualisiert 2025-10-08 18:22:17 +02:00
d8a4b17b28 srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 18:17:47 +02:00
f6c539ac8d srv/poe_manager/templates/devices.html aktualisiert 2025-10-08 18:14:34 +02:00
5f0d024a15 srv/poe_manager/app.py aktualisiert 2025-10-08 18:06:25 +02:00
c4708bbb19 srv/poe_manager/app.py aktualisiert 2025-10-07 23:21:49 +02:00
cc40ae6276 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 23:21:16 +02:00
ca86166c5d srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 23:20:23 +02:00
6529fd4643 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 23:19:00 +02:00
738b7f9cb0 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 23:16:01 +02:00
0e80925a11 srv/poe_manager/app.py aktualisiert 2025-10-07 23:12:32 +02:00
a644de01ad srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 23:09:01 +02:00
8d0bdeb936 srv/poe_manager/app.py aktualisiert 2025-10-07 23:08:07 +02:00
aa8facfe28 srv/poe_manager/app.py aktualisiert 2025-10-07 23:05:56 +02:00
1a5ba7b0ae srv/poe_manager/app.py aktualisiert 2025-10-07 23:00:47 +02:00
cce1de034a srv/poe_manager/app.py aktualisiert 2025-10-07 22:59:00 +02:00
0fd53990e6 srv/poe_manager/app.py aktualisiert 2025-10-07 22:53:14 +02:00
d33c567769 srv/poe_manager/app.py aktualisiert 2025-10-07 22:49:23 +02:00
02f9445f42 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:30:41 +02:00
2d74cbce10 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:29:46 +02:00
11857b224a srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:28:51 +02:00
4b25d4dcf6 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:28:18 +02:00
5cdb336e4a srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:28:01 +02:00
38ad402048 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:27:25 +02:00
a29c46345d srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:24:59 +02:00
1e03dad68f srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:24:07 +02:00
b00d55e1ad srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 18:23:54 +02:00
0d569dcd1b srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:23:21 +02:00
81afc82190 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:22:04 +02:00
43d30dd822 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:19:58 +02:00
98950d706c revert 0f6b87ba1f
revert srv/poe_manager/templates/devices.html aktualisiert
2025-10-07 18:17:08 +02:00
ae3caefd18 revert bbbe53f0db
revert srv/poe_manager/app.py aktualisiert
2025-10-07 18:13:06 +02:00
0f6b87ba1f srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 18:11:57 +02:00
bbbe53f0db srv/poe_manager/app.py aktualisiert 2025-10-07 18:11:44 +02:00
290584e0d9 srv/poe_manager/templates/settings.html aktualisiert 2025-10-07 18:05:01 +02:00
51594cbd2f srv/poe_manager/static/css/style.css aktualisiert 2025-10-07 18:01:06 +02:00
79b980b069 srv/poe_manager/static/css/style.css aktualisiert 2025-10-07 17:59:37 +02:00
53b966307a srv/poe_manager/templates/base.html aktualisiert 2025-10-07 17:57:39 +02:00
9b3d78ad79 revert 178ea8ff58
revert srv/poe_manager/static/css/style.css aktualisiert
2025-10-07 17:54:12 +02:00
72392c97b1 revert d3bdc46314
revert srv/poe_manager/templates/switche.html aktualisiert
2025-10-07 17:52:11 +02:00
7a5de70fc1 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 17:51:02 +02:00
add19eeb4d srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 17:47:05 +02:00
d3bdc46314 srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 17:43:35 +02:00
4b5f6abe99 srv/poe_manager/templates/users.html aktualisiert 2025-10-07 17:42:33 +02:00
178ea8ff58 srv/poe_manager/static/css/style.css aktualisiert 2025-10-07 17:40:49 +02:00
119dbccf5d srv/poe_manager/static/css/style.css aktualisiert 2025-10-07 17:38:20 +02:00
a5ec09297f srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 17:36:38 +02:00
43a77700db srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 17:35:46 +02:00
dd4f4106a5 srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 17:34:04 +02:00
202d13e552 srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 17:33:06 +02:00
ecbba08074 srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 17:32:18 +02:00
ffd2a03e6d srv/poe_manager/app.py aktualisiert 2025-10-07 17:30:51 +02:00
83b4b5a32c srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 17:30:24 +02:00
e9cfbe4678 srv/poe_manager/app.py aktualisiert 2025-10-07 17:25:51 +02:00
c9449dc756 srv/poe_manager/app.py aktualisiert 2025-10-07 17:24:28 +02:00
02122f966a srv/poe_manager/app.py aktualisiert 2025-10-07 07:57:15 +02:00
a24b09e18d srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 07:53:09 +02:00
b526f9d0a0 srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 07:50:07 +02:00
c3186813d4 srv/poe_manager/templates/devices.html aktualisiert 2025-10-07 07:48:10 +02:00
9af5740ed3 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 22:18:53 +02:00
ecad3cda88 srv/poe_manager/templates/login.html aktualisiert 2025-10-06 22:10:25 +02:00
022050400e srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 22:09:54 +02:00
d248087326 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 22:02:55 +02:00
183725b03f .gitignore aktualisiert 2025-10-06 21:42:52 +02:00
1fcdd27f6a srv/poe_manager/templates/settings.html aktualisiert 2025-10-06 21:39:57 +02:00
99395089f9 srv/poe_manager/templates/users.html aktualisiert 2025-10-06 21:39:35 +02:00
bcd2c30c70 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 21:38:47 +02:00
be43c85961 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 21:38:23 +02:00
ffcde260c4 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 21:16:46 +02:00
8b130a072e srv/poe_manager/templates/users.html aktualisiert 2025-10-06 21:14:45 +02:00
9c895514e4 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 21:13:45 +02:00
7af700119e srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 21:11:06 +02:00
9be5fdbeb4 srv/poe_manager/static/css/style.css aktualisiert 2025-10-06 21:09:52 +02:00
862577632a DB 2025-10-06 19:07:04 +00:00
1e37b9551b CSS 2025-10-06 19:04:39 +00:00
d116d687b4 Merge branch 'dev' of https://gitea.int.eertmoed.net/WiS/Aruba-PoE into dev 2025-10-05 08:29:47 +00:00
c367531860 DB 2025-10-05 08:20:15 +00:00
96a93188f7 srv/poe_manager/create_db.py aktualisiert 2025-09-30 19:10:55 +02:00
c5f6e1c708 DB-Out 2025-09-30 17:07:27 +00:00
55513c7fae poe_web.service 2025-09-30 17:34:25 +02:00
1136c97ce2 Login textfarbe 2025-09-30 17:28:12 +02:00
28a436df16 images 2025-09-30 15:14:26 +00:00
a10343d37e Devices is_active + Table 2025-09-29 18:17:58 +00:00
b465bd9c54 Devices is_active + Table 2025-09-29 17:54:03 +00:00
4a093c1317 usr/local/bin/custom/poe.sh aktualisiert 2025-09-28 19:25:25 +02:00
ca389f488c usr/local/bin/custom/poe.sh aktualisiert 2025-09-28 19:18:50 +02:00
c39289c584 Improvement 2025-09-28 17:07:59 +00:00
a25462c78d srv/poe_manager/app.py aktualisiert 2025-09-28 12:49:03 +02:00
fe936ceca9 srv/poe_manager/app.py aktualisiert 2025-09-28 12:46:02 +02:00
ca7620abaf srv/poe_manager/templates/logs.html aktualisiert 2025-09-28 12:41:33 +02:00
57005486cb srv/poe_manager/templates/logs.html aktualisiert 2025-09-28 12:33:28 +02:00
80d596b94c srv/poe_manager/templates/logs.html aktualisiert 2025-09-28 12:28:29 +02:00
2f50633eff Bugfix Intervall 2025-09-26 19:10:03 +00:00
84a24c811b NGINX 2025-09-26 18:56:50 +00:00
d3cdc5d3bd v1.0 2025-09-26 18:47:57 +00:00
3a454780a2 README.md aktualisiert 2025-09-26 20:25:31 +02:00
0f090d594e README.md aktualisiert 2025-09-26 20:24:43 +02:00
b602ebe089 v1.0 2025-09-26 18:22:08 +00:00
c0c3ed4dd7 Userverwaltung 2025-09-26 18:08:32 +00:00
85d7872934 Webinterface 2025-09-26 15:51:18 +00:00
f057dc65c5 Bugfix 2025-09-26 15:43:33 +00:00
d948096494 README.md 2025-09-26 15:41:46 +00:00
8b39425b38 README.md 2025-09-26 15:40:59 +00:00
12564647a2 Webinterface 2025-09-26 15:39:01 +00:00
b7256868ee venv aus Repo entfernen 2025-09-26 15:34:15 +00:00
dc9e42fadf Webinterface 2025-09-26 15:31:32 +00:00
e7f6ff6ee1 Webinterface 2025-09-26 15:28:24 +00:00
d38b8e4873 Webinterface 2025-09-26 15:25:11 +00:00
70d30e95fb Webinterface 2025-09-26 15:22:31 +00:00
e99e17be3b Webinterface 2025-09-26 15:21:37 +00:00
26fb560aac Webinterface 2025-09-26 15:17:37 +00:00
b95b1fbb9f rm test 2025-09-23 16:22:53 +00:00
02e7adf6b9 usr/local/bin/custom/dawdwadaw hinzugefügt 2025-09-23 18:21:45 +02:00
649d0598f9 .gitignore 2025-09-23 16:21:15 +00:00
c067d26456 etc/systemd/system/rpi-test.service gelöscht 2025-09-23 18:19:18 +02:00
02efd0e40a first test 2025-09-23 16:16:14 +00:00
58d1660073 .gitignore hinzugefügt 2025-09-23 18:11:29 +02:00
d60a525bfb README.md aktualisiert 2025-09-22 00:40:14 +02:00
f45a346601 README.md aktualisiert 2025-09-22 00:32:42 +02:00
1e0df286db usr/local/bin/custom/poe.sh aktualisiert 2025-09-22 00:31:52 +02:00
28 changed files with 2105 additions and 58 deletions

47
.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Alles ignorieren
*
# .gitignore selbst tracken
!.gitignore
# Virtuelle Umgebung ignorieren
/srv/poe_manager/venv/
__pycache__/
# Logfiles ignorieren
*.log
# Systemd Services tracken
!etc/
!etc/systemd/
!etc/systemd/system/
!etc/systemd/system/rpi*.service
!etc/systemd/system/rpi*.timer
!etc/systemd/system/poe*.service
!etc/nginx/
!etc/nginx/sites-enabled/
!etc/nginx/sites-enabled/default
# Custom Scripts tracken
!usr/
!usr/local/
!usr/local/bin/
!usr/local/bin/custom/
!usr/local/bin/custom/*
# Web-App Dateien im poe_manager tracken
!/srv/
!/srv/poe_manager/
!/srv/poe_manager/*.*
!/srv/poe_manager/templates/
!/srv/poe_manager/templates/*.*
!/srv/poe_manager/static/
!/srv/poe_manager/static/css/
!/srv/poe_manager/static/css/*
!/srv/poe_manager/static/js/
!/srv/poe_manager/static/js/*
!/srv/poe_manager/static/images/
!/srv/poe_manager/static/images/*
# Optional: SQLite DB ignorieren (falls du nicht willst, dass Passwörter im Repo landen)
!/srv/poe_manager/sqlite.db

View File

@@ -1,15 +1,42 @@
# Aruba PoE
# PoE Manager Web-App
```
Dieses Repo konfiguriert auf Ubuntu/Debian einen Service,
dieser Service ist in der Lage IPs zu Pingen und entsprechend daran POE auf vorgegebenen Ports an einem Aruba Switch
zu Aktivieren bzw. Deaktivieren.
```
Webbasierte Verwaltung und Monitoring von PoE-Devices und Switches.
Die App ermöglicht:
Download:
- Anzeige von Device-Status (Online/Offline)
- Verwaltung von Devices und Switches
- Einstellung des Prüfintervalls
- Live-Log-Ansicht
- Benutzerverwaltung mit Adminrechten
---
## **Installation (nach einem frischen Clone)**
```bash
wget -qO- --header 'Authorization:token 9031f8d227dd83ba601680bf3a9f6c2d26c1a970' https://gitea.int.eertmoed.net/WiS/Aruba-PoE/archive/latest.tar.gz | tar xvz ; bash /root/aruba-poe/install.sh ;
# Pakete installieren
sudo apt update
sudo apt install python3 python3-venv python3-pip nginx sqlite3 git nano -y
# Repo klonen
git clone https://gitea.int.eertmoed.net/WiS/Aruba-PoE.git /srv/poe_manager
cd /srv/poe_manager
# Virtuelle Umgebung erstellen
python3 -m venv venv
source venv/bin/activate
# Abhängigkeiten installieren
pip install --upgrade pip
pip install -r requirements.txt
# Datenbank initialisieren
python create_db.py
# Admin-Benutzer erstellen
python create_admin.py
# Web-App starten
python app.py --host=0.0.0.0 --port=5000
```

View File

@@ -0,0 +1 @@
/etc/nginx/sites-available/default

View File

@@ -0,0 +1,16 @@
[Unit]
Description=PoE Manager Web App
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/srv/poe_manager
# Nutze die virtuelle Umgebung
ExecStart=/srv/poe_manager/venv/bin/python3 /srv/poe_manager/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -3,9 +3,12 @@ Description=RPI Ping Check
After=network.target
[Service]
ExecStart=/usr/local/bin/custom/poe.sh
Type=simple
User=root
WorkingDirectory=/srv/poe_manager
ExecStart=/srv/poe_manager/venv/bin/python3 /srv/poe_manager/poe_wrapper.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target

613
srv/poe_manager/app.py Normal file
View File

@@ -0,0 +1,613 @@
#u!/usr/bin/env python3
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt
from cryptography.fernet import Fernet
from datetime import datetime
from collections import defaultdict
import sqlite3, glob, os, re
app = Flask(__name__)
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
bcrypt = Bcrypt(app)
login_manager = LoginManager()
login_manager.login_view = "login"
login_manager.init_app(app)
DB_PATH = "/srv/poe_manager/sqlite.db"
class User(UserMixin):
def __init__(self, id_, username, is_admin):
self.id = id_
self.username = username
self.is_admin = is_admin
def get_interval_seconds():
conn = get_db_connection()
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
conn.close()
return int(row['value']) if row else 300
with open("/srv/poe_manager/fernet.key", "rb") as f:
fernet = Fernet(f.read())
def encrypt_password(password: str) -> str:
return fernet.encrypt(password.encode()).decode()
def decrypt_password(token: str) -> str:
return fernet.decrypt(token.encode()).decode()
def get_db_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def get_devices():
"""
Liefert eine Liste aller Devices aus der Datenbank als Dictionaries.
"""
conn = get_db_connection()
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname, is_active FROM devices ORDER BY name ASC").fetchall()
conn.close()
return devices
@login_manager.user_loader
def load_user(user_id):
conn = get_db_connection()
user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
conn.close()
if user:
return User(user['id'], user['username'], user['is_admin'])
return None
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
conn = get_db_connection()
user = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
conn.close()
if user and bcrypt.check_password_hash(user['password'], password):
login_user(User(user['id'], user['username'], user['is_admin']))
return redirect(url_for('index'))
else:
flash("Ungültiger Benutzername oder Passwort")
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
def get_last_seen(dev_name: str):
"""Letztes Mal, dass ein Gerät erreichbar war."""
log_files = glob.glob("/var/log/rpi-*.log")
if not log_files:
return None
latest_time = None
# alle Logs durchgehen
for logfile in sorted(log_files):
with open(logfile, "r") as f:
for line in f:
line = line.strip()
if f"{dev_name} ist erreichbar!" in line:
try:
ts_str = line.split(" ")[0] + " " + line.split(" ")[1] # "YYYY-MM-DD HH:MM:SS"
ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
if latest_time is None or ts > latest_time:
latest_time = ts
except Exception:
continue
if latest_time:
datetime_str = latest_time.strftime("Zuletzt Online am %d.%m.%Y um %H:%M Uhr")
return f"{datetime_str}"
return None
@app.route("/")
@login_required
def index():
# Geräte aus DB laden
conn = sqlite3.connect("sqlite.db")
c = conn.cursor()
c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC")
devices = c.fetchall()
devices = sorted(devices, key=lambda d: d[1][0].upper())
grouped_devices = defaultdict(list)
for d in devices:
first_letter = d[1][:2].upper()
grouped_devices[first_letter].append(d)
# Intervall aus DB laden
c.execute("SELECT value FROM settings WHERE key='interval'")
row = c.fetchone()
interval = int(row[0]) if row else 5
conn.close()
# Status aus Logdateien ermitteln
log_files = glob.glob("/var/log/rpi-*.log")
status_dict = {}
last_seen_dict = {}
if log_files:
latest_log = max(log_files, key=os.path.getctime)
with open(latest_log, "r") as f:
lines = f.readlines()
for dev in devices:
last_status = None
for line in reversed(lines):
if f"{dev[1]} ist erreichbar!" in line:
last_status = "online"
break
elif f"{dev[1]} ist nicht erreichbar!" in line:
last_status = "offline"
break
if last_status:
status_dict[dev[0]] = last_status
if last_status == "offline":
last_seen_dict[dev[0]] = get_last_seen(dev[1])
else:
status_dict[dev[0]] = "unbekannt"
return render_template("index.html", grouped_devices=grouped_devices, devices=devices, status=status_dict, last_seen=last_seen_dict, interval=interval)
@app.route("/settings", methods=["GET", "POST"])
@login_required
def settings():
if not current_user.is_admin:
flash("Nur Admins dürfen die Einstellungen ändern!")
return redirect(url_for("index"))
# Aktuellen Prüfintervall laden
conn = sqlite3.connect("sqlite.db")
c = conn.cursor()
c.execute("SELECT value FROM settings WHERE key='interval'")
row = c.fetchone()
interval = int(row[0]) if row else 5 # Default 5 Minuten
conn.close()
if request.method == "POST":
new_interval = int(request.form["interval"])
# Minuten und Sekunden berechnen
interval_minutes = new_interval
check_interval_seconds = new_interval * 60
conn = sqlite3.connect("sqlite.db")
c = conn.cursor()
# interval (Minuten)
c.execute("""
INSERT INTO settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
""", ("interval", interval_minutes))
# check_interval (Sekunden)
c.execute("""
INSERT INTO settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
""", ("check_interval", check_interval_seconds))
conn.commit()
conn.close()
# rpi-check.service neu starten
import subprocess
subprocess.run(["systemctl", "restart", "rpi-check.service"])
flash(f"Intervall auf {interval_minutes} Minuten gesetzt und Service neu gestartet!")
return redirect(url_for("settings"))
return render_template("settings.html", interval=interval)
@app.route('/devices', methods=['GET', 'POST'])
@login_required
def devices():
conn = get_db_connection()
switches = conn.execute("SELECT hostname FROM switches").fetchall()
if request.method == 'POST':
# -----------------------
# Gerät hinzufügen
# -----------------------
if 'add_device' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('devices'))
mac = request.form.get('mac')
rpi_ip = request.form.get('rpi_ip')
port = request.form.get('port')
name = request.form.get('name')
switch_hostname = request.form.get('switch_hostname')
is_active = 1 if 'is_active' in request.form else 0
if not all([mac, rpi_ip, name]):
flash("Alle Felder müssen ausgefüllt sein!")
return redirect(url_for('devices'))
# Prüfen auf doppelte IP
ip_device = conn.execute("SELECT name FROM devices WHERE rpi_ip=?", (rpi_ip,)).fetchone()
if ip_device:
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
return redirect(url_for('devices'))
# Prüfen auf doppelte MAC
mac_device = conn.execute("SELECT name FROM devices WHERE mac=?", (mac,)).fetchone()
if mac_device:
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
return redirect(url_for('devices'))
try:
conn.execute("""
INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname, is_active)
VALUES (?, ?, ?, ?, ?, ?)
""", (mac, rpi_ip, port, name, switch_hostname, is_active))
conn.commit()
flash(f"Gerät {name} hinzugefügt.")
except sqlite3.IntegrityError:
flash("Fehler beim Hinzufügen des Geräts!")
# -----------------------
# Gerät bearbeiten
# -----------------------
elif 'edit_device' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('devices'))
old_mac = request.form.get('old_mac')
mac = request.form.get('mac')
rpi_ip = request.form.get('rpi_ip')
port = request.form.get('port') or None
name = request.form.get('name')
switch_hostname = request.form.get('switch_hostname') or None
is_active = 1 if 'is_active' in request.form else 0
# --- Nur Switch ändern ---
# Prüfen, ob nur das Switch-Feld gesendet wurde und die anderen Felder leer sind
if 'switch_hostname' in request.form and all(not f for f in [mac, rpi_ip, name]):
device = conn.execute("SELECT name, switch_hostname FROM devices WHERE mac=?", (old_mac,)).fetchone()
if not device:
flash("Gerät nicht gefunden!")
return redirect(url_for('devices'))
old_switch = device['switch_hostname'] or "unbekannt"
device_name = device['name']
switch_hostname = request.form.get('switch_hostname') or ""
try:
conn.execute("""
UPDATE devices
SET switch_hostname=?
WHERE mac=?
""", (switch_hostname, old_mac))
conn.commit()
flash(f"Switch von {device_name} geändert: {old_switch}{switch_hostname or 'Kein Switch'}")
except sqlite3.IntegrityError:
flash("Fehler beim Ändern des Switch!")
return redirect(url_for('devices'))
# --- Normales Gerät bearbeiten ---
# Pflichtfelder prüfen
if not all([old_mac, mac, rpi_ip, name]):
flash("Felder 'MAC', 'IP' und 'Name' müssen ausgefüllt sein!")
return redirect(url_for('devices'))
# Prüfen auf doppelte IP außer das aktuelle Gerät
ip_device = conn.execute(
"SELECT name FROM devices WHERE rpi_ip=? AND mac<>?",
(rpi_ip, old_mac)
).fetchone()
if ip_device:
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
return redirect(url_for('devices'))
# Prüfen auf doppelte MAC außer das aktuelle Gerät
mac_device = conn.execute(
"SELECT name FROM devices WHERE mac=? AND mac<>?",
(mac, old_mac)
).fetchone()
if mac_device:
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
return redirect(url_for('devices'))
# Update durchführen
try:
conn.execute("""
UPDATE devices
SET mac=?, rpi_ip=?, port=?, name=?, is_active=?
WHERE mac=?
""", (mac, rpi_ip, port, name, is_active, old_mac))
conn.commit()
flash(f"Gerät {name} aktualisiert.")
except sqlite3.IntegrityError:
flash("Fehler beim Aktualisieren des Geräts!")
# -----------------------
# Gerät löschen
# -----------------------
elif 'delete_device' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('devices'))
del_mac = request.form.get('delete_device')
if del_mac:
device = conn.execute("SELECT name FROM devices WHERE mac=?", (del_mac,)).fetchone()
hostname = device['name'] if device else del_mac
conn.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
conn.commit()
flash(f"Gerät {hostname} gelöscht.")
else:
flash("Keine MAC-Adresse übermittelt!")
return redirect(url_for('devices'))
# -----------------------
# Devices für Anzeige
# -----------------------
devices = conn.execute("""
SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, devices.is_active,
switches.hostname AS switch_hostname
FROM devices
LEFT JOIN switches ON devices.switch_hostname = switches.hostname
ORDER BY switches.hostname ASC, devices.name ASC
""").fetchall()
conn.close()
return render_template('devices.html', devices=devices, switches=switches)
@app.route('/devices/toggle/<mac>', methods=['POST'])
@login_required
def toggle_device(mac):
if not current_user.is_admin:
return {"success": False, "msg": "Zugriff verweigert!"}, 403
conn = get_db_connection()
device = conn.execute("SELECT is_active, name FROM devices WHERE mac=?", (mac,)).fetchone()
if not device:
conn.close()
return {"success": False, "msg": "Gerät nicht gefunden!"}, 404
new_status = 0 if device['is_active'] else 1
conn.execute("UPDATE devices SET is_active=? WHERE mac=?", (new_status, mac))
conn.commit()
conn.close()
status_text = "deaktiviert" if new_status == 0 else "aktiviert"
return {"success": True, "msg": f"Gerät {device['name']} wurde {status_text}.", "new_status": new_status}
@app.route('/switches', methods=['GET', 'POST'])
@login_required
def switches():
conn = get_db_connection()
# Inline-Add
if request.method == 'POST' and 'add_switch' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('switches'))
hostname = request.form['hostname']
ip = request.form['ip']
username = request.form['username']
password = encrypt_password(request.form['password'])
try:
conn.execute("INSERT INTO switches (hostname, ip, username, password) VALUES (?, ?, ?, ?)",
(hostname, ip, username, password))
conn.commit()
flash(f"Switch {hostname} hinzugefügt.")
except sqlite3.IntegrityError:
flash("Hostname existiert bereits oder Eingabefehler!")
# Inline-Edit
if request.method == 'POST' and 'edit_switch' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('switches'))
old_hostname = request.form['old_hostname']
hostname = request.form['hostname']
ip = request.form['ip']
username = request.form['username']
password = encrypt_password(request.form['password'])
try:
conn.execute("""
UPDATE switches
SET hostname=?, ip=?, username=?, password=?
WHERE hostname=?
""", (hostname, ip, username, password, old_hostname))
conn.commit()
flash(f"Switch {hostname} aktualisiert.")
except sqlite3.IntegrityError:
flash("Hostname existiert bereits oder Eingabefehler!")
switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall()
conn.close()
return render_template('switche.html', switches=switches)
@app.route('/switches/delete/<hostname>', methods=['POST'])
@login_required
def delete_switch(hostname):
conn = get_db_connection()
devices = conn.execute("SELECT name FROM devices WHERE switch_hostname=?", (hostname,)).fetchall()
if devices:
device_names = [d['name'] for d in devices]
flash(f"Folgende Geräte sind noch dem Switch {hostname} zugewiesen: {', '.join(device_names)}", "danger")
conn.close()
return redirect(url_for('switches'))
conn.execute("DELETE FROM switches WHERE hostname=?", (hostname,))
conn.commit()
conn.close()
flash(f"Switch '{hostname}' gelöscht.", "success")
return redirect(url_for('switches'))
@app.route("/get_log")
@login_required
def get_log():
log_files = glob.glob("/var/log/rpi-*.log")
if not log_files:
return "Keine Logfiles gefunden."
latest_log = max(log_files, key=os.path.getctime)
try:
with open(latest_log, "r") as f:
return f.read()
except Exception as e:
return f"Fehler beim Lesen des Logs: {e}"
@app.route('/logs')
@login_required
def logs():
# Intervall aus DB laden
conn = sqlite3.connect("sqlite.db")
c = conn.cursor()
c.execute("SELECT value FROM settings WHERE key='interval'")
row = c.fetchone()
interval = int(row[0]) if row else 5 # Default 5 Minuten
conn.close()
# alle Logfiles mit Muster rpi-YYYYMMDDHHMMSS.log
log_files = glob.glob("/var/log/rpi-*.log")
if not log_files:
return render_template('logs.html', log_content="Keine Logfiles gefunden.")
# das neuste Logfile auswählen
latest_log = max(log_files, key=os.path.getctime)
try:
with open(latest_log, "r") as f:
log_content = f.read()
except Exception as e:
log_content = f"Fehler beim Lesen des Logs: {e}"
return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval)
def load_device_status():
status = {}
# Devices aus DB laden (Name → MAC)
conn = sqlite3.connect("sqlite.db")
conn.row_factory = sqlite3.Row
devices = conn.execute("SELECT mac, name FROM devices").fetchall()
name_to_mac = {d['name']: d['mac'] for d in devices}
conn.close()
# Logfile
log_files = glob.glob("/var/log/rpi-*.log")
if not log_files:
return status
latest_log = max(log_files, key=os.path.getctime)
online_re = re.compile(r"(\S+) ist erreichbar!")
offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
with open(latest_log, "r") as f:
for line in f:
line = line.strip()
m_online = online_re.search(line)
m_offline = offline_re.search(line)
if m_online:
name = m_online.group(1)
mac = name_to_mac.get(name)
if mac:
status[mac] = 'online'
elif m_offline:
name = m_offline.group(1)
mac = name_to_mac.get(name)
if mac:
status[mac] = 'offline'
return status
@app.route("/users", methods=["GET", "POST"])
@login_required
def users():
if not current_user.is_admin:
flash("Nur Admins dürfen Benutzer verwalten!")
return redirect(url_for("index"))
conn = sqlite3.connect("sqlite.db")
conn.row_factory = sqlite3.Row
c = conn.cursor()
if request.method == "POST":
# Neuen Benutzer hinzufügen
if "add_user" in request.form:
username = request.form["username"].strip()
password = request.form["password"].strip()
is_admin = int(request.form.get("is_admin", 0))
if username and password:
pw_hash = bcrypt.generate_password_hash(password).decode("utf-8")
try:
c.execute(
"INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
(username, pw_hash, is_admin)
)
conn.commit()
flash(f"Benutzer '{username}' erfolgreich angelegt!")
except sqlite3.IntegrityError:
flash("Benutzername existiert bereits!")
else:
flash("Username und Passwort dürfen nicht leer sein!")
# Rolle ändern
elif "change_role" in request.form:
user_id = request.form["user_id"]
username = request.form.get("username", "").strip()
is_admin = int(request.form.get("is_admin", 0))
if username:
c.execute(
"UPDATE users SET username=?, is_admin=? WHERE id=?",
(username, is_admin, user_id)
)
conn.commit()
flash("Rolle und Username geändert!")
else:
flash("Username darf nicht leer sein!")
# Passwort ändern
elif "change_password" in request.form:
user_id = request.form["user_id"]
new_password = request.form.get("new_password", "").strip()
if new_password:
pw_hash = bcrypt.generate_password_hash(new_password).decode("utf-8")
c.execute(
"UPDATE users SET password=? WHERE id=?",
(pw_hash, user_id)
)
conn.commit()
flash("Passwort erfolgreich geändert!")
else:
flash("Passwort darf nicht leer sein!")
# Benutzer löschen
elif "delete_user" in request.form:
user_id = request.form["delete_user"]
c.execute("DELETE FROM users WHERE id=?", (user_id,))
conn.commit()
flash("Benutzer gelöscht!")
# Alle Benutzer laden (GET oder nach POST)
c.execute("SELECT id, username, is_admin FROM users")
users_list = c.fetchall()
conn.close()
# Direkt rendern, Flash-Messages werden angezeigt
return render_template("users.html", users=users_list)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=True)

33
srv/poe_manager/create_admin.py Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import sqlite3
from getpass import getpass
from flask_bcrypt import Bcrypt
DB_PATH = "/srv/poe_manager/sqlite.db"
bcrypt = Bcrypt()
def main():
username = input("Admin-Benutzername: ")
password = getpass("Passwort: ")
password_confirm = getpass("Passwort bestätigen: ")
if password != password_confirm:
print("Passwörter stimmen nicht überein!")
return
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
try:
cur.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
(username, pw_hash, 1))
conn.commit()
print(f"Admin-Benutzer '{username}' erfolgreich angelegt.")
except sqlite3.IntegrityError:
print("Benutzername existiert bereits!")
finally:
conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
import sqlite3
conn = sqlite3.connect("sqlite.db")
c = conn.cursor()
# Switches
c.execute("""
CREATE TABLE IF NOT EXISTS switches (
hostname TEXT PRIMARY KEY,
ip TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
);
""")
# Devices
c.execute("""
CREATE TABLE devices (
mac TEXT PRIMARY KEY,
rpi_ip TEXT NOT NULL,
switch_hostname TEXT NOT NULL,
port TEXT,
name TEXT NOT NULL,
is_active INTEGER DEFAULT 0,
FOREIGN KEY (switch_hostname) REFERENCES switches(hostname)
);
""")
# Benutzer
c.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
is_admin INTEGER DEFAULT 0
);
""")
# Settings (z.B. Prüfintervall)
c.execute("""
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
""")
# Standard-Setting: Prüfintervall 5 Minuten
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("interval_minutes", "5"))
conn.commit()
conn.close()
print("Datenbank sqlite.db wurde initialisiert inklusive Settings.")

33
srv/poe_manager/create_user.py Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import sqlite3
from getpass import getpass
from flask_bcrypt import Bcrypt
DB_PATH = "/srv/poe_manager/sqlite.db"
bcrypt = Bcrypt()
def main():
username = input("Benutzername: ")
password = getpass("Passwort: ")
password_confirm = getpass("Passwort bestätigen: ")
if password != password_confirm:
print("Passwörter stimmen nicht überein!")
return
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
try:
cur.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
(username, pw_hash, 0))
conn.commit()
print(f"Benutzer '{username}' erfolgreich angelegt.")
except sqlite3.IntegrityError:
print("Benutzername existiert bereits!")
finally:
conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
lKahy0lgo1McpoZtUDBwqddKkkRg7EnRnK3zdCL4_dw=

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import sqlite3
from app import decrypt_password, DB_PATH
def generate_ips_list():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
switches = {row['hostname']: row for row in conn.execute(
"SELECT hostname, ip, username, password FROM switches"
)}
devices = conn.execute("""
SELECT mac, rpi_ip, port, name, switch_hostname
FROM devices
WHERE is_active=1
""").fetchall()
conn.close()
for dev in devices:
switch = switches.get(dev['switch_hostname'])
if switch:
switch_ip = switch['ip']
switch_user = switch['username']
switch_pass = decrypt_password(switch['password'])
else:
switch_ip = ""
switch_user = ""
switch_pass = ""
port = dev['port'] or ""
print(f"{dev['rpi_ip']}:{dev['name']}:{switch_ip}:{dev['switch_hostname'] or 'kein Switch'}:{port}:{switch_user}:{switch_pass}")
if __name__ == "__main__":
generate_ips_list()

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import subprocess
import os
# Virtuelle Umgebung aktivieren (Pfad zu deinem venv)
venv_python = "/srv/poe_manager/venv/bin/python3"
# Poe.sh über bash ausführen, innerhalb der venv
subprocess.run(["/bin/bash", "/usr/local/bin/custom/poe.sh"], env={"PATH": f"/srv/poe_manager/venv/bin:" + os.environ["PATH"]})

View File

@@ -0,0 +1,13 @@
bcrypt==5.0.0
blinker==1.9.0
cffi==2.0.0
click==8.3.0
cryptography==46.0.1
Flask==3.1.2
Flask-Bcrypt==1.0.1
Flask-Login==0.6.3
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
pycparser==2.23
Werkzeug==3.1.3

BIN
srv/poe_manager/sqlite.db Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,177 @@
body {
padding: 20px;
background: #3c4346;
}
h1, h2, h3, h4, h5, h6 {
color: #fff;
}
.table-dark {
background-color: #343a40 !important;
color: #fff !important;
}
.badge {
font-size: 0.9rem;
}
pre {
background-color: #f8f9fa7c;
padding: 10px;
border-radius: 5px;
max-height: 600px;
overflow-y: scroll;
}
.mb-3 a.btn {
margin-right: 5px;
}
#dashboard-timer {
font-size: 0.9rem;
padding: 0.25rem 0.5rem;
}
.device-card {
border: 1px solid #ccc;
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
text-align: center;
min-width: 150px;
}
.device-name {
margin-bottom: 0.5rem;
font-weight: bold;
}
.status-online {
color: green;
font-weight: bold;
}
.status-offline {
color: red;
font-weight: bold;
}
.content-wrapper {
max-width: 1080px;
margin-left: auto;
margin-right: auto;
}
.navbar {
display: flex;
justify-content: flex-end;
}
.navbar-container {
display: flex;
justify-content: space-between; /* Logo links, Navbar rechts */
align-items: center;
margin-bottom: 1rem;
}
.navbar-buttons .btn {
margin-right: 0.25rem; /* kleine Lücke zwischen Buttons */
}
.navbar-logo img {
display: flex;
justify-content: flex-start;
height: 83px; /* Logo Höhe, kann angepasst werden */
}
#log-container {
position: relative;
height: calc(100vh - 252px); /* Füllt die Seite minus Header */
padding: 1rem; /* optional, Abstand innen */
box-sizing: border-box; /* damit Padding nicht die Höhe sprengt */
display: flex;
flex-direction: column;
}
#log-box {
flex: 1 1 auto; /* füllt den Container, berücksichtigt Header/Padding */
overflow: auto;
white-space: pre-wrap;
font-family: monospace;
border: 1px solid #dee2e6;
background-color: #f8f9fa;
padding: 0.5rem;
}
#refresh-timer {
position: absolute;
bottom: 18px;
right: 30px;
font-size: 0.9em;
color: gray;
background-color: rgba(255,255,255,0.7); /* optional, besser lesbar */
padding: 2px 4px;
border-radius: 3px;
pointer-events: none; /* damit Scrollbar nicht blockiert wird */
}
/* Tabelle anpassen */
.custom-table input,
.custom-table select {
height: 28px;
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
/* Spaltenbreiten */
.custom-table .col-small {
width: 140px; /* Hostname, Port */
}
.custom-table .col-ip {
width: 140px; /* IP-Adresse schmaler */
}
.custom-table .col-mac {
width: 140px; /* MAC-Adresse schmaler */
}
/* Checkbox sichtbar auch bei disabled */
.checkbox-visible:disabled {
opacity: 1; /* nicht ausgegraut */
cursor: not-allowed;
}
/* Aktiv-Label für Neues Gerät */
label.text-white {
color: white !important;
}
.checkbox-visible:not(:checked) {
accent-color: #dc3545; /* rot wenn unchecked */
}
.checkbox-visible:disabled {
cursor: not-allowed;
opacity: 1; /* verhindert Ausgrauen */
}
.custom-actions {
margin: auto 0;
background: #fff;
}
.black {
color: #000000;
}
.white {
color: #ffffff;
}
button.login {
background-color: #ff7100;
border-color: #ff7300a9;;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ title or "PoE Manager" }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body class="p-4">
<div class="content-wrapper">
<!-- Logo + Navbar -->
<div class="navbar-container">
<div class="navbar-logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
</div>
<div class="navbar">
<div class="navbar-buttons btn-group" role="group">
<a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a>
<a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a>
{% if current_user.is_admin %}
<a href="{{ url_for('switches') }}" class="btn btn-secondary">Switche</a>
<a href="{{ url_for('users') }}" class="btn btn-secondary">Users</a>
<a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a>
<a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a>
{% endif %}
<a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>
</div>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<div id="flash-messages" class="mt-2">
{% for message in messages %}
<div class="alert alert-info alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Content -->
<div>
{% block content %}{% endblock %}
</div>
</div>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script>
// Automatisch alle Alerts nach 5 Sekunden ausblenden
document.addEventListener("DOMContentLoaded", function() {
setTimeout(() => {
const alerts = document.querySelectorAll('#flash-messages .alert');
alerts.forEach(alert => {
bootstrap.Alert.getOrCreateInstance(alert).close();
});
}, 5000);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,296 @@
{% extends "base.html" %}
{% block content %}
<h2>Devices</h2>
{% if current_user.is_admin %}
<!-- Neues Gerät hinzufügen -->
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>Hostname</th>
<th>IP-Adresse</th>
<th>MAC-Adresse</th>
<th>Switch</th>
<th>Port</th>
{% if current_user.is_admin %}<th>Status</th>{% endif %}
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</tr>
</thead>
<tbody>
{% for d in devices %}
<tr>
<td>{{ d['name'] }}</td>
<td>{{ d['rpi_ip'] }}</td>
<td>{{ d['mac'] }}</td>
<td>{{ d['switch_hostname'] or '-' }}</td>
<td>{{ d['port'] }}</td>
{% if current_user.is_admin %}
<td>
<button class="btn btn-sm {% if d['is_active'] %}btn-success{% else %}btn-secondary{% endif %}"
onclick="toggleDevice('{{ d['mac'] }}', this)">
{% if d['is_active'] %}Deaktivieren{% else %}Aktivieren{% endif %}
</button>
</td>
<td>
<!-- Bearbeiten Modal -->
<button class="btn btn-secondary btn-sm button login white" data-bs-toggle="modal" data-bs-target="#editDeviceModal"
onclick="openEditDeviceModal('{{ d['mac'] }}','{{ d['name'] }}','{{ d['rpi_ip'] }}','{{ d['port'] }}')">
Bearbeiten
</button>
<!-- Switch ändern Modal -->
<button class="btn btn-secondary btn-sm button login white" data-bs-toggle="modal" data-bs-target="#switchModal"
onclick="openSwitchModal('{{ d['mac'] }}','{{ d['switch_hostname'] }}')">
Switch ändern
</button>
<!-- Löschen -->
<form method="post" style="display:inline;">
<input type="hidden" name="delete_device" value="{{ d['mac'] }}">
<button class="btn btn-danger btn-sm" onclick="return confirm('Willst du das Gerät wirklich löschen?');">
Löschen
</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<!-- Modal: Neues Gerät -->
<div class="modal fade" id="deviceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" onsubmit="return validateDeviceForm(this);">
<div class="modal-header">
<h5 class="modal-title black">Neues Gerät hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="add_device" value="1">
<div class="mb-3">
<label>Hostname</label>
<input type="text" name="name" class="form-control" required placeholder="z.B. Sensor01">
</div>
<div class="mb-3">
<label>IP-Adresse</label>
<input type="text" name="rpi_ip" class="form-control" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label>MAC-Adresse</label>
<input type="text" name="mac" class="form-control" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
<div class="invalid-feedback">Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).</div>
</div>
<div class="mb-3">
<label>Port</label>
<input type="text" name="port" class="form-control" placeholder="z.B. 3">
</div>
<div class="mb-3">
<label>Switch (optional)</label>
<select name="switch_hostname" class="form-select">
<option value="">Kein Switch</option>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success button login" type="submit">Hinzufügen</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal: Bearbeiten -->
<div class="modal fade" id="editDeviceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" onsubmit="return validateDeviceForm(this);">
<input type="hidden" name="edit_device" value="1">
<input type="hidden" name="old_mac" id="edit_old_mac">
<div class="modal-header">
<h5 class="modal-title black">Gerät bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label>Hostname</label>
<input type="text" name="name" id="edit_name" class="form-control" required placeholder="z.B. Sensor01">
</div>
<div class="mb-3">
<label>IP-Adresse</label>
<input type="text" name="rpi_ip" id="edit_ip" class="form-control" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label>MAC-Adresse</label>
<input type="text" name="mac" id="edit_mac" class="form-control" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
<div class="invalid-feedback">Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).</div>
</div>
<div class="mb-3">
<label>Port</label>
<input type="text" name="port" id="edit_port" class="form-control" placeholder="z.B. 3">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary button login" type="submit">Speichern</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal: Switch ändern -->
<div class="modal fade" id="switchModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<input type="hidden" name="edit_device" value="1">
<input type="hidden" name="old_mac" id="switch_mac">
<div class="modal-header">
<h5 class="modal-title black">Switch ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<select name="switch_hostname" id="switch_select" class="form-select">
<option value="">Kein Switch</option>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button class="btn btn-primary button login" type="submit">Speichern</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openDeviceModal() {
document.querySelector("#deviceModal form").reset();
}
function openEditDeviceModal(mac, name, ip, port) {
document.getElementById("edit_old_mac").value = mac;
document.getElementById("edit_name").value = name;
document.getElementById("edit_ip").value = ip;
document.getElementById("edit_mac").value = mac;
document.getElementById("edit_port").value = port;
}
function openSwitchModal(mac, switch_hostname) {
document.getElementById("switch_mac").value = mac;
document.getElementById("switch_select").value = switch_hostname;
}
// -------------------
// IP-Validierung
// -------------------
const ipPattern = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
// -------------------
// MAC-Validierung
// -------------------
const macPattern = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
function validateIP(input) {
if (!ipPattern.test(input.value)) {
input.classList.add("is-invalid");
return false;
} else {
input.classList.remove("is-invalid");
return true;
}
}
function validateMAC(input) {
if (!macPattern.test(input.value)) {
input.classList.add("is-invalid");
return false;
} else {
input.classList.remove("is-invalid");
return true;
}
}
function validateDeviceForm(form) {
const ipInput = form.querySelector("input[name='rpi_ip']");
const macInput = form.querySelector("input[name='mac']");
let valid = true;
if (ipInput) valid = validateIP(ipInput) && valid;
if (macInput) valid = validateMAC(macInput) && valid;
return valid;
}
// Live-Feedback beim Tippen
document.addEventListener("input", function(e) {
if (e.target.name === "rpi_ip") validateIP(e.target);
if (e.target.name === "mac") validateMAC(e.target);
});
// Aktivieren/Deaktivieren
function toggleDevice(mac, btn) {
fetch(`/devices/toggle/${mac}`, { method: 'POST' })
.then(resp => resp.json())
.then(data => {
if (data.success) {
// Button aktualisieren
if (data.new_status === 1) {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-success');
btn.innerText = 'Deaktivieren';
} else {
btn.classList.remove('btn-success');
btn.classList.add('btn-secondary');
btn.innerText = 'Aktivieren';
}
// Flash-Nachricht im Frontend aktualisieren
let flashContainer = document.getElementById('flash-messages');
if (!flashContainer) {
flashContainer = document.createElement('div');
flashContainer.id = 'flash-messages';
flashContainer.style.position = 'fixed';
flashContainer.style.top = '10px';
flashContainer.style.right = '10px';
flashContainer.style.zIndex = 1050;
document.body.appendChild(flashContainer);
}
const flash = document.createElement('div');
flash.className = 'alert alert-success alert-dismissible fade show';
flash.role = 'alert';
flash.innerHTML = `
${data.msg}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
flashContainer.appendChild(flash);
// Automatisch nach 3 Sekunden ausblenden
setTimeout(() => {
flash.classList.remove('show');
flash.classList.add('hide');
flash.addEventListener('transitionend', () => flash.remove());
}, 3000);
} else {
console.error(data.msg);
}
})
.catch(err => console.error(err));
}
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block content %}
<h2 class="d-flex justify-content-between align-items-center">
Dashboard
<span id="dashboard-timer" class="badge bg-success">
Nächste Prüfung in -- Sekunden
</span>
</h2>
<div class="row g-3">
{% for letter, group in grouped_devices.items() %}
<div class="row g-3">
{% for d in group %}
<div class="col-6 col-md-4 col-lg-3 col-xl-2">
<div class="card text-center p-2"
{% if last_seen.get(d[0]) %}
title="{{ last_seen[d[0]] }}"
{% elif status[d[0]] == 'offline' %}
title="Noch nie online"
{% endif %}>
<div class="card-header">{{ d[1] }}</div>
<div class="card-body">
<span class="fw-bold" style="color:
{% if d[2] == 0 %}gray
{% elif status[d[0]] == 'online' %}green
{% else %}red
{% endif %};">
{% if d[2] == 0 %}
Deaktiviert
{% else %}
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %}
{% endif %}
</span>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const intervalMinutes = {{ interval | int }}; // aus DB
const intervalMilliseconds = intervalMinutes * 60 * 1000;
let lastUpdateTime = Date.now();
function parseLogTimestamp(ts) {
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!parts) return Date.now();
const [, year, month, day, hour, minute, second] = parts.map(Number);
return new Date(year, month - 1, day, hour, minute, second).getTime();
}
function updateTimer() {
const now = Date.now();
const elapsed = now - lastUpdateTime;
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
const remainingSec = Math.ceil(remainingMs / 1000);
document.getElementById("dashboard-timer").innerText =
`Nächste Prüfung in ${remainingSec} Sekunden`;
if (remainingSec <= 1) {
// Timer abgelaufen → Reload starten
window.location.reload();
}
}
function fetchLastLog() {
fetch("{{ url_for('get_log') }}")
.then(response => response.text())
.then(data => {
const lines = data.split("\n");
let lastSepIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("--------------------------------------------------------------------")) {
lastSepIndex = i;
break;
}
}
if (lastSepIndex >= 0 && lastSepIndex + 1 < lines.length) {
const firstLine = lines[lastSepIndex + 1];
const match = firstLine.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
if (match) lastUpdateTime = parseLogTimestamp(match[1]);
}
})
.catch(err => console.error("Fehler beim Laden der Logs:", err));
}
// Timer alle 1 Sekunde aktualisieren
setInterval(updateTimer, 1000);
// einmal beim Laden die letzte Log-Zeit setzen
fetchLastLog();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body class="p-5">
<h2>Login</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mt-2 alert alert-danger">
{% for message in messages %}{{ message }}<br>{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="post" class="w-25">
<div class="mb-3">
<label class="form-label label text-white">Username</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label label text-white">Password</label>
<input type="password" name="password" class="form-control" required>
</div>
<button class="btn btn-primary button login">Login</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block content %}
<h2>Live Log</h2>
<button id="refresh-btn" class="btn btn-success mb-3">
<span style="font-size: 1.2rem; color: white;"></span> Logs aktualisieren
</button>
<div id="log-container">
<div id="log-box"></div>
<div id="refresh-timer">
Nächstes Update in <span id="timer"></span> Sekunden
</div>
</div>
<script>
function parseLogTimestamp(ts) {
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!parts) return Date.now();
const [, year, month, day, hour, minute, second] = parts.map(Number);
return new Date(year, month - 1, day, hour, minute, second).getTime();
}
document.addEventListener("DOMContentLoaded", () => {
const intervalMinutes = {{ interval | int }};
const intervalMilliseconds = intervalMinutes * 60 * 1000;
let lastUpdateTime = Date.now();
function fetchLog() {
fetch("{{ url_for('get_log') }}")
.then(res => res.text())
.then(data => {
const box = document.getElementById("log-box");
const filteredLines = data
.split("\n");
box.innerText = filteredLines.join("\n");
box.scrollTop = box.scrollHeight;
// letzte Separator-Linie
let lastSep = -1;
for (let i = filteredLines.length - 1; i >= 0; i--) {
if (filteredLines[i].startsWith("--------------------------------------------------------------------")) {
lastSep = i;
break;
}
}
if (lastSep >= 0 && lastSep + 1 < filteredLines.length) {
const firstLine = filteredLines[lastSep + 1];
const match = firstLine.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
if (match) {
lastUpdateTime = parseLogTimestamp(match[1]);
} else {
lastUpdateTime = Date.now();
}
} else {
lastUpdateTime = Date.now();
}
})
.catch(err => console.error(err));
}
function updateTimer() {
const now = Date.now();
const elapsed = now - lastUpdateTime;
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
const remainingSec = Math.ceil(remainingMs / 1000);
document.getElementById("timer").innerText = remainingSec;
}
document.getElementById("refresh-btn").addEventListener("click", fetchLog);
fetchLog();
setInterval(fetchLog, intervalMilliseconds);
setInterval(updateTimer, 1000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}
<h2>Settings</h2>
<form method="post" class="row g-2">
<div class="col-auto">
<label for="interval" class="form-label white">Prüfintervall (Minuten):</label>
<input type="number" name="interval" id="interval" class="form-control" value="{{ interval }}" min="1" required>
</div>
<div class="col-auto align-self-end">
<button type="submit" class="btn btn-success">Speichern & Service neustarten</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,183 @@
{% extends "base.html" %}
{% block content %}
<h2>Switch Verwaltung</h2>
<!-- Button zum Hinzufügen -->
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#addSwitchModal">Neuen Switch hinzufügen</button>
<table class="table table-striped">
<thead>
<tr>
<th>Hostname</th>
<th>IP-Adresse</th>
<th>Username</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for s in switches %}
<tr>
<td>{{ s['hostname'] }}</td>
<td>{{ s['ip'] }}</td>
<td>{{ s['username'] }}</td>
<td>
<!-- Bearbeiten -->
<button class="btn btn-sm button login white" data-bs-toggle="modal" data-bs-target="#editSwitchModal{{ loop.index }}">Bearbeiten</button>
<!-- Löschen -->
<form method="post" action="{{ url_for('delete_switch', hostname=s['hostname']) }}" style="display:inline;">
<button class="btn btn-danger btn-sm" onclick="return confirm('Willst du den Switch wirklich löschen?');">
Löschen
</button>
</form>
</td>
</tr>
<!-- Bearbeiten Modal -->
<div class="modal fade" id="editSwitchModal{{ loop.index }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-dark">Switch bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" onsubmit="return validateSwitchForm(this, '{{ loop.index }}')">
<div class="modal-body">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<div class="mb-3">
<label class="form-label">Hostname</label>
<input type="text" name="hostname" class="form-control" value="{{ s['hostname'] }}" required>
</div>
<div class="mb-3">
<label class="form-label">IP-Adresse</label>
<input type="text" name="ip" class="form-control" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" value="{{ s['username'] }}" required>
</div>
<div class="mb-3">
<label class="form-label">Neues Passwort</label>
<input type="password" id="password_edit_{{ loop.index }}" name="password" class="form-control" placeholder="Neues Passwort (optional)">
</div>
<div class="mb-3">
<label class="form-label">Passwort bestätigen</label>
<input type="password" id="password_confirm_edit_{{ loop.index }}" name="password_confirm" class="form-control" placeholder="Bestätigen">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button class="btn button login white" type="submit">Speichern</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>
<!-- Hinzufügen Modal -->
<div class="modal fade" id="addSwitchModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-dark">Neuen Switch hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" onsubmit="return validateSwitchForm(this, 'add')">
<div class="modal-body">
<input type="hidden" name="add_switch" value="1">
<div class="mb-3">
<label class="form-label">Hostname</label>
<input type="text" name="hostname" class="form-control" required placeholder="z.B. Switch01">
</div>
<div class="mb-3">
<label class="form-label">IP-Adresse</label>
<input type="text" name="ip" class="form-control" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" required placeholder="z.B. admin">
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" id="password_add" name="password" class="form-control" required placeholder="z.B. geheim123">
</div>
<div class="mb-3">
<label class="form-label">Passwort bestätigen</label>
<input type="password" id="password_confirm_add" name="password_confirm" class="form-control" required placeholder="Passwort wiederholen">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button class="btn button login white" type="submit">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
<!-- JS: Passwort + IP-Validierung -->
<script>
// Passwortcheck
function validatePassword(id, isEdit) {
let pass = document.getElementById(isEdit ? `password_edit_${id}` : `password_${id}`) || document.getElementById('password_add');
let confirm = document.getElementById(isEdit ? `password_confirm_edit_${id}` : `password_confirm_${id}`) || document.getElementById('password_confirm_add');
if (!pass || !confirm) return true;
if (pass.value !== confirm.value) {
confirm.classList.add("is-invalid");
return false;
} else {
confirm.classList.remove("is-invalid");
return true;
}
}
// -------------------
// IP-Validierung
// -------------------
const ipPattern = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
function validateIP(input) {
if (!ipPattern.test(input.value)) {
input.classList.add("is-invalid");
return false;
} else {
input.classList.remove("is-invalid");
return true;
}
}
function validateSwitchForm(form, id) {
const ipInput = form.querySelector("input[name='ip']");
let valid = true;
if (ipInput) valid = validateIP(ipInput) && valid;
// Passwortcheck
valid = validatePassword(id, id !== 'add') && valid;
return valid;
}
// Live-Feedback IP & Passwort
document.addEventListener('input', e => {
if (e.target.name === "ip") validateIP(e.target);
if (e.target.id.startsWith('password_confirm')) {
const id = e.target.id.replace('password_confirm_', '');
const pass = document.getElementById('password_' + id);
if (pass && pass.value !== e.target.value) {
e.target.classList.add('is-invalid');
} else {
e.target.classList.remove('is-invalid');
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,167 @@
{% extends "base.html" %}
{% block content %}
<h2>Users</h2>
{% if current_user.is_admin %}
<!-- Button Neues User-Popup -->
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th class="col-ip">Username</th>
<th class="col-ip">Rolle</th>
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td class="col-small">{{ u['username'] }}</td>
<td class="col-small">{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td>
{% if current_user.is_admin %}
<td class="custom-actions d-flex gap-1 align-items-center">
<!-- Rolle ändern -->
<button class="btn btn-sm button login white" data-bs-toggle="modal"
data-bs-target="#roleModal"
onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})">
Rolle ändern
</button>
<!-- Passwort ändern -->
<button class="btn btn-sm button login white" data-bs-toggle="modal"
data-bs-target="#passwordModal"
onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')">
Passwort ändern
</button>
<!-- Benutzer löschen -->
<form method="post" style="display:inline;">
<button name="delete_user" value="{{ u['id'] }}" class="btn btn-danger btn-sm"
onclick="return confirm('Willst du den Benutzer wirklich löschen?');">Löschen</button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<!-- Modals bleiben unverändert -->
<!-- Modal für neuen Benutzer -->
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" id="userForm">
<div class="modal-header">
<h5 class="modal-title black">Neuen Benutzer anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label>Username</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label>Passwort</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label>Rolle</label>
<select name="is_admin" class="form-control">
<option value="0">User</option>
<option value="1">Admin</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_user" value="1" class="btn btn-success">Benutzer anlegen</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal für Rolle ändern -->
<div class="modal fade" id="roleModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" id="roleForm">
<div class="modal-header">
<h5 class="modal-title black">Rolle ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="user_id" id="role_user_id">
<div class="mb-3">
<label>Username</label>
<input type="text" name="username" id="role_username" class="form-control" required>
</div>
<div class="mb-3">
<label>Rolle</label>
<select name="is_admin" id="role_is_admin" class="form-control">
<option value="0">User</option>
<option value="1">Admin</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="change_role" value="1" class="btn btn-primary">Rolle ändern</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal für Passwort ändern -->
<div class="modal fade" id="passwordModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" id="passwordForm">
<div class="modal-header">
<h5 class="modal-title black">Passwort ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="user_id" id="password_user_id">
<div class="mb-3">
<label>Username</label>
<input type="text" name="username" id ="password_username" class="form-control" readonly>
</div>
<div class="mb-3">
<label>Neues Passwort</label>
<input type="password" name="new_password" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="change_password" value="1" class="btn btn-warning">Passwort ändern</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openUserModal() {
document.getElementById('userForm').reset();
}
function openRoleModal(user_id, username, is_admin) {
document.getElementById('role_user_id').value = user_id;
document.getElementById('role_username').value = username;
document.getElementById('role_is_admin').value = is_admin;
}
function openPasswordModal(user_id, username) {
document.getElementById('password_user_id').value = user_id;
document.getElementById('password_username').value = username;
document.querySelector("#passwordForm input[name='new_password']").value = "";
}
</script>
{% endblock %}

View File

@@ -1,8 +0,0 @@
#IP-RPI:IP-SWITCH:PORT:NAME
192.168.120.104:192.168.200.117:37:HAP04
192.168.120.110:192.168.200.116:1/14:HAP10
192.168.120.114:192.168.200.116:1/26:HAP14
192.168.120.115:192.168.200.116:1/23:HAP15
192.168.120.118:192.168.200.116:1/34:HAP18
192.168.120.123:192.168.200.117:36:HAP23
192.168.120.131:192.168.200.118:1/36:HAP31

View File

@@ -1,35 +1,55 @@
#!/bin/bash
# ============================================================================
# PoE Device Check Script
# - prüft Erreichbarkeit von Geräten
# - startet PoE-Port bei Ausfall neu
# - loggt Ereignisse
# - löscht alte Logfiles (>30 Tage)
# ============================================================================
USER="admin"
PASS="Expl0rer#2022"
IP_FILE="/usr/local/bin/custom/ips.list"
LOGFILE="/var/log/rpi-$(date '+%Y%m%d%H%M%S').log"
LOG_DIR="/var/log"
LOGFILE="$LOG_DIR/rpi-$(date '+%Y%m%d%H%M%S').log"
# Alte Logfiles löschen (älter als 30 Tage)
find "$LOG_DIR" -type f -name "rpi-*.log" -mtime +30 -delete
# Intervall aus DB abrufen
SLEEP=$(python3 - <<'END'
import sqlite3
conn = sqlite3.connect("/srv/poe_manager/sqlite.db")
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
conn.close()
print(row[0] if row else 300)
END
)
SLEEP=${SLEEP:-300}
function disable_poe() {
local switch_ip=$1
local port=$2
local switch_port=$2
local username=$3
local password=$4
expect <<EOF
set timeout 5
spawn ssh $USER@$switch_ip
spawn ssh $username@$switch_ip
expect {
"assword:" { send "$PASS\r"; exp_continue }
"assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue }
-re ".*> $" { }
}
send "configure terminal\r"
expect "(config)#"
send "interface $port\r"
expect "(eth-$port)#"
send "interface $switch_port\r"
expect "(eth-$switch_port)#"
send "no power-over-ethernet\r"
expect "(eth-$port)#"
expect "(eth-$switch_port)#"
send "exit\r"
expect "(config)#"
send "exit\r"
expect "#"
send "exit\r"
expect ">"
send "exit\r"
expect ">"; send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof
EOF
@@ -37,49 +57,75 @@ EOF
function enable_poe() {
local switch_ip=$1
local port=$2
local switch_port=$2
local username=$3
local password=$4
expect <<EOF
set timeout 5
spawn ssh $USER@$switch_ip
spawn ssh $username@$switch_ip
expect {
"assword:" { send "$PASS\r"; exp_continue }
"assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue }
-re ".*> $" { }
}
send "configure terminal\r"
expect "(config)#"
send "interface $port\r"
expect "(eth-$port)#"
send "interface $switch_port\r"
expect "(eth-$switch_port)#"
send "power-over-ethernet\r"
expect "(eth-$port)#"
expect "(eth-$switch_port)#"
send "exit\r"
expect "(config)#"
send "exit\r"
expect "#"
send "exit\r"
expect ">"
send "exit\r"
expect ">"; send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof
EOF
}
echo "" > $LOGFILE
echo "" > "$LOGFILE"
MAX_PARALLEL=10 # maximal gleichzeitig laufende Geräte
while true; do
echo "--------------------------------------------------------------------" >> $LOGFILE
while IFS=: read -r ip switch port hap; do
ping -c 1 -W 2 $ip &> /dev/null
if [ $? -ne 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap ist nicht erreichbar!" >> $LOGFILE
disable_poe $switch $port
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap PoE auf Port $port für IP $ip am Switch $switch deaktiviert." >> $LOGFILE
sleep 2
enable_poe $switch $port
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap PoE auf Port $port für IP $ip am Switch $switch aktiviert." >> $LOGFILE
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $hap ist erreichbar!" >> $LOGFILE
fi
done < "$IP_FILE"
sleep 300
done
echo "--------------------------------------------------------------------" >> "$LOGFILE"
python3 /srv/poe_manager/generate_ips.py | while IFS=: read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass; do
# Funktion für ein Gerät
check_device() {
local rpi_ip="$1"
local dev_name="$2"
local switch_ip="$3"
local switch_hostname="$4"
local switch_port="$5"
local switch_user="$6"
local switch_pass="$7"
if ping -c 4 -W 1 "$rpi_ip" &> /dev/null; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE"
if [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; then
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
fi
fi
}
# Job in Hintergrund starten
check_device "$rpi_ip" "$dev_name" "$switch_ip" "$switch_hostname" "$switch_port" "$switch_user" "$switch_pass" &
# Limit auf MAX_PARALLEL Jobs
while [ "$(jobs -rp | wc -l)" -ge "$MAX_PARALLEL" ]; do
sleep 0.2
done
done
wait # alle Hintergrundjobs beenden, bevor sleep
sleep "$SLEEP"
done