Compare commits

..

213 Commits

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
19 changed files with 1096 additions and 310 deletions

6
.gitignore vendored
View File

@@ -17,6 +17,7 @@ __pycache__/
!etc/systemd/system/ !etc/systemd/system/
!etc/systemd/system/rpi*.service !etc/systemd/system/rpi*.service
!etc/systemd/system/rpi*.timer !etc/systemd/system/rpi*.timer
!etc/systemd/system/poe*.service
!etc/nginx/ !etc/nginx/
!etc/nginx/sites-enabled/ !etc/nginx/sites-enabled/
!etc/nginx/sites-enabled/default !etc/nginx/sites-enabled/default
@@ -39,7 +40,8 @@ __pycache__/
!/srv/poe_manager/static/css/* !/srv/poe_manager/static/css/*
!/srv/poe_manager/static/js/ !/srv/poe_manager/static/js/
!/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) # Optional: SQLite DB ignorieren (falls du nicht willst, dass Passwörter im Repo landen)
# /srv/poe_manager/sqlite.db !/srv/poe_manager/sqlite.db

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,8 +3,9 @@ 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_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
import sqlite3 from datetime import datetime
import glob, os, re from collections import defaultdict
import sqlite3, glob, os, re
app = Flask(__name__) app = Flask(__name__)
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928" app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
@@ -47,7 +48,7 @@ def get_devices():
Liefert eine Liste aller Devices aus der Datenbank als Dictionaries. Liefert eine Liste aller Devices aus der Datenbank als Dictionaries.
""" """
conn = get_db_connection() conn = get_db_connection()
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall() devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname, is_active FROM devices ORDER BY name ASC").fetchall()
conn.close() conn.close()
return devices return devices
@@ -81,28 +82,84 @@ def logout():
logout_user() logout_user()
return redirect(url_for('login')) 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("/") @app.route("/")
@login_required @login_required
def index(): def index():
# Geräte aus DB laden
conn = sqlite3.connect("sqlite.db") conn = sqlite3.connect("sqlite.db")
c = conn.cursor() c = conn.cursor()
c.execute("SELECT mac, name FROM devices") c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC")
devices = c.fetchall() 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() conn.close()
# Status aus letztem Log ermitteln # Status aus Logdateien ermitteln
import glob, os
log_files = glob.glob("/var/log/rpi-*.log") log_files = glob.glob("/var/log/rpi-*.log")
status_dict = {} status_dict = {}
last_seen_dict = {}
if log_files: if log_files:
latest_log = max(log_files, key=os.path.getctime) latest_log = max(log_files, key=os.path.getctime)
with open(latest_log, "r") as f: with open(latest_log, "r") as f:
for line in f: lines = f.readlines()
for dev in devices:
if dev[1] in line:
status_dict[dev[0]] = "online" if "erreichbar" in line else "offline"
return render_template("index.html", devices=devices, status=status_dict) 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"]) @app.route("/settings", methods=["GET", "POST"])
@login_required @login_required
@@ -121,12 +178,23 @@ def settings():
if request.method == "POST": if request.method == "POST":
new_interval = int(request.form["interval"]) 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") conn = sqlite3.connect("sqlite.db")
c = conn.cursor() c = conn.cursor()
# upsert # interval (Minuten)
c.execute("INSERT INTO settings (key, value) VALUES (?, ?) " c.execute("""
"ON CONFLICT(key) DO UPDATE SET value=excluded.value", INSERT INTO settings (key, value) VALUES (?, ?)
("interval", new_interval)) 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.commit()
conn.close() conn.close()
@@ -134,7 +202,7 @@ def settings():
import subprocess import subprocess
subprocess.run(["systemctl", "restart", "rpi-check.service"]) subprocess.run(["systemctl", "restart", "rpi-check.service"])
flash(f"Intervall auf {new_interval} Minuten gesetzt und Service neu gestartet!") flash(f"Intervall auf {interval_minutes} Minuten gesetzt und Service neu gestartet!")
return redirect(url_for("settings")) return redirect(url_for("settings"))
return render_template("settings.html", interval=interval) return render_template("settings.html", interval=interval)
@@ -145,65 +213,180 @@ def devices():
conn = get_db_connection() conn = get_db_connection()
switches = conn.execute("SELECT hostname FROM switches").fetchall() switches = conn.execute("SELECT hostname FROM switches").fetchall()
# Inline-Add if request.method == 'POST':
if request.method == 'POST' and 'add_device' in request.form: # -----------------------
# Gerät hinzufügen
# -----------------------
if 'add_device' in request.form:
if not current_user.is_admin: if not current_user.is_admin:
flash("Zugriff verweigert!") flash("Zugriff verweigert!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
mac = request.form['mac']
rpi_ip = request.form['rpi_ip'] mac = request.form.get('mac')
port = request.form['port'] rpi_ip = request.form.get('rpi_ip')
name = request.form['name'] port = request.form.get('port')
switch_hostname = request.form['switch_hostname'] 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: try:
conn.execute("INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname) VALUES (?, ?, ?, ?, ?)", conn.execute("""
(mac, rpi_ip, port, name, switch_hostname)) INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname, is_active)
VALUES (?, ?, ?, ?, ?, ?)
""", (mac, rpi_ip, port, name, switch_hostname, is_active))
conn.commit() conn.commit()
flash(f"Gerät {name} hinzugefügt.") flash(f"Gerät {name} hinzugefügt.")
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("MAC existiert bereits oder Eingabefehler!") flash("Fehler beim Hinzufügen des Geräts!")
# Inline-Edit # -----------------------
if request.method == 'POST' and 'edit_device' in request.form: # Gerät bearbeiten
# -----------------------
elif 'edit_device' in request.form:
if not current_user.is_admin: if not current_user.is_admin:
flash("Zugriff verweigert!") flash("Zugriff verweigert!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
old_mac = request.form['old_mac']
mac = request.form['mac'] old_mac = request.form.get('old_mac')
rpi_ip = request.form['rpi_ip'] mac = request.form.get('mac')
port = request.form['port'] rpi_ip = request.form.get('rpi_ip')
name = request.form['name'] port = request.form.get('port') or None
switch_hostname = request.form['switch_hostname'] 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: try:
conn.execute(""" conn.execute("""
UPDATE devices UPDATE devices
SET mac=?, rpi_ip=?, port=?, name=?, switch_hostname=? SET switch_hostname=?
WHERE mac=? WHERE mac=?
""", (mac, rpi_ip, port, name, switch_hostname, old_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() conn.commit()
flash(f"Gerät {name} aktualisiert.") flash(f"Gerät {name} aktualisiert.")
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("MAC existiert bereits oder Eingabefehler!") flash("Fehler beim Aktualisieren des Geräts!")
# Inline-Delete
if request.method == 'POST' and 'delete_device' in request.form: # -----------------------
# Gerät löschen
# -----------------------
elif 'delete_device' in request.form:
if not current_user.is_admin: if not current_user.is_admin:
flash("Zugriff verweigert!") flash("Zugriff verweigert!")
return redirect(url_for('devices')) return redirect(url_for('devices'))
del_mac = request.form['delete_device']
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.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
conn.commit() conn.commit()
flash(f"Gerät {del_mac} gelöscht.") 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(""" devices = conn.execute("""
SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, switches.hostname AS switch_hostname SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, devices.is_active,
switches.hostname AS switch_hostname
FROM devices FROM devices
JOIN switches ON devices.switch_hostname = switches.hostname LEFT JOIN switches ON devices.switch_hostname = switches.hostname
ORDER BY switches.hostname ASC, devices.name ASC
""").fetchall() """).fetchall()
conn.close() conn.close()
interval_min = get_interval_seconds() // 60
return render_template('devices.html', devices=devices, switches=switches) 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']) @app.route('/switches', methods=['GET', 'POST'])
@login_required @login_required
def switches(): def switches():
@@ -247,20 +430,27 @@ def switches():
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("Hostname existiert bereits oder Eingabefehler!") flash("Hostname existiert bereits oder Eingabefehler!")
# Inline-Delete
if request.method == 'POST' and 'delete_switch' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('switches'))
del_hostname = request.form['delete_switch']
conn.execute("DELETE FROM switches WHERE hostname=?", (del_hostname,))
conn.commit()
flash(f"Switch {del_hostname} gelöscht.")
switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall() switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall()
conn.close() conn.close()
return render_template('switche.html', switches=switches) 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") @app.route("/get_log")
@login_required @login_required
def get_log(): def get_log():
@@ -304,36 +494,43 @@ def logs():
return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval) return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval)
def load_device_status(): def load_device_status():
"""
Liest das aktuellste rpi-Logfile und extrahiert den letzten Status jedes Devices.
Gibt ein Dictionary zurück: {Device-Name: 'online'/'offline'}
"""
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") log_files = glob.glob("/var/log/rpi-*.log")
if not log_files: if not log_files:
return status return status
latest_log = max(log_files, key=os.path.getctime) latest_log = max(log_files, key=os.path.getctime)
# Jede Zeile des Logs lesen
with open(latest_log, "r") as f:
lines = f.readlines()
# Regex für Ping-Ergebnisse
online_re = re.compile(r"(\S+) ist erreichbar!") online_re = re.compile(r"(\S+) ist erreichbar!")
offline_re = re.compile(r"(\S+) ist nicht erreichbar!") offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
for line in lines: with open(latest_log, "r") as f:
for line in f:
line = line.strip() line = line.strip()
m_online = online_re.search(line) m_online = online_re.search(line)
m_offline = offline_re.search(line) m_offline = offline_re.search(line)
if m_online: if m_online:
status[m_online.group(1)] = 'online' name = m_online.group(1)
mac = name_to_mac.get(name)
if mac:
status[mac] = 'online'
elif m_offline: elif m_offline:
status[m_offline.group(1)] = 'offline' name = m_offline.group(1)
mac = name_to_mac.get(name)
if mac:
status[mac] = 'offline'
return status return status
@app.route("/users", methods=["GET", "POST"]) @app.route("/users", methods=["GET", "POST"])
@login_required @login_required
def users(): def users():

View File

@@ -15,11 +15,11 @@ CREATE TABLE IF NOT EXISTS switches (
# Devices # Devices
c.execute(""" c.execute("""
CREATE TABLE IF NOT EXISTS devices ( CREATE TABLE devices (
mac TEXT PRIMARY KEY, mac TEXT PRIMARY KEY,
rpi_ip TEXT NOT NULL, rpi_ip TEXT NOT NULL,
switch_hostname TEXT NOT NULL, switch_hostname TEXT NOT NULL,
port TEXT NOT NULL, port TEXT,
name TEXT NOT NULL, name TEXT NOT NULL,
is_active INTEGER DEFAULT 0, is_active INTEGER DEFAULT 0,
FOREIGN KEY (switch_hostname) REFERENCES switches(hostname) FOREIGN KEY (switch_hostname) REFERENCES switches(hostname)

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

@@ -1,36 +1,35 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sqlite3 import sqlite3
import tempfile
import os
from app import decrypt_password, DB_PATH from app import decrypt_password, DB_PATH
def generate_ips_list(): def generate_ips_list():
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
# Alle Switches laden switches = {row['hostname']: row for row in conn.execute(
switches = {row['hostname']: row for row in conn.execute("SELECT hostname, ip, username, password FROM switches")} "SELECT hostname, ip, username, password FROM switches"
)}
# Alle Geräte laden devices = conn.execute("""
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall() SELECT mac, rpi_ip, port, name, switch_hostname
FROM devices
WHERE is_active=1
""").fetchall()
conn.close() conn.close()
tmp = tempfile.NamedTemporaryFile(delete=False, mode='w', prefix='ips_', suffix='.list')
tmp_path = tmp.name
for dev in devices: for dev in devices:
switch = switches.get(dev['switch_hostname']) switch = switches.get(dev['switch_hostname'])
if not switch: if switch:
continue # Switch existiert nicht, überspringen switch_ip = switch['ip']
switch_user = switch['username']
switch_pass = decrypt_password(switch['password'])
else:
switch_ip = ""
switch_user = ""
switch_pass = ""
password = decrypt_password(switch['password']) port = dev['port'] or ""
# Format: IP-Device:Hostname-Device:IP-Switch:Hostname-Switch:Port-Switch:Username-Switch:Password-Switch print(f"{dev['rpi_ip']}:{dev['name']}:{switch_ip}:{dev['switch_hostname'] or 'kein Switch'}:{port}:{switch_user}:{switch_pass}")
line = f"{dev['rpi_ip']}:{dev['name']}:{switch['ip']}:{switch['hostname']}:{dev['port']}:{switch['username']}:{password}\n"
tmp.write(line)
tmp.close()
return tmp_path
if __name__ == "__main__": if __name__ == "__main__":
path = generate_ips_list() generate_ips_list()
print(path) # optional, gibt die Tempdatei zurück

Binary file not shown.

View File

@@ -17,7 +17,7 @@ h1, h2, h3, h4, h5, h6 {
} }
pre { pre {
background-color: #f8f9fa; background-color: #f8f9fa7c;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
max-height: 600px; max-height: 600px;
@@ -28,6 +28,11 @@ pre {
margin-right: 5px; margin-right: 5px;
} }
#dashboard-timer {
font-size: 0.9rem;
padding: 0.25rem 0.5rem;
}
.device-card { .device-card {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 5px; border-radius: 5px;
@@ -53,7 +58,7 @@ pre {
} }
.content-wrapper { .content-wrapper {
max-width: 1024px; max-width: 1080px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@@ -77,28 +82,96 @@ pre {
.navbar-logo img { .navbar-logo img {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
height: 60px; /* Logo Höhe, kann angepasst werden */ height: 83px; /* Logo Höhe, kann angepasst werden */
} }
#log-container { #log-container {
position: relative; position: relative;
height: calc(100vh - 150px); /* Füllt die Seite minus Header */ 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 { #log-box {
height: 97%; flex: 1 1 auto; /* füllt den Container, berücksichtigt Header/Padding */
overflow: auto; overflow: auto;
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace; font-family: monospace;
border: 1px solid #dee2e6; /* Bootstrap-like border */ border: 1px solid #dee2e6;
padding: 1rem;
background-color: #f8f9fa; background-color: #f8f9fa;
padding: 0.5rem;
} }
#refresh-timer { #refresh-timer {
position: absolute; position: absolute;
bottom: 10px; bottom: 18px;
right: 10px; right: 30px;
font-size: 0.9em; font-size: 0.9em;
color: gray; 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

View File

@@ -19,7 +19,7 @@
<a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a> <a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a>
<a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a> <a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<a href="{{ url_for('switches') }}" class="btn btn-secondary">Switches</a> <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('users') }}" class="btn btn-secondary">Users</a>
<a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a> <a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a>
<a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a> <a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a>

View File

@@ -3,35 +3,12 @@
<h2>Devices</h2> <h2>Devices</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mt-2 alert alert-info">
{% for message in messages %}{{ message }}<br>{% endfor %}
</div>
{% endif %}
{% endwith %}
{% if current_user.is_admin %} {% if current_user.is_admin %}
<h4>Neues Gerät hinzufügen</h4> <!-- Neues Gerät hinzufügen -->
<form method="post" class="row g-2 mb-4"> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button>
<input type="hidden" name="add_device" value="1">
<div class="col"><input type="text" name="name" placeholder="Hostname" class="form-control" required></div>
<div class="col"><input type="text" name="rpi_ip" placeholder="IP-Adresse" class="form-control" required></div>
<div class="col"><input type="text" name="mac" placeholder="MAC-Adresse" class="form-control" required></div>
<div class="col"><input type="text" name="port" placeholder="Port" class="form-control" required></div>
<div class="col">
<select name="switch_hostname" class="form-control" required>
<option value="">Switch wählen</option>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %}
</select>
</div>
<div class="col"><button class="btn btn-success w-100">Hinzufügen</button></div>
</form>
{% endif %} {% endif %}
<table class="table table-bordered"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
@@ -39,45 +16,281 @@
<th>MAC-Adresse</th> <th>MAC-Adresse</th>
<th>Switch</th> <th>Switch</th>
<th>Port</th> <th>Port</th>
{% if current_user.is_admin %}<th>Status</th>{% endif %}
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %} {% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for d in devices %} {% for d in devices %}
<tr> <tr>
{% if current_user.is_admin %}
<form method="post">
<input type="hidden" name="edit_device" value="1">
<input type="hidden" name="old_mac" value="{{ d['mac'] }}">
<td><input name="name" value="{{ d['name'] }}" class="form-control" required></td>
<td><input name="rpi_ip" value="{{ d['rpi_ip'] }}" class="form-control" required></td>
<td><input name="mac" value="{{ d['mac'] }}" class="form-control" required></td>
<td>
<select name="switch_hostname" class="form-control" required>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}" {% if sw['hostname']==d['switch_hostname'] %}selected{% endif %}>
{{ sw['hostname'] }}
</option>
{% endfor %}
</select>
</td>
<td><input name="port" value="{{ d['port'] }}" class="form-control" required></td>
<td class="d-flex gap-1">
<button class="btn btn-primary btn-sm">Speichern</button>
<button name="delete_device" value="{{ d['mac'] }}" class="btn btn-danger btn-sm"
onclick="return confirm('Willst du das Gerät wirklich löschen?');">Löschen</button>
</td>
</form>
{% else %}
<td>{{ d['name'] }}</td> <td>{{ d['name'] }}</td>
<td>{{ d['rpi_ip'] }}</td> <td>{{ d['rpi_ip'] }}</td>
<td>{{ d['mac'] }}</td> <td>{{ d['mac'] }}</td>
<td>{{ d['switch_hostname'] }}</td> <td>{{ d['switch_hostname'] or '-' }}</td>
<td>{{ d['port'] }}</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 %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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 %} {% endblock %}

View File

@@ -1,20 +1,97 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Dashboard</h2> <h2 class="d-flex justify-content-between align-items-center">
<div class="row row-cols-1 row-cols-md-6 g-3"> Dashboard
{% for d in devices %} <span id="dashboard-timer" class="badge bg-success">
<div class="col"> Nächste Prüfung in -- Sekunden
<div class="card text-center p-2"> </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-header">{{ d[1] }}</div>
<div class="card-body"> <div class="card-body">
<span class="fw-bold" style="color: {% if status[d[0]]=='online' %}green{% else %}red{% endif %};"> <span class="fw-bold" style="color:
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}unbekannt{% endif %} {% 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> </span>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </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 %} {% endblock %}

View File

@@ -19,14 +19,14 @@
<form method="post" class="w-25"> <form method="post" class="w-25">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Username</label> <label class="form-label label text-white">Username</label>
<input type="text" name="username" class="form-control" required> <input type="text" name="username" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Password</label> <label class="form-label label text-white">Password</label>
<input type="password" name="password" class="form-control" required> <input type="password" name="password" class="form-control" required>
</div> </div>
<button class="btn btn-primary">Login</button> <button class="btn btn-primary button login">Login</button>
</form> </form>
</body> </body>

View File

@@ -3,6 +3,10 @@
<h2>Live Log</h2> <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-container">
<div id="log-box"></div> <div id="log-box"></div>
<div id="refresh-timer"> <div id="refresh-timer">
@@ -11,36 +15,65 @@
</div> </div>
<script> <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 intervalMinutes = {{ interval | int }};
const intervalMilliseconds = intervalMinutes * 60 * 1000; const intervalMilliseconds = intervalMinutes * 60 * 1000;
let lastUpdateTime = Date.now(); let lastUpdateTime = Date.now();
function fetchLog() { function fetchLog() {
fetch("{{ url_for('get_log') }}") fetch("{{ url_for('get_log') }}")
.then(response => response.text()) .then(res => res.text())
.then(data => { .then(data => {
const box = document.getElementById("log-box"); const box = document.getElementById("log-box");
const filteredLines = data const filteredLines = data
.split("\n") .split("\n");
.filter(line => !line.includes("ist erreichbar!")) box.innerText = filteredLines.join("\n");
.join("\n");
box.innerText = filteredLines;
box.scrollTop = box.scrollHeight; 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(); lastUpdateTime = Date.now();
}
} else {
lastUpdateTime = Date.now();
}
}) })
.catch(err => console.error("Fehler beim Laden der Logs:", err)); .catch(err => console.error(err));
} }
function updateTimer() { function updateTimer() {
const now = Date.now(); const now = Date.now();
const remainingMs = intervalMilliseconds - (now - lastUpdateTime); const elapsed = now - lastUpdateTime;
const remainingSec = Math.max(Math.ceil(remainingMs / 1000), 0); const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
const remainingSec = Math.ceil(remainingMs / 1000);
document.getElementById("timer").innerText = remainingSec; document.getElementById("timer").innerText = remainingSec;
} }
setInterval(updateTimer, 1000); document.getElementById("refresh-btn").addEventListener("click", fetchLog);
fetchLog(); fetchLog();
setInterval(fetchLog, intervalMilliseconds); setInterval(fetchLog, intervalMilliseconds);
setInterval(updateTimer, 1000);
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -5,20 +5,11 @@
<form method="post" class="row g-2"> <form method="post" class="row g-2">
<div class="col-auto"> <div class="col-auto">
<label for="interval" class="form-label">Prüfintervall (Minuten):</label> <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> <input type="number" name="interval" id="interval" class="form-control" value="{{ interval }}" min="1" required>
</div> </div>
<div class="col-auto align-self-end"> <div class="col-auto align-self-end">
<button type="submit" class="btn btn-success">Speichern & Service neustarten</button> <button type="submit" class="btn btn-success">Speichern & Service neustarten</button>
</div> </div>
</form> </form>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mt-2 alert alert-info">
{% for message in messages %}{{ message }}<br>{% endfor %}
</div>
{% endif %}
{% endwith %}
{% endblock %} {% endblock %}

View File

@@ -1,53 +1,183 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Switch Verwaltung</h2>
<h2>Switche</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>
{% if current_user.is_admin %} <table class="table table-striped">
<h4>Neuen Switch hinzufügen</h4>
<form method="post" class="row g-2 mb-4">
<input type="hidden" name="add_switch" value="1">
<div class="col"><input type="text" name="hostname" placeholder="Hostname" class="form-control" required></div>
<div class="col"><input type="text" name="ip" placeholder="IP-Adresse" class="form-control" required></div>
<div class="col"><input type="text" name="username" placeholder="Username" class="form-control" required></div>
<div class="col"><input type="password" name="password" placeholder="Password" class="form-control" required></div>
<div class="col"><button class="btn btn-success w-100">Hinzufügen</button></div>
</form>
{% endif %}
<table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
<th>IP-Adresse</th> <th>IP-Adresse</th>
<th>Username</th> <th>Username</th>
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %} <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for s in switches %} {% for s in switches %}
<tr> <tr>
{% if current_user.is_admin %}
<form method="post">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<td><input name="hostname" value="{{ s['hostname'] }}" class="form-control" required></td>
<td><input name="ip" value="{{ s['ip'] }}" class="form-control" required></td>
<td><input name="username" value="{{ s['username'] }}" class="form-control" required></td>
<td class="d-flex gap-1">
<button class="btn btn-primary btn-sm">Speichern</button>
<button name="delete_switch" value="{{ s['hostname'] }}" class="btn btn-danger btn-sm"
onclick="return confirm('Willst du den Switch wirklich löschen?');">Löschen</button>
</td>
</form>
{% else %}
<td>{{ s['hostname'] }}</td> <td>{{ s['hostname'] }}</td>
<td>{{ s['ip'] }}</td> <td>{{ s['ip'] }}</td>
<td>{{ s['username'] }}</td> <td>{{ s['username'] }}</td>
{% endif %} <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> </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 %} {% endfor %}
</tbody> </tbody>
</table> </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 %} {% endblock %}

View File

@@ -8,30 +8,30 @@
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button>
{% endif %} {% endif %}
<table class="table table-bordered"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>Username</th> <th class="col-ip">Username</th>
<th>Rolle</th> <th class="col-ip">Rolle</th>
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %} {% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for u in users %} {% for u in users %}
<tr> <tr>
<td>{{ u['username'] }}</td> <td class="col-small">{{ u['username'] }}</td>
<td>{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td> <td class="col-small">{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<td> <td class="custom-actions d-flex gap-1 align-items-center">
<!-- Rolle ändern --> <!-- Rolle ändern -->
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" <button class="btn btn-sm button login white" data-bs-toggle="modal"
data-bs-target="#roleModal" data-bs-target="#roleModal"
onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})"> onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})">
Rolle ändern Rolle ändern
</button> </button>
<!-- Passwort ändern --> <!-- Passwort ändern -->
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" <button class="btn btn-sm button login white" data-bs-toggle="modal"
data-bs-target="#passwordModal" data-bs-target="#passwordModal"
onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')"> onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')">
Passwort ändern Passwort ändern
@@ -49,13 +49,14 @@
</tbody> </tbody>
</table> </table>
<!-- Modals bleiben unverändert -->
<!-- Modal für neuen Benutzer --> <!-- Modal für neuen Benutzer -->
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<form method="post" id="userForm"> <form method="post" id="userForm">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Neuen Benutzer anlegen</h5> <h5 class="modal-title black">Neuen Benutzer anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -90,7 +91,7 @@
<div class="modal-content"> <div class="modal-content">
<form method="post" id="roleForm"> <form method="post" id="roleForm">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Rolle ändern</h5> <h5 class="modal-title black">Rolle ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -122,7 +123,7 @@
<div class="modal-content"> <div class="modal-content">
<form method="post" id="passwordForm"> <form method="post" id="passwordForm">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Passwort ändern</h5> <h5 class="modal-title black">Passwort ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -159,7 +160,6 @@ function openRoleModal(user_id, username, is_admin) {
function openPasswordModal(user_id, username) { function openPasswordModal(user_id, username) {
document.getElementById('password_user_id').value = user_id; document.getElementById('password_user_id').value = user_id;
document.getElementById('password_username').value = username; document.getElementById('password_username').value = username;
// Nur das Passwort-Feld zurücksetzen
document.querySelector("#passwordForm input[name='new_password']").value = ""; document.querySelector("#passwordForm input[name='new_password']").value = "";
} }
</script> </script>

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,9 +1,20 @@
#!/bin/bash #!/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)
# ============================================================================
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"
# Intervall aus DB (Sekunden) abrufen # Alte Logfiles löschen (älter als 30 Tage)
SLEEP=$(python3 - <<END find "$LOG_DIR" -type f -name "rpi-*.log" -mtime +30 -delete
# Intervall aus DB abrufen
SLEEP=$(python3 - <<'END'
import sqlite3 import sqlite3
conn = sqlite3.connect("/srv/poe_manager/sqlite.db") conn = sqlite3.connect("/srv/poe_manager/sqlite.db")
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone() row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
@@ -12,8 +23,7 @@ print(row[0] if row else 300)
END END
) )
# Umrechnung falls nötig SLEEP=${SLEEP:-300}
SLEEP=${SLEEP:-300} # default 300 Sekunden
function disable_poe() { function disable_poe() {
local switch_ip=$1 local switch_ip=$1
@@ -23,7 +33,6 @@ function disable_poe() {
expect <<EOF expect <<EOF
set timeout 5 set timeout 5
spawn ssh $username@$switch_ip spawn ssh $username@$switch_ip
expect { expect {
"assword:" { send "$password\r"; exp_continue } "assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue } "Press any key" { send "\r"; exp_continue }
@@ -40,8 +49,7 @@ expect "(config)#"
send "exit\r" send "exit\r"
expect "#" expect "#"
send "exit\r" send "exit\r"
expect ">" expect ">"; send "exit\r"
send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" } expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof expect eof
EOF EOF
@@ -55,7 +63,6 @@ function enable_poe() {
expect <<EOF expect <<EOF
set timeout 5 set timeout 5
spawn ssh $username@$switch_ip spawn ssh $username@$switch_ip
expect { expect {
"assword:" { send "$password\r"; exp_continue } "assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue } "Press any key" { send "\r"; exp_continue }
@@ -72,30 +79,53 @@ expect "(config)#"
send "exit\r" send "exit\r"
expect "#" expect "#"
send "exit\r" send "exit\r"
expect ">" expect ">"; send "exit\r"
send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" } expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof expect eof
EOF EOF
} }
echo "" > $LOGFILE echo "" > "$LOGFILE"
MAX_PARALLEL=10 # maximal gleichzeitig laufende Geräte
while true; do while true; do
echo "--------------------------------------------------------------------" >> $LOGFILE echo "--------------------------------------------------------------------" >> "$LOGFILE"
IP_FILE=$(python3 /srv/poe_manager/generate_ips.py) 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
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
ping -c 1 -W 2 "$rpi_ip" &> /dev/null check_device() {
if [ $? -ne 0 ]; then local rpi_ip="$1"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> $LOGFILE 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" 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 echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2 sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass" 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 echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> $LOGFILE
fi fi
done < "$IP_FILE" fi
rm -f "$IP_FILE" }
sleep $SLEEP
# 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 done