3352 Commits

Author SHA1 Message Date
MacRimi 53ba7b3b2f Revise and clarify ROADMAP.es.md content
Updated the Spanish roadmap document for ProxMenux, improving clarity and correcting phrasing throughout. Adjusted sections on version planning and contributions.
2026-05-28 14:11:05 +02:00
MacRimi fe1297936f Update AppImage 1.2.1.3 2026-05-27 17:55:41 +02:00
MacRimi e22ff85dc8 Update install_coral_lxc.sh 2026-05-27 17:36:11 +02:00
MacRimi 3143fedb7a Updates scripts share 2026-05-26 17:21:24 +02:00
MacRimi 2dc3a2b93c Update scripts share 2026-05-26 12:41:50 +02:00
MacRimi a3aa5d9c1a Update flask_server.py 2026-05-25 18:01:24 +02:00
MacRimi b299227da2 Update AppImage 1.2.1.3 2026-05-24 17:52:04 +02:00
MacRimi 3286fc315c Update AppImage 1.2.1.3 2026-05-24 16:42:44 +02:00
MacRimi 105576cf17 Update AppImage 2026-05-24 11:37:20 +02:00
MacRimi 4b934db7db Update AppImage 1.2.1.3 2026-05-23 21:27:18 +02:00
MacRimi 9d2685d4a8 Update beta_version.txt 2026-05-22 18:48:30 +02:00
MacRimi 4507eacf1a Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-05-22 18:47:46 +02:00
MacRimi f2a40b993a Update AppImage 1.2.1.3 2026-05-22 18:47:30 +02:00
MacRimi 69956a46d0 Update beta version from 1.2.1.3 to 1.2.1.2 2026-05-22 18:39:53 +02:00
MacRimi 840385272c Add ProxMenux beta 1.2.1.3 2026-05-22 18:24:03 +02:00
MacRimi 95d0667077 Update AppImage 1.2.1.2 2026-05-21 22:25:29 +02:00
MacRimi 56fac4c34b Update AppImage 1.2.1.2 2026-05-21 22:00:35 +02:00
MacRimi 2d523b030f Update AppImage 1.2.1.2 2026-05-21 21:41:27 +02:00
MacRimi f5b7a0a74b Update AppImage 1.2.1.2 2026-05-21 21:17:59 +02:00
MacRimi 3e9dd599a6 Update AppImage 1..2.1.2 2026-05-21 19:31:47 +02:00
MacRimi 0651f57e86 Update customizable_post_install.sh 2026-05-21 18:43:27 +02:00
MacRimi 7eccc3119b Update customizable_post_install.sh 2026-05-21 18:21:25 +02:00
MacRimi 9545587b67 Update AppImage 1.2.1.2 2026-05-21 17:24:09 +02:00
MacRimi ef22c88861 Update AppImage 1.2.1.2 2026-05-21 17:18:23 +02:00
MacRimi 3723888b0c Update Install 2026-05-20 20:46:58 +02:00
MacRimi bb982629b5 Update roadmap 2026-05-20 20:19:39 +02:00
MacRimi 48300d7f01 Update roadmap 2026-05-20 20:12:18 +02:00
MacRimi 2ae838b4a4 Add Roadmap 2026-05-20 20:05:55 +02:00
MacRimi ceb563cd60 Add ProxMenux Phases 2026-05-20 19:50:17 +02:00
MacRimi 298cd2c6d4 Update Beta 1.2.1.2 2026-05-20 19:47:42 +02:00
MacRimi 4112323961 Update AppImage 2026-05-20 18:14:32 +02:00
MacRimi 1087a87ea2 Update samba_lxc_server.sh 2026-05-20 16:32:03 +02:00
MacRimi 73389d842a Reset auth_fail cooldowns on NotificationManager.start()
Pedro Rico, 19/05: after reinstalling the Monitor from GitHub a real
SSH/web login failure went unnotified. Root cause was the auth_fail
cooldown surviving across the service restart — install_proxmenux_beta
extracts the new AppImage but leaves the notification_last_sent SQLite
table intact (desirable: we don't want to lose legitimate cooldowns
on every update). On startup `_load_cooldowns_from_db()` then loaded
the stale auth_fail row from the previous run into the in-memory
cache, and `_passes_cooldown` blocked the new event.

This extends the existing reset-on-start mechanism (already in place
for update_summary, proxmenux_update, post_install_update, …) to also
clear auth_fail rows. A security-relevant event shouldn't be silenced
because the same source IP happened to fail to log in yesterday.

- Rename `_UPDATE_EVENT_TYPES_RESET_ON_START` → `_EVENT_TYPES_RESET_ON_START`
  (the list no longer covers only update-status reports).
- Rename `_reset_update_cooldowns_on_start()` → `_reset_cooldowns_on_start()`
  for the same reason.
- Add `'auth_fail'` to the curated list.

High-frequency sources (log_critical_*, disk SMART errors, …) are
deliberately NOT on this list — they keep their 24h cooldown across
restarts to prevent inbox floods if the user toggles the service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:18:49 +02:00
MacRimi 4e26c5942f Reset update-type cooldowns on NotificationManager.start()
When the user reinstalls or restarts the Monitor (deploy of a new
beta AppImage), they expect to see a fresh "what's available now"
summary in Telegram/Gotify/etc. instead of silence — even if the
24h anti-spam cooldown for `update_summary` etc. hasn't expired yet.

Without this, the operator had to wait up to 24h after every
deploy before the next `update_summary`, `proxmenux_update`,
`post_install_update`, `pve_update`, `update_available`,
`nvidia_driver_update_available` or `secure_gateway_update_available`
notification fired. The 24h cooldown is the right default for steady
state (don't pester the user every poll cycle with the same "177
packages pending" reminder), but a service restart is an explicit
signal that the user wants a fresh status report.

- New _UPDATE_EVENT_TYPES_RESET_ON_START tuple lists the event types
  to clear (everything in the "*_update*" + "update_*" family).
- New _reset_update_cooldowns_on_start() runs at start() right after
  the running flag flips, before watchers/dispatcher come up.
- Patterns match both fingerprint shapes:
    "<host>:<entity>:<event_type>:"               trailing-colon form
    "<host>:<entity>:<event_type>"                no-suffix form (managed installs)
- In-memory `_cooldowns` cache is also pruned so the live dispatcher
  picks up the reset immediately, without waiting for the next
  `_load_cooldowns_from_db()` cycle.

Non-update cooldowns (auth_fail, log_critical_*, disk errors, …) are
preserved so a restart doesn't unleash a backlog of stale alerts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:03:44 +02:00
MacRimi 06e6ae417e Fix notification dispatch: NameError in _dispatch_to_channels (Quiet Hours)
`_dispatch_to_channels` does NOT receive the NotificationEvent object —
only the rendered primitives (title, body, severity, event_type, …).
The Quiet Hours + Daily Digest merge introduced two references to
`event.severity` / `event` inside this function, which raised
`NameError: name 'event' is not defined` for every event passing
through dispatch.

The dispatch loop swallows the exception with a broad `except`, so the
visible symptom was "the Test button works but no real event ever
arrives" — both for community beta users (multiple reports on
Telegram, 9-18 May) and verified live on a test host (id 905 in
notification_history confirms the pipeline post-fix).

- _dispatch_to_channels: read `severity` / `event_type` directly
  instead of `event.severity` / `event.event_type`.
- _should_buffer_for_digest: take (ch_name, severity, event_type)
  primitives instead of a NotificationEvent.
- _buffer_digest_event: same — take (ch_name, event_type,
  event_group, severity, title, body).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:23:29 +02:00
MacRimi 6eb1312c61 1.2.1.1-beta: notification + LXC + post-install fixes
- flask_notification_routes: PVE webhook X-Webhook-Secret written in
  standard base64 so PVE can decode it (GH #198)
- notification_channels: Gmail SMTP App Password handling — normalize
  tls_mode (None/empty → starttls), reject creds without host (false-
  positive sendmail delivery), surface "AUTH not advertised" hint
- notification_events: is_vzdump_active_on_host() reads /var/log/pve/
  tasks/active directly so backup_start fallback and vm_shutdown
  suppression survive a Monitor restart mid-backup
- notification_templates: extract --storage flag from vzdump log →
  "PBS-Cloud: vm/104/…" instead of generic "PBS:" prefix when multiple
  PBS endpoints exist
- health_monitor: pve_storage_capacity + zfs_pool_capacity respect
  per-item dismiss (don't keep category WARNING/CRITICAL after user
  dismisses); updates_check cache invalidated when /var/log/apt/
  history.log mtime advances
- lxc_mount_points: PVE volume size from subvol quota (df via
  /proc/<host_pid>/root/<target> + lxc.conf size=NNNG fallback);
  host_source_state detects "host detached" zombie binds; per-mount
  subprocess work parallelised via ThreadPoolExecutor so a CT with
  many bind mounts doesn't trip the Caddy 3s reverse-proxy timeout
- virtual-machines: "host detached" badge on bind mounts whose host
  source path disappeared
- auto/customizable_post_install: log2ram FUNC_VERSION 1.1 → 1.2; new
  log2ram-check.sh vacuums journal + truncates non-rotating logs
  (pveproxy/access.log, pveam.log) instead of only calling
  `log2ram write` (which leaves the tmpfs full); auto flow gains the
  missing SystemMaxUse in /etc/systemd/journald.conf

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:06:49 +02:00
MacRimi 81844fa456 Update AppImage 2026-05-18 18:09:15 +02:00
MacRimi 4bedeb9fcd Update appimage 2026-05-18 17:47:25 +02:00
MacRimi c13601cd2d Merge pull request #203 from jcastro/feature/fix-open-bug-issues
Fix webhook loopback detection and update handoff

Thanks for the thorough work here, jcastro — really appreciate the bug triage on top of the fix itself, it makes reviewing much easier.
The changes look good to me:
	•	_is_loopback_addr() is a clean solution. Using the stdlib ipaddress module and explicitly handling the IPv4-mapped IPv6 case (::ffff:127.0.0.1) is exactly what was needed after the dual-stack binding change. The docstring explaining why is a nice touch.
	•	The exec bash swap in menu is the right fix for #180. Replacing the shell before the installer overwrites /usr/local/bin/menu avoids the half-old/half-new parsing that was causing the syntax error on line 105. Good catch removing the now-unreachable return 0 too.
Validation looks solid (8/8 loopback cases, syntax checks on all scripts).
Merging into develop. Thanks again! 🙌
2026-05-14 17:25:59 +02:00
MacRimi c0dd7eacb6 Merge pull request #204 from jcastro/feature/select-iso-from-all-storages
Select VM ISOs from all ISO storages
2026-05-14 17:05:17 +02:00
MacRimi f2d5eac330 Merge pull request #202 from jcastro/feature/settings-release-channel
Add release channel switcher to Settings
2026-05-14 15:58:50 +02:00
jcastro 092b548d20 Select VM ISOs from all ISO storages 2026-05-14 14:45:45 +02:00
jcastro 70ab072c79 Fix webhook loopback detection and update handoff 2026-05-14 14:33:27 +02:00
jcastro f8a8c43d0d Add release channel switcher to settings 2026-05-14 14:23:43 +02:00
MacRimi fcd431b421 Merge pull request #200 from jcastro/feature/enable-zfs-autotrim-auto-post-install
Enable ZFS autotrim in auto post-install
2026-05-14 07:28:27 +02:00
MacRimi 2a9ba5b526 Merge pull request #201 from jcastro/feature/update-figurine-2.0.0
Update Figurine to 2.0.0
2026-05-14 07:26:12 +02:00
jcastro aba9402830 Update Figurine to 2.0.0 2026-05-14 06:52:08 +02:00
jcastro 8877f9871f Enable ZFS autotrim in auto post-install 2026-05-14 06:46:08 +02:00
MacRimi f569826b78 Merge pull request #197 from pespinel/codex/fix-beta-monitor-service
fix(monitor): update beta service runtime path
2026-05-10 23:06:29 +02:00
pespinel 0daab74a58 fix(monitor): update beta service runtime path 2026-05-10 22:24:09 +02:00
MacRimi 16c97e94cc Comment out extraction info message
Commented out the message logging for extracting AppImage runtime.
2026-05-10 08:48:12 +02:00
MacRimi bd9af49412 Add ProxMenux-Monitor AppImage SHA256 file 2026-05-10 06:10:09 +02:00
MacRimi ab5c7093eb Update AppImage 2026-05-10 05:19:36 +02:00
MacRimi b4e8c5101a Update AppImage 2026-05-10 05:11:51 +02:00
MacRimi 911886b90c Update AppImage 2026-05-10 05:00:00 +02:00
MacRimi c14b72456f Update AppImage 2026-05-10 04:46:33 +02:00
MacRimi 6d7e06a0d2 Update AppImage 2026-05-09 23:37:46 +02:00
MacRimi 0288c14a29 Update AppImage 2026-05-09 23:22:45 +02:00
MacRimi 748334eed6 Update beta_version.txt 2026-05-09 19:10:56 +02:00
MacRimi 07301ea599 Update beta_version.txt 2026-05-09 19:10:05 +02:00
MacRimi 2f919de9e3 update beta ProxMenux 1.2.1.1-beta 2026-05-09 18:59:59 +02:00
MacRimi 5ed1fc44fd Update 1.2.1.1-beta 2026-05-09 18:31:47 +02:00
MacRimi 32bbf5bb27 Update attribution clause in LICENSE file 2026-05-07 17:16:24 +02:00
github-actions[bot] b8b49da99e Update AppImage beta build (2026-04-21 19:30:40) 2026-04-21 19:30:40 +00:00
MacRimi da2e89bc94 Create CHANGELOG.md 2026-04-21 21:22:34 +02:00
MacRimi 5fea839e34 Delete CHANGELOG.md 2026-04-21 21:22:01 +02:00
MacRimi 99f73ad745 Fix image link in CHANGELOG for SR-IOV state
Updated image link for SR-IOV active state in the Monitor UI.
2026-04-21 21:19:16 +02:00
MacRimi c742393efc Update CHANGELOG.md 2026-04-21 21:15:31 +02:00
MacRimi c3a5d6201e Delete AppImage/ProxMenux-Monitor.AppImage.sha256 2026-04-21 21:12:02 +02:00
MacRimi 9e7350c3bb Delete AppImage/ProxMenux-1.2.0.AppImage 2026-04-21 21:09:21 +02:00
MacRimi 5bee471884 Update version.txt 2026-04-21 21:07:15 +02:00
MacRimi 77eb8c7b78 update pci_passthrough_helpers.sh 2026-04-21 21:06:22 +02:00
MacRimi 20c1140676 Create build-appimage-manual.yml 2026-04-19 19:33:58 +02:00
github-actions[bot] 9bf99c0fdd Update AppImage beta build (2026-04-19 11:49:17) 2026-04-19 11:49:17 +00:00
MacRimi 899eb61dcf update verified_ai_models.json 2026-04-19 13:47:12 +02:00
github-actions[bot] a5e6e112a5 Update AppImage beta build (2026-04-19 10:29:06) 2026-04-19 10:29:06 +00:00
MacRimi 834795d6d9 update gpu-switch-mode-indicator.tsx 2026-04-19 12:26:52 +02:00
github-actions[bot] bcca760403 Update AppImage beta build (2026-04-19 00:10:21) 2026-04-19 00:10:21 +00:00
MacRimi 3e0b907138 update notification_events.py 2026-04-19 02:01:04 +02:00
MacRimi 14adb673f6 Update nvidia_installer.sh 2026-04-18 19:20:36 +02:00
MacRimi d3e91b5d06 Update menu 2026-04-18 19:00:08 +02:00
MacRimi 74fcd7d569 Update beta_version.txt 2026-04-18 18:53:31 +02:00
MacRimi 0843cd8363 Create beta_version.txt 2026-04-18 18:52:07 +02:00
MacRimi a5a55f3c7d Delete beta_version.txt 2026-04-18 18:50:29 +02:00
MacRimi 2fb9e74a13 Update menu 2026-04-18 18:48:33 +02:00
MacRimi f950882ffd Update beta_version.txt 2026-04-18 18:20:57 +02:00
MacRimi 7318c81fe0 Create beta_version.txt 2026-04-18 18:01:40 +02:00
MacRimi 43959fc758 Delete beta_version.txt 2026-04-18 17:58:02 +02:00
MacRimi ff7b1e10a4 Create version.txt 2026-04-18 17:55:35 +02:00
MacRimi 67000f5ff1 Update nvidia_installer.sh 2026-04-18 09:13:52 +02:00
MacRimi c8b1cd0fab Update nvidia_installer.sh 2026-04-18 08:58:50 +02:00
MacRimi f6e9497f1e Delete version.txt 2026-04-18 01:23:47 +02:00
MacRimi 802dc491f8 Update menu 2026-04-18 01:20:39 +02:00
MacRimi 3ca5a36240 Update config_menu.sh 2026-04-18 01:13:13 +02:00
MacRimi 3046299414 Update hw_grafics_menu.sh 2026-04-18 01:03:16 +02:00
MacRimi 37c60cb82a Upate menu 2026-04-18 00:58:25 +02:00
MacRimi 9446112081 Update beta_version.txt 2026-04-18 00:38:32 +02:00
MacRimi 4b72490486 update menu 2026-04-18 00:33:40 +02:00
MacRimi 09ff203662 Update README with security note on VirusTotal
Added a security note regarding VirusTotal false positives for the installation script.
2026-04-17 23:58:31 +02:00
MacRimi 4f7977b5ca Merge branch 'main' into develop 2026-04-17 23:49:29 +02:00
MacRimi 512cc11894 Delete AppImage/ProxMenux-1.0.1.AppImage 2026-04-17 23:39:24 +02:00
github-actions[bot] 51be0bd3bd Update AppImage release build (2026-04-17 21:38:53) 2026-04-17 21:38:53 +00:00
MacRimi 0f6095f8c3 Create ProxMenux_ai.png 2026-04-17 23:28:16 +02:00
MacRimi 831bf67ee4 update install 2026-04-17 22:26:25 +02:00
MacRimi e7cca5e532 Delete AppImage/ProxMenux-1.0.2-beta.AppImage 2026-04-17 22:02:18 +02:00
MacRimi 75b08de934 update hw_grafics_menu.sh 2026-04-17 21:55:53 +02:00
github-actions[bot] 987b665115 Update AppImage beta build (2026-04-17 18:58:06) 2026-04-17 18:58:06 +00:00
MacRimi 68ca68c6f1 update hardware.tsx 2026-04-17 20:56:07 +02:00
ProxMenuxBot 5c4ca290fb Update helpers_cache.json 2026-04-17 18:16:35 +00:00
github-actions[bot] 1d61442a49 Update AppImage beta build (2026-04-17 18:03:26) 2026-04-17 18:03:26 +00:00
MacRimi 0e2ede5e66 update v1.2.0 2026-04-17 20:01:30 +02:00
github-actions[bot] 0db74814be Update AppImage beta build (2026-04-17 17:56:07) 2026-04-17 17:56:07 +00:00
MacRimi 75c6f74fc4 update nstall_coral_pve9.sh 2026-04-17 19:53:17 +02:00
MacRimi e6faec24fa update nvidia_installer.sh 2026-04-17 19:02:46 +02:00
MacRimi 35b7d01d7e update nvidia_installer.sh 2026-04-17 18:35:42 +02:00
MacRimi 415bc439bb Update nvidia_update.sh 2026-04-17 18:24:07 +02:00
MacRimi fe47522275 Update nvidia_update.sh 2026-04-17 18:07:43 +02:00
MacRimi 24b97831a4 Update nvidia_update.sh 2026-04-17 18:06:21 +02:00
github-actions[bot] ac95a5afba Update AppImage beta build (2026-04-17 15:39:07) 2026-04-17 15:39:07 +00:00
MacRimi 03850d2958 update virtual-machines.tsx 2026-04-17 17:36:57 +02:00
github-actions[bot] c7b49cfc4a Update AppImage beta build (2026-04-17 15:03:32) 2026-04-17 15:03:32 +00:00
MacRimi 5398211ab5 update virtual-machines.tsx 2026-04-17 17:01:24 +02:00
github-actions[bot] dc8ebb651a Update AppImage beta build (2026-04-17 14:41:07) 2026-04-17 14:41:07 +00:00
MacRimi 039e35f3c5 update health_monitor.py 2026-04-17 16:39:08 +02:00
github-actions[bot] ffadb2c508 Update AppImage beta build (2026-04-17 13:42:17) 2026-04-17 13:42:17 +00:00
MacRimi 019e98e6b6 update flask_proxmenux_routes.py 2026-04-17 15:33:50 +02:00
github-actions[bot] c998e39038 Update AppImage beta build (2026-04-17 08:42:52) 2026-04-17 08:42:52 +00:00
MacRimi baa2ff4fa9 update health_persistence.py 2026-04-17 10:38:39 +02:00
github-actions[bot] 4b6a91e74c Update AppImage beta build (2026-04-16 18:54:07) 2026-04-16 18:54:07 +00:00
MacRimi 37f56c8a16 Update settings.tsx 2026-04-16 20:51:52 +02:00
github-actions[bot] 09bb47f408 Update AppImage beta build (2026-04-16 18:42:29) 2026-04-16 18:42:29 +00:00
MacRimi 5db6762690 upddate flask_proxmenux_routes.py 2026-04-16 20:40:34 +02:00
github-actions[bot] b1cc880253 Update AppImage beta build (2026-04-16 18:15:34) 2026-04-16 18:15:34 +00:00
MacRimi 7d4ea806a2 Update flask_proxmenux_routes.py 2026-04-16 20:13:13 +02:00
github-actions[bot] 2b306c9033 Update AppImage beta build (2026-04-16 18:05:44) 2026-04-16 18:05:44 +00:00
MacRimi a776d6b746 Update health_persistence.py 2026-04-16 20:01:56 +02:00
github-actions[bot] 07f87de742 Update AppImage beta build (2026-04-16 17:36:53) 2026-04-16 17:36:53 +00:00
MacRimi cf871da880 update health_persistence.py 2026-04-16 19:33:47 +02:00
github-actions[bot] 774d42d5be Update AppImage beta build (2026-04-16 17:20:45) 2026-04-16 17:20:46 +00:00
MacRimi 6660122e69 Update health_persistence.py 2026-04-16 19:18:42 +02:00
github-actions[bot] 1ef4bc4fed Update AppImage beta build (2026-04-16 17:13:00) 2026-04-16 17:13:00 +00:00
MacRimi ee1204c566 update health_monitor.py 2026-04-16 19:10:47 +02:00
github-actions[bot] 7f2b0c5de1 Update AppImage beta build (2026-04-16 16:12:35) 2026-04-16 16:12:35 +00:00
MacRimi 9737ffd996 Update storage-overview.tsx 2026-04-16 18:10:27 +02:00
github-actions[bot] be60b7e17c Update AppImage beta build (2026-04-16 16:02:46) 2026-04-16 16:02:46 +00:00
MacRimi 97368a6f44 update storage-overview.tsx 2026-04-16 17:58:27 +02:00
github-actions[bot] c3d7f01b40 Update AppImage beta build (2026-04-16 15:41:42) 2026-04-16 15:41:42 +00:00
MacRimi cbebd5147c update storage-overview.tsx 2026-04-16 17:36:23 +02:00
github-actions[bot] 4611be734f Update AppImage beta build (2026-04-16 15:00:04) 2026-04-16 15:00:04 +00:00
MacRimi 528b57664f Update storage-overview.tsx 2026-04-16 16:57:40 +02:00
github-actions[bot] cc86d68507 Update AppImage beta build (2026-04-16 14:44:10) 2026-04-16 14:44:10 +00:00
MacRimi 7c8da462db update storage-overview.tsx 2026-04-16 16:42:11 +02:00
github-actions[bot] b7963c3b70 Update AppImage beta build (2026-04-16 14:11:03) 2026-04-16 14:11:03 +00:00
MacRimi d51dd35376 Update storage-overview.tsx 2026-04-16 16:08:58 +02:00
github-actions[bot] 196086498e Update AppImage beta build (2026-04-16 13:54:35) 2026-04-16 13:54:35 +00:00
MacRimi f6ff76f9ce Update storage-overview.tsx 2026-04-16 15:52:26 +02:00
github-actions[bot] 20a2db6739 Update AppImage beta build (2026-04-16 13:38:55) 2026-04-16 13:38:55 +00:00
MacRimi aa3b8ebe82 Update storage-overview.tsx 2026-04-16 15:28:48 +02:00
github-actions[bot] d03afa1793 Update AppImage beta build (2026-04-16 13:11:30) 2026-04-16 13:11:30 +00:00
MacRimi 056cee2f94 Update storage-overview.tsx 2026-04-16 15:09:16 +02:00
github-actions[bot] f80e087429 Update AppImage beta build (2026-04-16 12:47:49) 2026-04-16 12:47:49 +00:00
MacRimi eea765300e Update flask_server.py 2026-04-16 14:45:47 +02:00
ProxMenuxBot c2396d7e81 Update helpers_cache.json 2026-04-16 12:19:51 +00:00
github-actions[bot] 0b94acf7f6 Update AppImage beta build (2026-04-16 10:10:25) 2026-04-16 10:10:25 +00:00
MacRimi 324cb23f75 Update storage-overview.tsx 2026-04-16 12:08:36 +02:00
github-actions[bot] b341ba8297 Update AppImage beta build (2026-04-16 09:46:02) 2026-04-16 09:46:02 +00:00
MacRimi f5b9da0908 update storage-overview.tsx 2026-04-16 11:43:42 +02:00
ProxMenuxBot c83672a4bc Update helpers_cache.json 2026-04-16 00:24:34 +00:00
ProxMenuxBot af8e3f6a71 Update helpers_cache.json 2026-04-15 12:18:08 +00:00
ProxMenuxBot 5025d38a76 Update helpers_cache.json 2026-04-14 12:18:35 +00:00
ProxMenuxBot f3c7fb97fb Update helpers_cache.json 2026-04-13 18:23:34 +00:00
github-actions[bot] cb2ab5f67b Update AppImage beta build (2026-04-13 17:32:33) 2026-04-13 17:32:33 +00:00
MacRimi 003c8850b7 Update storage-overview.tsx 2026-04-13 19:30:19 +02:00
github-actions[bot] d9461c170d Update AppImage beta build (2026-04-13 17:25:04) 2026-04-13 17:25:04 +00:00
MacRimi 57b5de4a4a Update storage-overview.tsx 2026-04-13 19:22:59 +02:00
github-actions[bot] c406c52086 Update AppImage beta build (2026-04-13 17:16:44) 2026-04-13 17:16:44 +00:00
MacRimi df4855ec47 Update storage-overview.tsx 2026-04-13 19:11:57 +02:00
github-actions[bot] 8c7f4a4c20 Update AppImage beta build (2026-04-13 16:53:47) 2026-04-13 16:53:47 +00:00
MacRimi 3ec733d9c6 update storage-overview.tsx 2026-04-13 18:49:18 +02:00
github-actions[bot] 71c64d1ae5 Update AppImage beta build (2026-04-13 13:31:25) 2026-04-13 13:31:25 +00:00
MacRimi 98becfd368 update storage-overview.tsx 2026-04-13 15:29:22 +02:00
github-actions[bot] 4eea90bd97 Update AppImage beta build (2026-04-13 12:51:54) 2026-04-13 12:51:54 +00:00
MacRimi 2344935357 update storage-overview.tsx 2026-04-13 14:49:48 +02:00
ProxMenuxBot 550279ec68 Update helpers_cache.json 2026-04-13 12:20:26 +00:00
github-actions[bot] 44aefb5d3b Update AppImage beta build (2026-04-13 09:13:46) 2026-04-13 09:13:46 +00:00
MacRimi 7d3cf4d364 Update flask_server.py 2026-04-13 11:11:42 +02:00
github-actions[bot] f6ef383598 Update AppImage beta build (2026-04-13 08:09:07) 2026-04-13 08:09:07 +00:00
MacRimi a6149e3cd8 update storage-overview.tsx 2026-04-13 10:07:09 +02:00
github-actions[bot] 07f1098418 Update AppImage beta build (2026-04-13 07:50:38) 2026-04-13 07:50:38 +00:00
MacRimi 3d00f33dbf Update flask_server.py 2026-04-13 09:48:48 +02:00
MacRimi 98c859fbf8 Update storage-overview.tsx 2026-04-13 09:35:23 +02:00
github-actions[bot] 9460bee72f Update AppImage beta build (2026-04-13 07:20:17) 2026-04-13 07:20:17 +00:00
MacRimi 5a957fe904 Update storage-overview.tsx 2026-04-13 09:18:09 +02:00
github-actions[bot] 59a4b6e4ca Update AppImage beta build (2026-04-13 06:42:00) 2026-04-13 06:42:00 +00:00
MacRimi 9000316224 Update storage-overview.tsx 2026-04-13 08:38:32 +02:00
github-actions[bot] da96c57819 Update AppImage beta build (2026-04-12 21:56:49) 2026-04-12 21:56:49 +00:00
MacRimi b3cef0b009 Update storage-overview.tsx 2026-04-12 23:54:52 +02:00
MacRimi 78ace237dd Update storage-overview.tsx 2026-04-12 23:48:49 +02:00
MacRimi e94e065eca update storage-overview.tsx 2026-04-12 23:45:23 +02:00
github-actions[bot] 4ffe0f3f46 Update AppImage beta build (2026-04-12 21:13:40) 2026-04-12 21:13:40 +00:00
MacRimi 7af4150e44 update storage-overview.tsx 2026-04-12 23:11:31 +02:00
github-actions[bot] 71950369e1 Update AppImage beta build (2026-04-12 21:01:05) 2026-04-12 21:01:05 +00:00
MacRimi adb4815c9b Update storage-overview.tsx 2026-04-12 22:58:49 +02:00
github-actions[bot] 1841feb643 Update AppImage beta build (2026-04-12 20:52:32) 2026-04-12 20:52:32 +00:00
MacRimi f14a0393b7 update storage-overview.tsx 2026-04-12 22:50:30 +02:00
github-actions[bot] 710f77764b Update AppImage beta build (2026-04-12 20:37:41) 2026-04-12 20:37:41 +00:00
MacRimi ae2e86d1d1 Update storage-overview.tsx 2026-04-12 22:35:12 +02:00
github-actions[bot] 167fcb2921 Update AppImage beta build (2026-04-12 20:30:17) 2026-04-12 20:30:17 +00:00
MacRimi 47145ab9d1 update storage-overview.tsx 2026-04-12 22:28:15 +02:00
github-actions[bot] 441ee8e948 Update AppImage beta build (2026-04-12 19:58:21) 2026-04-12 19:58:21 +00:00
github-actions[bot] 9c4528dfcb Update AppImage beta build (2026-04-12 19:39:05) 2026-04-12 19:39:05 +00:00
MacRimi d7c60631b4 Update storage-overview.tsx 2026-04-12 21:37:02 +02:00
github-actions[bot] 530f5c2dbc Update AppImage beta build (2026-04-12 19:30:36) 2026-04-12 19:30:36 +00:00
MacRimi 03dc2afe8d Update storage-overview.tsx 2026-04-12 21:28:36 +02:00
MacRimi 7d35d91415 Update storage-overview.tsx 2026-04-12 21:06:01 +02:00
MacRimi 4843fae0eb Update scripts 2026-04-12 20:32:34 +02:00
ProxMenuxBot 3dfbeac541 Update helpers_cache.json 2026-04-11 00:18:18 +00:00
MacRimi 4fa4bbb08b update switch_gpu_mode.sh 2026-04-10 09:51:03 +02:00
github-actions[bot] 4204e619db Update AppImage beta build (2026-04-10 07:10:14) 2026-04-10 07:10:14 +00:00
MacRimi a663b83daa Update script-terminal-modal.tsx 2026-04-10 09:07:57 +02:00
github-actions[bot] fb218a9331 Update AppImage beta build (2026-04-09 20:08:09) 2026-04-09 20:08:09 +00:00
MacRimi 75b677576e update switch_gpu_mode_direct.sh 2026-04-09 22:05:23 +02:00
MacRimi 04bda0bf10 update switch_gpu_mode_direct.sh 2026-04-09 21:42:44 +02:00
MacRimi 8ca33dec6f update switch_gpu_mode_direct.sh 2026-04-09 21:03:06 +02:00
MacRimi eed9303e41 Update script-terminal-modal.tsx 2026-04-09 20:50:56 +02:00
MacRimi 5277e7b47d Update script-terminal-modal.tsx 2026-04-09 20:41:12 +02:00
github-actions[bot] 727c86a804 Update AppImage beta build (2026-04-09 18:32:02) 2026-04-09 18:32:02 +00:00
MacRimi 21edae5944 Update script-terminal-modal.tsx 2026-04-09 20:29:50 +02:00
github-actions[bot] a0d48a1191 Update AppImage beta build (2026-04-09 18:22:51) 2026-04-09 18:22:51 +00:00
MacRimi 086ba9e577 Update switch_gpu_mode_direct.sh 2026-04-09 20:20:46 +02:00
github-actions[bot] bda5fdbecd Update AppImage beta build (2026-04-09 18:14:32) 2026-04-09 18:14:32 +00:00
MacRimi 13d2eeb9b2 update gpu-switch-mode-indicator.tsx 2026-04-09 20:12:21 +02:00
github-actions[bot] 1bbf814ea3 Update AppImage beta build (2026-04-09 18:01:03) 2026-04-09 18:01:03 +00:00
MacRimi 6d0e5add0b create switch_gpu_mode_direct.sh 2026-04-09 19:58:54 +02:00
github-actions[bot] 06849ca666 Update AppImage beta build (2026-04-09 17:42:58) 2026-04-09 17:42:58 +00:00
MacRimi 4346a5554f Update hardware.tsx 2026-04-09 19:40:49 +02:00
github-actions[bot] 2a174df697 Update AppImage beta build (2026-04-09 17:30:29) 2026-04-09 17:30:29 +00:00
MacRimi 28df51092c update gpu-switch-mode-indicator.tsx 2026-04-09 19:27:45 +02:00
github-actions[bot] ca70852060 Update AppImage beta build (2026-04-09 13:44:32) 2026-04-09 13:44:32 +00:00
MacRimi 86df5cda6e Update hardware.tsx 2026-04-09 15:42:05 +02:00
github-actions[bot] 7db7b7d98f Update AppImage beta build (2026-04-09 13:31:57) 2026-04-09 13:31:57 +00:00
MacRimi 3cede88a3d update gpu-switch-mode-indicator.tsx 2026-04-09 15:29:41 +02:00
github-actions[bot] af63b71ab8 Update AppImage beta build (2026-04-09 13:04:37) 2026-04-09 13:04:38 +00:00
MacRimi 2805c46a22 update gpu-switch-mode-indicator.tsx 2026-04-09 15:02:25 +02:00
github-actions[bot] 104353f013 Update AppImage beta build (2026-04-09 12:22:57) 2026-04-09 12:22:57 +00:00
MacRimi 435f346d98 update notification_events.py 2026-04-09 14:08:56 +02:00
MacRimi 2b8caa924f update notification_events.py 2026-04-09 12:34:03 +02:00
ProxMenuxBot dd22f303ac Update helpers_cache.json 2026-04-08 18:23:52 +00:00
ProxMenuxBot 02141ae16f Update helpers_cache.json 2026-04-08 12:16:35 +00:00
ProxMenuxBot b9b10a69d5 Update helpers_cache.json 2026-04-07 18:17:36 +00:00
github-actions[bot] d8631a8594 Update AppImage beta build (2026-04-07 13:37:06) 2026-04-07 13:37:06 +00:00
MacRimi 463769aba9 Update health_persistence.py 2026-04-07 15:34:48 +02:00
ProxMenuxBot a9fbaf15b2 Update helpers_cache.json 2026-04-07 12:16:27 +00:00
ProxMenuxBot b04c3b9f78 Update helpers_cache.json 2026-04-07 00:19:15 +00:00
MacRimi f2229c2393 Update add_gpu_vm.sh 2026-04-06 23:10:02 +02:00
MacRimi b4aadfee3e Update add_gpu_vm.sh 2026-04-06 22:58:17 +02:00
MacRimi 840e8a774e Update add_gpu_vm.sh 2026-04-06 22:51:30 +02:00
MacRimi c429d391f5 Update add_gpu_vm.sh 2026-04-06 22:34:37 +02:00
MacRimi 2fda3dd1d5 Update add_gpu_vm.sh 2026-04-06 22:26:54 +02:00
MacRimi 4018093c9d Update add_gpu_vm.sh 2026-04-06 22:21:38 +02:00
MacRimi 2f5b8ce4ed update add_gpu_vm.sh 2026-04-06 22:04:38 +02:00
MacRimi 2174c04e4f update add_gpu_vm.sh 2026-04-06 21:50:46 +02:00
MacRimi 109a4d7f54 update switch_gpu_mode.sh 2026-04-06 21:12:13 +02:00
ProxMenuxBot 080ee5cff0 Update helpers_cache.json 2026-04-06 18:16:41 +00:00
MacRimi 257668ab96 Update gpu_hook_guard_helpers.sh 2026-04-06 19:36:35 +02:00
MacRimi d747ac7659 Update gpu_hook_guard_helpers.sh 2026-04-06 19:25:47 +02:00
MacRimi d7c04ebbc7 update vm_storage_helpers.sh 2026-04-06 19:13:31 +02:00
MacRimi c5b01b4bb7 update switch_gpu_mode.sh 2026-04-06 18:40:57 +02:00
MacRimi e8cc90b83d update m_storage_helpers.sh 2026-04-06 17:26:01 +02:00
MacRimi d3974018d8 add_controller_nvme_vm.sh 2026-04-06 17:16:26 +02:00
MacRimi 10ce9dbcde add_controller_nvme_vm.sh 2026-04-06 13:39:07 +02:00
github-actions[bot] 5be4bd2ae9 Update AppImage beta build (2026-04-06 10:16:08) 2026-04-06 10:16:08 +00:00
MacRimi ea1d8ab037 Update system-overview.tsx 2026-04-06 12:06:08 +02:00
MacRimi adde2ce5b9 update health_persistence.py 2026-04-06 12:02:05 +02:00
ProxMenuxBot 9d37a7293b Update helpers_cache.json 2026-04-06 00:19:03 +00:00
ProxMenuxBot 975d4aab60 Update helpers_cache.json 2026-04-05 12:07:31 +00:00
github-actions[bot] 5ead9ee661 Update AppImage beta build (2026-04-05 10:19:43) 2026-04-05 10:19:43 +00:00
MacRimi 95e876b37f update health_monitor.py 2026-04-05 12:17:42 +02:00
github-actions[bot] 4c72d0b3ef Update AppImage beta build (2026-04-05 10:05:01) 2026-04-05 10:05:01 +00:00
MacRimi e7dc030304 Update health_monitor.py 2026-04-05 12:02:59 +02:00
github-actions[bot] c9d5c84d35 Update AppImage beta build (2026-04-05 10:00:14) 2026-04-05 10:00:14 +00:00
MacRimi 4b01ba1d2f update health_monitor.py 2026-04-05 11:58:14 +02:00
github-actions[bot] 723e56ada2 Update AppImage beta build (2026-04-05 09:53:20) 2026-04-05 09:53:20 +00:00
MacRimi e9851da12f update virtual-machines.tsx 2026-04-05 11:51:26 +02:00
MacRimi 5826b0419b update pci_passthrough_helpers.sh 2026-04-05 11:24:08 +02:00
ProxMenuxBot 77124c4549 Update helpers_cache.json 2026-04-05 00:19:09 +00:00
github-actions[bot] 00dbd1c87e Update AppImage beta build (2026-04-03 23:33:41) 2026-04-03 23:33:41 +00:00
MacRimi e0e732dd2c update health_persistence.py 2026-04-04 01:31:37 +02:00
MacRimi ce69c0ba1f Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-04 00:55:09 +02:00
MacRimi 58c4e115ba update add_gpu_vm.sh 2026-04-04 00:54:57 +02:00
github-actions[bot] 44478057dd Update AppImage beta build (2026-04-03 22:53:29) 2026-04-03 22:53:29 +00:00
MacRimi d1efae37a4 update health_persistence.py 2026-04-04 00:51:20 +02:00
ProxMenuxBot 8d8e4bab26 Update helpers_cache.json 2026-04-03 12:10:34 +00:00
MacRimi db113f0433 Update menu_post_install.sh 2026-04-03 10:44:10 +02:00
MacRimi 5a66167709 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-03 10:34:56 +02:00
MacRimi 7326201b0a update system_utils.sh 2026-04-03 10:34:46 +02:00
github-actions[bot] f116a6b0f2 Update AppImage beta build (2026-04-03 08:07:42) 2026-04-03 08:07:42 +00:00
MacRimi 3087ab36da Update flask_server.py 2026-04-03 10:05:44 +02:00
ProxMenuxBot 28a7189905 Update helpers_cache.json 2026-04-03 00:19:43 +00:00
ProxMenuxBot 0f8e215706 Update helpers_cache.json 2026-04-02 18:15:08 +00:00
github-actions[bot] fd92bed465 Update AppImage beta build (2026-04-02 18:01:23) 2026-04-02 18:01:23 +00:00
MacRimi 281f6975ec Update notification_templates.py 2026-04-02 19:59:27 +02:00
github-actions[bot] e2c40eae48 Update AppImage beta build (2026-04-02 17:26:45) 2026-04-02 17:26:45 +00:00
MacRimi 26968b02a1 update post_install.sh 2026-04-02 19:23:55 +02:00
MacRimi 0d8070455d Update notification_templates.py 2026-04-02 18:29:35 +02:00
MacRimi 2537e964a5 Update notification_templates.py 2026-04-02 17:38:47 +02:00
MacRimi ca7134e610 Update notification_templates.py 2026-04-02 17:20:02 +02:00
MacRimi a8c591affd Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-02 17:05:10 +02:00
MacRimi e3842f200d Update add_gpu_lxc.sh 2026-04-02 17:05:00 +02:00
github-actions[bot] cc952d8c79 Update AppImage beta build (2026-04-02 15:01:15) 2026-04-02 15:01:15 +00:00
MacRimi e11daa0b36 update ai_context_enrichment.py 2026-04-02 16:59:09 +02:00
github-actions[bot] 873c77d659 Update AppImage beta build (2026-04-02 09:40:56) 2026-04-02 09:40:56 +00:00
MacRimi 1756a6eb28 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-02 11:38:51 +02:00
MacRimi 9636d3671c Update notification_events.py 2026-04-02 11:38:36 +02:00
github-actions[bot] 22e2ebb96c Update AppImage beta build (2026-04-02 09:12:04) 2026-04-02 09:12:04 +00:00
MacRimi c53d3807e7 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-02 11:10:03 +02:00
MacRimi 23a6392979 Update notification_events.py 2026-04-02 11:09:50 +02:00
github-actions[bot] fa599ad183 Update AppImage beta build (2026-04-02 07:52:09) 2026-04-02 07:52:09 +00:00
MacRimi e79fdcfe58 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-02 09:50:10 +02:00
MacRimi 3746f356b9 Update notification_events.py 2026-04-02 09:49:54 +02:00
github-actions[bot] a5f3362d6e Update AppImage beta build (2026-04-02 07:31:50) 2026-04-02 07:31:50 +00:00
MacRimi 2072264918 update notification_manager.py 2026-04-02 09:29:29 +02:00
github-actions[bot] a5cb01133e Update AppImage beta build (2026-04-02 07:01:18) 2026-04-02 07:01:18 +00:00
MacRimi 62b55cbf16 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-04-02 08:58:02 +02:00
MacRimi 7ea0c4d36c Update notification_events.py 2026-04-02 08:57:37 +02:00
github-actions[bot] 546200844e Update AppImage beta build (2026-04-02 06:40:10) 2026-04-02 06:40:10 +00:00
MacRimi 007e3d1c0e update notification_templates.py 2026-04-02 08:38:01 +02:00
ProxMenuxBot 74a508e3a8 Update helpers_cache.json 2026-04-02 00:17:06 +00:00
MacRimi 5f5dc171be update scripts gpu 2026-04-01 23:09:51 +02:00
github-actions[bot] bccba6e9b9 Update AppImage beta build (2026-04-01 13:27:08) 2026-04-01 13:27:08 +00:00
MacRimi c2073a5db5 Update health_monitor.py 2026-04-01 15:24:47 +02:00
ProxMenuxBot 52ad229d93 Update helpers_cache.json 2026-04-01 12:16:07 +00:00
github-actions[bot] f6a7352672 Update AppImage beta build (2026-04-01 11:24:43) 2026-04-01 11:24:43 +00:00
MacRimi 46e0322e6f update health_persistence.py 2026-04-01 13:01:48 +02:00
MacRimi 618538a854 update health_persistence.py 2026-04-01 12:30:19 +02:00
MacRimi d62396717a update health_persistence.py 2026-04-01 12:03:54 +02:00
MacRimi a734fa5566 Update health_monitor.py 2026-04-01 00:01:12 +02:00
MacRimi f98b302b94 Update health_persistence.py 2026-03-31 23:47:38 +02:00
MacRimi 215d36900a Update health_persistence.py 2026-03-31 23:20:33 +02:00
MacRimi 2df55d2839 Update health_monitor.py 2026-03-31 23:14:48 +02:00
MacRimi e00051caa7 update health_monitor.py 2026-03-31 23:00:00 +02:00
MacRimi 5138b2f1d5 update health_persistence.py 2026-03-31 20:55:17 +02:00
MacRimi 65dfb9103f update /system-logs.tsx 2026-03-31 20:10:58 +02:00
MacRimi 39bbc036cd update system-logs.tsx 2026-03-31 19:38:23 +02:00
MacRimi aaf6dd36f0 update system-logs.tsx 2026-03-31 19:30:10 +02:00
MacRimi e7519e68a3 update system-logs.tsx 2026-03-31 19:09:41 +02:00
MacRimi ad5803ef9c update proxmox-dashboard.tsx 2026-03-31 18:51:38 +02:00
MacRimi c04b514a2a update proxmox-dashboard.tsx 2026-03-31 18:42:35 +02:00
MacRimi cb9f567154 Fix link in security note in README.md
Updated the link in the security note to point to the correct GitHub issue.
2026-03-30 23:40:06 +02:00
MacRimi 80afa789e7 Update notification service 2026-03-30 22:26:20 +02:00
MacRimi 43f2ce52a5 Update notification service 2026-03-30 22:10:40 +02:00
MacRimi cf2e24269e Update notification_templates.py 2026-03-30 21:34:17 +02:00
MacRimi 6899650bf8 Update health_persistence.py 2026-03-30 21:19:08 +02:00
MacRimi c16df51892 Update notification_templates.py 2026-03-30 21:10:54 +02:00
MacRimi c549737ad0 Update HealthMonitor 2026-03-30 20:52:25 +02:00
MacRimi a85b51843a Update switch.tsx 2026-03-30 20:27:40 +02:00
MacRimi 60d401f5ea Update settings.tsx 2026-03-30 20:19:00 +02:00
ProxMenuxBot ca02b9001f Update helpers_cache.json 2026-03-30 18:17:00 +00:00
MacRimi 2fc5e2865d Update notification service 2026-03-30 19:55:19 +02:00
MacRimi 261b2bfb3c Update notification_manager.py 2026-03-30 19:16:31 +02:00
MacRimi 30e32e89b2 Update notification_manager.py 2026-03-30 19:11:19 +02:00
MacRimi 8b1a2b9bff Update security note in README.md
Clarified security note regarding VirusTotal false positives.
2026-03-30 19:04:53 +02:00
MacRimi f71289b248 Update README with security note and support request
Added a security note regarding VirusTotal false positives and encouraged support for the project.
2026-03-30 19:04:02 +02:00
MacRimi 54eab9af49 Update notification service 2026-03-30 18:53:03 +02:00
ProxMenuxBot a05546e811 Update helpers_cache.json 2026-03-30 12:16:12 +00:00
ProxMenuxBot 276c648f29 Update helpers_cache.json 2026-03-29 18:07:42 +00:00
ProxMenuxBot 8c389f4790 Update helpers_cache.json 2026-03-29 12:07:15 +00:00
ProxMenuxBot a09144d21a Update helpers_cache.json 2026-03-29 00:18:49 +00:00
MacRimi cb9a43f496 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-28 23:18:42 +01:00
MacRimi 30606e4743 Update beta_version.txt 2026-03-28 23:18:03 +01:00
github-actions[bot] d05913fbdf Update AppImage beta build (2026-03-28 22:15:36) 2026-03-28 22:15:36 +00:00
MacRimi a34efb50e0 Update notification service 2026-03-28 23:13:35 +01:00
MacRimi a26c69fc8d Update beta_version.txt 2026-03-28 23:09:39 +01:00
MacRimi 107803705c Update beta_version.txt 2026-03-28 23:02:01 +01:00
MacRimi 858b1689bf Update security_menu.sh 2026-03-28 22:56:07 +01:00
MacRimi 641acbd1f4 Update security_menu.sh 2026-03-28 22:53:56 +01:00
MacRimi 1de76ae6c1 Create security_menu.sh 2026-03-28 22:50:44 +01:00
github-actions[bot] 94f535b8ec Update AppImage beta build (2026-03-28 21:48:28) 2026-03-28 21:48:28 +00:00
MacRimi f072e285fc Update startup_grace.py 2026-03-28 22:44:58 +01:00
MacRimi 2c363bbb8e Update health_persistence.py 2026-03-28 22:44:52 +01:00
MacRimi 1c2b87d584 Update notification_templates.py 2026-03-28 22:37:21 +01:00
MacRimi e55154bd5e Update notification service 2026-03-28 22:19:33 +01:00
MacRimi 71098abb65 Update terminal-panel.tsx 2026-03-28 22:13:20 +01:00
MacRimi 7a3a4d1413 Update terminal-panel.tsx 2026-03-28 22:04:47 +01:00
MacRimi 7858fb0283 Update terminal-panel.tsx 2026-03-28 21:50:04 +01:00
MacRimi 2f9959c009 Update virtual-machines.tsx 2026-03-28 21:32:59 +01:00
MacRimi e7d3b20295 Update menus 2026-03-28 19:32:15 +01:00
MacRimi 264fa4982f Update notification service 2026-03-28 19:29:16 +01:00
MacRimi 9636614761 Update notification service 2026-03-28 19:25:05 +01:00
MacRimi ac6561ca52 Update menus 2026-03-28 18:29:58 +01:00
MacRimi 923172d39b udate lynis install 2026-03-28 17:27:29 +01:00
MacRimi d46c42d26b Unistall Fail2ban 2026-03-28 17:01:08 +01:00
MacRimi d628233982 Update notification service 2026-03-28 15:50:30 +01:00
ProxMenuxBot e9e1d471ec Update helpers_cache.json 2026-03-28 12:07:42 +00:00
MacRimi f4740916f5 Update install_coral_lxc.sh 2026-03-28 12:49:52 +01:00
ProxMenuxBot 3a2c9b1b05 Update helpers_cache.json 2026-03-28 00:17:05 +00:00
MacRimi 4cc1147579 Update notification service 2026-03-27 20:42:03 +01:00
MacRimi 976f23a90e update notification service 2026-03-27 20:31:21 +01:00
MacRimi 8ed500adf7 Update notification service 2026-03-27 20:17:59 +01:00
MacRimi 8658044c0c Update notification service 2026-03-27 19:55:15 +01:00
MacRimi 0edc2cc3af Update notification service 2026-03-27 19:40:17 +01:00
ProxMenuxBot f4db4cde13 Update helpers_cache.json 2026-03-27 18:16:33 +00:00
MacRimi 6bb9313b95 Update notification service 2026-03-27 19:15:11 +01:00
ProxMenuxBot 6447dfef50 Update helpers_cache.json 2026-03-27 12:12:51 +00:00
ProxMenuxBot 55cb3a1267 Update helpers_cache.json 2026-03-27 00:18:11 +00:00
MacRimi 7c5e7208b9 Update notification service 2026-03-26 20:04:53 +01:00
ProxMenuxBot aad4b13fda Update helpers_cache.json 2026-03-26 18:19:00 +00:00
MacRimi 839a20df97 Update notification service 2026-03-26 19:05:11 +01:00
MacRimi d497763e38 Update repository and clean up 2026-03-26 19:03:01 +01:00
MacRimi 12c088c10b Update nvidia_installer.sh 2026-03-26 18:18:26 +01:00
MacRimi 8c6a6bece6 Update menu_Helper_Scripts.sh 2026-03-26 17:43:34 +01:00
ProxMenuxBot 819ca8a212 Update helpers_cache.json 2026-03-26 12:16:15 +00:00
MacRimi 8d1becbd8c Update shutdown-notify.sh 2026-03-26 01:11:12 +01:00
MacRimi ebb7491c58 Create install_proxmenux_beta.sh 2026-03-26 00:56:20 +01:00
MacRimi 4f1278c37a Delete install_proxmenux_beta.sh 2026-03-26 00:54:28 +01:00
MacRimi 8ddee9013b Update install_proxmenux_beta.sh 2026-03-26 00:53:14 +01:00
MacRimi 7fe233ae2e Update install_proxmenux_beta.sh 2026-03-26 00:51:14 +01:00
MacRimi f7b37b1559 Update install_proxmenux_beta.sh 2026-03-26 00:49:06 +01:00
MacRimi 0b3624dbd5 Update notification service 2026-03-26 00:32:52 +01:00
MacRimi fb066eb2e9 Update install_proxmenux_beta.sh 2026-03-25 23:57:33 +01:00
MacRimi 22fadbb87b Update install_proxmenux_beta.sh 2026-03-25 23:56:27 +01:00
MacRimi 3b119d528c Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-25 23:50:59 +01:00
MacRimi bb3d3d759e Update install_proxmenux_beta.sh 2026-03-25 23:50:39 +01:00
github-actions[bot] 3c08ae9399 Update AppImage beta build (2026-03-25 22:48:45) 2026-03-25 22:48:45 +00:00
MacRimi 7a6fa2afa5 Upgrade GitHub Actions to use latest versions 2026-03-25 23:47:00 +01:00
MacRimi ba4e3c3adb Update notification service 2026-03-25 23:45:34 +01:00
MacRimi 66892f69ce Update notification service 2026-03-25 23:30:00 +01:00
MacRimi e37469ac2b Update install_proxmenux_beta.sh 2026-03-25 23:17:29 +01:00
MacRimi cdc2d7bbcb update notification service 2026-03-25 23:13:11 +01:00
MacRimi 92c0e6ff09 Update install_proxmenux_beta.sh 2026-03-25 22:58:51 +01:00
MacRimi 9b3fd324c3 Update install_proxmenux_beta.sh 2026-03-25 22:56:55 +01:00
MacRimi 23bd692f8e Update install_proxmenux_beta.sh 2026-03-25 22:53:54 +01:00
MacRimi 29b4573ca9 Update install_proxmenux_beta.sh 2026-03-25 22:50:32 +01:00
MacRimi 8b6755d866 Update notification service 2026-03-25 22:43:42 +01:00
MacRimi 6da20aab05 update notification service 2026-03-25 22:14:38 +01:00
MacRimi 5aa5942bcd Upgrade GitHub Actions to latest versions 2026-03-25 20:16:09 +01:00
MacRimi 68872d0e06 Update notification service 2026-03-25 20:12:08 +01:00
MacRimi d53c1dc402 Utdate notification service 2026-03-25 19:47:47 +01:00
MacRimi 6c2b03ae76 Update notification service 2026-03-25 19:21:37 +01:00
ProxMenuxBot 9f79d2b737 Update helpers_cache.json 2026-03-25 18:17:47 +00:00
MacRimi 2241b125d6 Update notification service 2026-03-25 18:28:54 +01:00
github-actions[bot] 152624302c Update AppImage beta build (2026-03-25 12:13:46) 2026-03-25 12:13:46 +00:00
ProxMenuxBot 6a703ee6a4 Update helpers_cache.json 2026-03-25 12:13:03 +00:00
MacRimi 0c0caa422d Update notification service 2026-03-25 13:10:36 +01:00
ProxMenuxBot 6fa7c1d4eb Update helpers_cache.json 2026-03-25 00:16:22 +00:00
MacRimi 509fff3972 Refactor discussion template for AI prompts
Removed prompt name and output language fields from the template. Updated description field to include output language information.
2026-03-24 18:20:58 +01:00
MacRimi bcacd8b98e Update notification service 2026-03-24 17:48:52 +01:00
MacRimi d2c8178772 Update notification service 2026-03-24 17:34:05 +01:00
MacRimi 365a246461 Update health-status-modal.tsx 2026-03-24 17:18:38 +01:00
MacRimi 098ae13f94 Update install_proxmenux_beta.sh 2026-03-24 15:14:36 +01:00
ProxMenuxBot 6a92225630 Update helpers_cache.json 2026-03-24 12:14:00 +00:00
github-actions[bot] c98044be9a Update AppImage beta build (2026-03-24 12:02:15) 2026-03-24 12:02:15 +00:00
MacRimi e6eb81cc61 Update notification_events.py 2026-03-24 12:48:26 +01:00
github-actions[bot] 0e50caadec Update AppImage beta build (2026-03-24 11:06:32) 2026-03-24 11:06:32 +00:00
MacRimi aad44ad42f Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-24 12:04:31 +01:00
MacRimi 59fd055526 Update beta_version.txt 2026-03-24 12:04:18 +01:00
github-actions[bot] 5fce75a60d Update AppImage beta build (2026-03-24 11:01:15) 2026-03-24 11:01:15 +00:00
MacRimi 74390726b4 Update flask_server.py 2026-03-24 11:41:49 +01:00
MacRimi 10f37b88c3 Upgrade GitHub Actions to v6 for build workflow 2026-03-24 10:58:16 +01:00
MacRimi 9bfacd9da9 Upgrade GitHub Actions to version 6 2026-03-24 10:57:45 +01:00
MacRimi a286770fd2 Upgrade GitHub Actions to version 6 2026-03-24 10:56:50 +01:00
github-actions[bot] 3101e84830 Update AppImage beta build (2026-03-24 09:41:18) 2026-03-24 09:41:18 +00:00
MacRimi 815b3bebda Update notification_templates.py 2026-03-24 10:38:06 +01:00
MacRimi c71eda1229 Update notification service 2026-03-24 10:27:52 +01:00
MacRimi 60518be5bd Update notification service 2026-03-23 23:05:27 +01:00
MacRimi 83254d9d70 Update notification_events.py 2026-03-23 21:04:01 +01:00
MacRimi d34cebc90d Update health_monitor.py 2026-03-23 20:25:27 +01:00
MacRimi c7ef51a73c Update notification service 2026-03-23 20:14:25 +01:00
MacRimi ab34fb08c1 Update health_monitor.py 2026-03-23 19:31:21 +01:00
ProxMenuxBot 6f99e1e8c1 Update helpers_cache.json 2026-03-23 18:14:42 +00:00
MacRimi 5fe87a04f0 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-23 18:08:35 +01:00
MacRimi 4ac71381da Update health_monitor.py 2026-03-23 18:08:22 +01:00
github-actions[bot] f95e1ad4b8 Update AppImage beta build (2026-03-23 16:56:41) 2026-03-23 16:56:41 +00:00
MacRimi 168726c131 Update notification service 2026-03-23 17:49:39 +01:00
ProxMenuxBot 4545aeb9c6 Update helpers_cache.json 2026-03-23 12:13:22 +00:00
ProxMenuxBot 84cf3e6a15 Update helpers_cache.json 2026-03-23 00:17:27 +00:00
MacRimi cd123b3479 Update notification_channels.py 2026-03-22 22:32:28 +01:00
MacRimi 4b99de8841 Update notification service 2026-03-22 22:24:17 +01:00
MacRimi 147ca0a41a Update gemini_provider.py 2026-03-22 21:35:25 +01:00
MacRimi 3f24d55945 update notification service 2026-03-22 21:14:19 +01:00
MacRimi 70871330d3 Update notification service 2026-03-22 20:58:57 +01:00
MacRimi 08bc354fa5 Update notification service 2026-03-22 20:47:51 +01:00
MacRimi 54c7322b23 Update flask_server.py 2026-03-22 20:20:59 +01:00
github-actions[bot] 4d2ceee26b Update AppImage beta build (2026-03-22 18:56:48) 2026-03-22 18:56:48 +00:00
MacRimi b6321e9698 Update notification_templates.py 2026-03-22 19:42:12 +01:00
MacRimi dd197c9826 Update auto_post_install.sh 2026-03-22 19:32:08 +01:00
MacRimi 01427f4926 Update notification service 2026-03-22 18:43:33 +01:00
MacRimi c47a7ba2a5 Update notification_events.py 2026-03-22 15:02:29 +01:00
MacRimi 04564bc9cf Update health_monitor.py 2026-03-22 14:57:46 +01:00
MacRimi 04661ce340 Update flask_server.py 2026-03-22 14:53:34 +01:00
MacRimi fcc54e2d6a Update flask_server.py 2026-03-22 14:48:30 +01:00
MacRimi 52fc3b03b7 Update flask_server.py 2026-03-22 14:39:09 +01:00
MacRimi 70c29ed7b6 Update flask_server.py 2026-03-22 14:29:46 +01:00
MacRimi d33741a90d Update notification service 2026-03-22 14:20:47 +01:00
ProxMenuxBot 484f117b8e Update helpers_cache.json 2026-03-22 12:05:28 +00:00
MacRimi 317739b508 Update beta_version.txt 2026-03-22 11:16:44 +01:00
github-actions[bot] d8062b4859 Update AppImage beta build (2026-03-22 10:15:19) 2026-03-22 10:15:19 +00:00
MacRimi 452eb70faf Update notification-settings.tsx 2026-03-22 10:22:17 +01:00
MacRimi 83889d7e3c Add discussion template for AI notifications prompts 2026-03-22 00:28:09 +01:00
MacRimi 2eb970a6a2 Create discussion template for custom prompts
Added a discussion template for sharing custom prompts, including fields for prompt name, AI provider, model, output language, description, prompt content, example output, additional notes, and confirmation checkboxes.
2026-03-22 00:18:20 +01:00
MacRimi 7838762a4e Update health_monitor.py 2026-03-21 23:19:41 +01:00
MacRimi f370a670ad Update flask_server.py 2026-03-21 23:03:30 +01:00
MacRimi 41fa6f4b10 Update notification service 2026-03-21 22:27:54 +01:00
MacRimi 18aa9a77dd Update notification service 2026-03-21 21:53:46 +01:00
MacRimi e53f6c0c52 Update notification-settings.tsx 2026-03-21 21:09:06 +01:00
MacRimi 642539cdfc Update notification-settings.tsx 2026-03-21 20:55:34 +01:00
MacRimi 6534fa7171 Update notification-settings.tsx 2026-03-21 20:41:02 +01:00
MacRimi ff08f4c0b5 Update notification-settings.tsx 2026-03-21 20:29:30 +01:00
MacRimi 134c62d543 Update notification-settings.tsx 2026-03-21 20:16:06 +01:00
MacRimi 1243842c68 Update notification-settings.tsx 2026-03-21 20:11:52 +01:00
MacRimi c1093be548 Update notification service 2026-03-21 19:57:28 +01:00
MacRimi b6f58758f2 Update notification-settings.tsx 2026-03-21 19:17:02 +01:00
MacRimi 1dbb59bc3f Update notification service 2026-03-21 18:53:35 +01:00
MacRimi 5e32857729 Update flask_server.py 2026-03-21 17:22:31 +01:00
MacRimi e3a611f33d Remove star history chart from README
Removed the star history chart HTML section from README.
2026-03-21 11:08:33 +01:00
MacRimi 8fb2a9094e Enhance README with star history chart
Added responsive star history chart with dark and light themes.
2026-03-21 11:07:56 +01:00
github-actions[bot] 46b9309336 Update AppImage beta build (2026-03-20 22:56:56) 2026-03-20 22:56:56 +00:00
MacRimi e60d2db8cb Update verified_ai_models.json 2026-03-20 23:48:07 +01:00
MacRimi e5e6c00100 Update notification service 2026-03-20 23:40:17 +01:00
MacRimi 40709b7480 Update flask_notification_routes.py 2026-03-20 23:29:25 +01:00
MacRimi 900c7154b6 Update notification service 2026-03-20 23:21:00 +01:00
MacRimi 2f4ea02544 Update notification service 2026-03-20 22:18:56 +01:00
MacRimi c24c10a13a Update notification service 2026-03-20 21:45:19 +01:00
MacRimi 22cd2e4bb3 Update notification service 2026-03-20 20:33:41 +01:00
github-actions[bot] 53ac43eb49 Update AppImage beta build (2026-03-20 19:13:03) 2026-03-20 19:13:03 +00:00
MacRimi fa29c46a95 Update notification service 2026-03-20 20:11:08 +01:00
MacRimi d871b4c78e Update notification_templates.py 2026-03-20 20:04:38 +01:00
MacRimi 03acdce2c8 Update beta_version.txt 2026-03-20 19:57:58 +01:00
github-actions[bot] 9aa3b61efc Update AppImage beta build (2026-03-20 18:56:21) 2026-03-20 18:56:21 +00:00
MacRimi 0a62e3deca Update notification_templates.py 2026-03-20 19:48:14 +01:00
MacRimi f454d5f045 Update notification_templates.py 2026-03-20 19:38:45 +01:00
MacRimi a5dca65e57 Update notification_templates.py 2026-03-20 19:20:51 +01:00
MacRimi 979a7e5d18 Update notification_templates.py 2026-03-20 19:16:36 +01:00
ProxMenuxBot d1e7154040 Update helpers_cache.json 2026-03-20 18:10:20 +00:00
MacRimi 4750ff8cd5 Update notification_templates.py 2026-03-20 19:03:04 +01:00
MacRimi 72d02010c7 Update notification service 2026-03-20 18:45:35 +01:00
MacRimi b49be42f2d Update notification_templates.py 2026-03-20 18:37:31 +01:00
MacRimi eddc183b85 Update notification_templates.py 2026-03-20 18:07:54 +01:00
MacRimi 812cf83de4 Update notification_templates.py 2026-03-20 17:46:02 +01:00
MacRimi 502cb8403f Update notification_templates.py 2026-03-20 17:09:32 +01:00
github-actions[bot] cf78bff21d Update AppImage beta build (2026-03-20 15:28:37) 2026-03-20 15:28:37 +00:00
MacRimi 1e352f4a7e Update notification service 2026-03-20 16:26:30 +01:00
MacRimi 47be85fdc0 Update notification_templates.py 2026-03-20 14:13:22 +01:00
MacRimi 5c9849e729 Update notification_templates.py 2026-03-20 13:55:47 +01:00
ProxMenuxBot e695b4e764 Update helpers_cache.json 2026-03-20 12:08:24 +00:00
github-actions[bot] d4d2e33619 Update AppImage beta build (2026-03-20 11:08:38) 2026-03-20 11:08:38 +00:00
MacRimi 1603f1ae66 Update notification service 2026-03-20 11:44:46 +01:00
MacRimi 88da476249 Update notification service 2026-03-20 11:26:26 +01:00
github-actions[bot] 1218fde6ea Update AppImage beta build (2026-03-19 20:30:24) 2026-03-19 20:30:24 +00:00
MacRimi 5046398e80 Update notification_templates.py 2026-03-19 21:28:27 +01:00
github-actions[bot] 20e5b6cc5a Update AppImage beta build (2026-03-19 19:47:33) 2026-03-19 19:47:33 +00:00
MacRimi c867c4ef51 Update ollama_provider.py 2026-03-19 20:45:35 +01:00
github-actions[bot] f2e804783b Update AppImage beta build (2026-03-19 19:22:23) 2026-03-19 19:22:23 +00:00
MacRimi 817e18ded2 Update ollama_provider.py 2026-03-19 20:20:11 +01:00
github-actions[bot] f99b498608 Update AppImage beta build (2026-03-19 19:11:56) 2026-03-19 19:11:56 +00:00
MacRimi d6cd4763f5 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-19 20:09:39 +01:00
MacRimi fdac846ede update ollama modal 2026-03-19 20:09:26 +01:00
github-actions[bot] d75c73df30 Update AppImage beta build (2026-03-19 19:00:26) 2026-03-19 19:00:27 +00:00
MacRimi 55c8dffe37 Update notification-settings.tsx 2026-03-19 19:58:17 +01:00
github-actions[bot] 8b35861602 Update AppImage beta build (2026-03-19 18:48:01) 2026-03-19 18:48:01 +00:00
MacRimi 6db7e64ca9 Update notification-settings.tsx 2026-03-19 19:42:42 +01:00
MacRimi 9484f78fb6 Update notification-settings.tsx 2026-03-19 19:36:39 +01:00
github-actions[bot] 1949aeb10f Update AppImage beta build (2026-03-19 18:20:31) 2026-03-19 18:20:31 +00:00
MacRimi 65fad4cc37 Create switch.tsx 2026-03-19 19:11:32 +01:00
MacRimi 876194cdc8 update storage settings 2026-03-19 19:07:26 +01:00
MacRimi cefeac72fc Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-19 17:56:57 +01:00
MacRimi 71708c3874 Update notification-settings.tsx 2026-03-19 17:56:51 +01:00
ProxMenuxBot b1eae7b768 Update helpers_cache.json 2026-03-19 12:09:20 +00:00
github-actions[bot] f02985e367 Update AppImage beta build (2026-03-19 09:11:42) 2026-03-19 09:11:42 +00:00
MacRimi 2848f672c1 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-19 10:08:11 +01:00
MacRimi f2210946c2 update ollama model 2026-03-19 10:08:08 +01:00
github-actions[bot] 4b79b9a417 Update AppImage beta build (2026-03-19 08:20:27) 2026-03-19 08:20:27 +00:00
MacRimi 10f8735f55 Update notification service 2026-03-19 09:18:14 +01:00
MacRimi 19a3a14417 Delete AppImage/ProxMenux-1.0.2.AppImage 2026-03-18 23:07:40 +01:00
github-actions[bot] aeaea1289c Update AppImage beta build (2026-03-18 22:05:40) 2026-03-18 22:05:40 +00:00
MacRimi 014ffa9b74 Update release-notes-modal.tsx 2026-03-18 23:02:18 +01:00
github-actions[bot] 0a9efe0122 Update AppImage beta build (2026-03-18 21:43:41) 2026-03-18 21:43:41 +00:00
MacRimi 82082a4b89 Update v1.0.2-beta 2026-03-18 22:41:39 +01:00
MacRimi 55bb5b5a1c Add beta program details to README
Added a beta program section with installation instructions and feedback guidelines.
2026-03-18 22:16:28 +01:00
MacRimi fd399edce7 Update README.md 2026-03-18 22:13:03 +01:00
MacRimi 5611b69ad2 Update README.md 2026-03-18 22:06:24 +01:00
MacRimi 7b181046d3 Update beta_version.txt 2026-03-18 21:56:26 +01:00
MacRimi b593d50b9a Update beta_version.txt 2026-03-18 21:55:49 +01:00
github-actions[bot] 8fe9426f5c Update AppImage beta build (2026-03-18 20:53:59) 2026-03-18 20:53:59 +00:00
MacRimi 0ce5f72df4 Update install_proxmenux_beta.sh 2026-03-18 21:47:24 +01:00
MacRimi d88c6570d0 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-18 21:24:33 +01:00
MacRimi 2980f7c9b8 Update menu 2026-03-18 21:24:18 +01:00
github-actions[bot] 7b2825e5ce Update AppImage beta build (2026-03-18 20:18:13) 2026-03-18 20:18:13 +00:00
MacRimi e47f79bcd4 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-18 21:15:49 +01:00
MacRimi 415020bf5d Beta Program Installer 2026-03-18 21:15:34 +01:00
github-actions[bot] 6ab1582db8 Update AppImage beta build (2026-03-18 20:05:10) 2026-03-18 20:05:10 +00:00
MacRimi 751a02aae8 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-18 21:02:46 +01:00
MacRimi bee637aa48 Beta Program Installer 2026-03-18 21:02:42 +01:00
github-actions[bot] ad6c7ffda8 Update AppImage beta build (2026-03-18 19:12:58) 2026-03-18 19:12:58 +00:00
MacRimi 7f59ca37f2 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-18 20:10:50 +01:00
MacRimi 9056964bb9 Update notification_templates.py 2026-03-18 20:10:48 +01:00
github-actions[bot] 154441bc1b Update AppImage beta build (2026-03-18 18:34:56) 2026-03-18 18:34:56 +00:00
MacRimi 873ec75586 update notification service 2026-03-18 19:32:38 +01:00
ProxMenuxBot e8232a9ea0 Update helpers_cache.json 2026-03-18 18:18:46 +00:00
MacRimi 751b361528 Update notification_templates.py 2026-03-18 19:18:27 +01:00
MacRimi 8990a3e243 Update notification_templates.py 2026-03-18 19:08:48 +01:00
MacRimi bc44490e96 Update notification_templates.py 2026-03-18 18:59:46 +01:00
MacRimi fe2d0b1d2a Update notification_templates.py 2026-03-18 18:43:05 +01:00
MacRimi bf3c5c1602 Update notification_templates.py 2026-03-18 18:37:55 +01:00
github-actions[bot] 6364931322 Update AppImage beta build (2026-03-18 16:52:23) 2026-03-18 16:52:23 +00:00
MacRimi 536d7141d9 Add files via upload 2026-03-18 17:50:13 +01:00
MacRimi 69e0bfe89a update notification service 2026-03-18 17:48:02 +01:00
ProxMenuxBot aeabb99be6 Update helpers_cache.json 2026-03-18 12:14:02 +00:00
MacRimi 38ee6d836d Add GitHub Actions workflow for AppImage beta build 2026-03-18 11:22:29 +01:00
MacRimi 7bb4bd3da5 Rename workflow for building AppImage release 2026-03-18 11:21:26 +01:00
MacRimi 7524615671 Save compiled AppImage files before git operations
Added steps to save compiled AppImage files before cleaning local changes.
2026-03-18 11:15:14 +01:00
MacRimi f5fe883d49 Enhance AppImage workflow to include checksum upload
Updated the workflow to upload both AppImage and checksum artifacts, and modified the commit process to use the current branch instead of main.
2026-03-18 11:11:05 +01:00
MacRimi bec6406216 Update GitHub Actions workflow for AppImage build 2026-03-18 10:48:41 +01:00
MacRimi c13c7ba626 Update notification service 2026-03-18 09:36:05 +01:00
ProxMenuxBot ef041f2702 Update helpers_cache.json 2026-03-18 00:17:00 +00:00
MacRimi 46b222180a Update notification-settings.tsx 2026-03-17 20:43:33 +01:00
MacRimi cd69b317c0 Update notification_manager.py 2026-03-17 20:41:09 +01:00
MacRimi 05fa751137 Update notification_templates.py 2026-03-17 20:34:55 +01:00
MacRimi 820317b9bd Update notification_manager.py 2026-03-17 20:25:19 +01:00
MacRimi bce01ad7a1 Update notification_templates.py 2026-03-17 20:05:23 +01:00
MacRimi bbe014798e Update notification_templates.py 2026-03-17 19:52:25 +01:00
MacRimi beea4dea04 Update notification_templates.py 2026-03-17 19:43:26 +01:00
MacRimi 71505362b4 Update notification service 2026-03-17 19:22:12 +01:00
MacRimi ff6904d436 Update notification-settings.tsx 2026-03-17 18:49:22 +01:00
MacRimi 1915bb3a9b Update notification service 2026-03-17 18:19:34 +01:00
MacRimi df0f15419e Add files via upload 2026-03-17 18:07:18 +01:00
MacRimi 04474d2e07 Create telegram.png 2026-03-17 17:35:27 +01:00
MacRimi 518bf0f217 Update notification service 2026-03-17 15:23:44 +01:00
MacRimi ac8f06c3a2 Update notification_manager.py 2026-03-17 15:04:29 +01:00
MacRimi feaf7b8abd Update notification-settings.tsx 2026-03-17 14:56:23 +01:00
MacRimi ac71057a3d Update notification-settings.tsx 2026-03-17 14:13:39 +01:00
MacRimi 0cb8900374 Update notification service 2026-03-17 14:07:47 +01:00
ProxMenuxBot dc531eaa37 Update helpers_cache.json 2026-03-17 12:13:15 +00:00
MacRimi 9a51a9e635 Update terminal-panel.tsx 2026-03-16 23:59:49 +01:00
MacRimi 754a0988ee Update terminal-panel.tsx 2026-03-16 23:48:35 +01:00
MacRimi 1985c0f815 Update terminal-panel.tsx 2026-03-16 23:35:31 +01:00
MacRimi 889b778d43 Update terminal-panel.tsx 2026-03-16 23:22:42 +01:00
MacRimi 26e90aa39e Update terminal panel 2026-03-16 23:13:13 +01:00
ProxMenuxBot 1eaabd14bd Update helpers_cache.json 2026-03-16 18:18:06 +00:00
MacRimi f406342b53 update latency modal 2026-03-16 18:19:19 +01:00
MacRimi b7c800b550 Update oci_manager.py 2026-03-16 17:58:04 +01:00
MacRimi b6780ba876 Update oci manager 2026-03-16 17:46:34 +01:00
ProxMenuxBot c9c8987cca Update helpers_cache.json 2026-03-16 12:14:04 +00:00
MacRimi f74d336072 Update health_persistence.py 2026-03-16 09:48:58 +01:00
MacRimi 6aaaa910af Update health_monitor.py 2026-03-16 09:36:40 +01:00
MacRimi 35eee03aa5 Update setupWebSocket 2026-03-16 09:20:45 +01:00
ProxMenuxBot 06dc6ea23f Update helpers_cache.json 2026-03-16 00:17:59 +00:00
MacRimi c1f8e7f511 Update lxc-terminal-modal.tsx 2026-03-15 21:34:45 +01:00
MacRimi 6d39acc627 Update terminal modal 2026-03-15 21:24:04 +01:00
MacRimi 11f768d26c Update oci_manager.py 2026-03-15 21:06:39 +01:00
MacRimi 602afc2954 Update jwt 2026-03-15 20:43:05 +01:00
MacRimi b7203b8219 update simple_jwt 2026-03-15 20:00:10 +01:00
MacRimi 513774bb7b Update SSL WebSocket 2026-03-15 19:04:35 +01:00
MacRimi 7375e306fb Update health_persistence.py 2026-03-15 18:27:55 +01:00
MacRimi 785d58cb59 Update health monitor 2026-03-15 18:12:42 +01:00
MacRimi af61d145da Update oci manager 2026-03-15 17:59:47 +01:00
MacRimi 0b75e967f3 Update oci manager 2026-03-15 17:19:35 +01:00
MacRimi cfa4210b0a Update terminal panel 2026-03-15 16:40:28 +01:00
MacRimi 0d6d570ae8 Update terminal panel 2026-03-15 16:27:37 +01:00
MacRimi 65add36b2f Update terminal panel 2026-03-15 16:16:03 +01:00
MacRimi 793b3dde12 Upgrade GitHub Actions to latest versions 2026-03-15 14:27:07 +01:00
MacRimi 8b3a76dfc5 Upgrade GitHub Actions to latest versions 2026-03-15 14:14:00 +01:00
ProxMenuxBot 60398210c7 Update helpers_cache.json 2026-03-15 12:06:54 +00:00
MacRimi 9112bcc52f Update health_monitor.py 2026-03-15 10:54:37 +01:00
MacRimi e534cffcf7 Update health_monitor.py 2026-03-15 10:41:34 +01:00
MacRimi a184dcc38f Update health_monitor.py 2026-03-15 10:36:19 +01:00
MacRimi 2c80223fc4 Update health_persistence.py 2026-03-15 10:28:53 +01:00
MacRimi 59a578fb2d Update health_persistence.py 2026-03-15 10:23:38 +01:00
MacRimi 91c3f3520b Update health_persistence.py 2026-03-15 10:13:44 +01:00
MacRimi e169200f40 Update health monitor 2026-03-15 10:03:35 +01:00
ProxMenuxBot 486c7ef530 Update helpers_cache.json 2026-03-15 00:18:01 +00:00
MacRimi 26c75e8309 Update health_persistence.py 2026-03-15 00:00:24 +01:00
MacRimi bc6eb0b5a0 Update secure-gateway-setup.tsx 2026-03-14 23:35:47 +01:00
MacRimi 9a057ef646 Update secure-gateway-setup.tsx 2026-03-14 23:29:17 +01:00
MacRimi a7b06bd5fc Update secure-gateway-setup.tsx 2026-03-14 23:09:46 +01:00
MacRimi 9d706d3aa3 Update secure-gateway-setup.tsx 2026-03-14 22:56:55 +01:00
MacRimi fdc4253117 Update secure-gateway-setup.tsx 2026-03-14 22:50:07 +01:00
MacRimi ff168937aa Update vpn seervice 2026-03-14 22:38:48 +01:00
MacRimi b7d060a1f3 Update vpn service 2026-03-14 22:10:17 +01:00
MacRimi c37466e948 Update secure-gateway-setup.tsx 2026-03-14 21:45:00 +01:00
MacRimi 09513c0beb Update vpn service 2026-03-14 21:29:16 +01:00
MacRimi a3f4277bdc Update secure-gateway-setup.tsx 2026-03-14 20:53:42 +01:00
MacRimi b43d8918bd update vpn service 2026-03-14 20:43:56 +01:00
MacRimi 4546adb894 Update vpn service 2026-03-14 20:30:53 +01:00
MacRimi db5ac37ad3 Update secure-gateway-setup.tsx 2026-03-14 20:07:25 +01:00
MacRimi 098c14f9e0 Update secure-gateway-setup.tsx 2026-03-14 19:55:54 +01:00
ProxMenuxBot 94131097a5 Update helpers_cache.json 2026-03-14 18:06:03 +00:00
MacRimi 6d69e009dc Rebuild Helper Scripts catalog for new data architecture
Rebuilt the Helper Scripts catalog to connect directly to the PocketBase API, enhancing data structure and script options. Acknowledged contributions from Community Scripts maintainers for their support in the integration.
2026-03-14 18:29:27 +01:00
MacRimi 6d9b132ab8 Bump version from 1.1.8 to 1.1.9 2026-03-14 18:18:44 +01:00
MacRimi 461a353e92 Update menu_Helper_Scripts.sh 2026-03-14 18:18:05 +01:00
MacRimi efec1aff18 Update script version and improve loading logic
Updated version to 1.3 and last updated date to 14/03/2025. Removed dependency on metadata.json and improved script loading and error handling.
2026-03-14 18:16:55 +01:00
MacRimi 258d6d9a49 Update menu_Helper_Scripts.sh 2026-03-14 18:15:15 +01:00
MacRimi 1c4b7c7b97 Update menu_Helper_Scripts.sh 2026-03-14 18:13:34 +01:00
ProxMenuxBot 8bc6306813 Update helpers_cache.json 2026-03-14 17:04:43 +00:00
MacRimi 2923c00738 Add files via upload 2026-03-14 18:04:09 +01:00
MacRimi b30b6a062a Delete .github/scripts/generate_helpers_cache.py 2026-03-14 18:03:51 +01:00
MacRimi 8f5df889ab Rename helpers_cache__.json to helpers_cache.json 2026-03-14 17:50:43 +01:00
MacRimi 4ec8b19251 Add backup JSON cache file 2026-03-14 17:50:25 +01:00
MacRimi 1035a94775 Rename helpers_cache_back.json to helpers_cache.json 2026-03-14 17:32:12 +01:00
MacRimi 3ca2ae7175 Add helpers_cache__.json file 2026-03-14 17:31:56 +01:00
ProxMenuxBot 4ba1ca890c Update helpers_cache.json 2026-03-14 15:44:57 +00:00
MacRimi cba012bd15 Add backup JSON helpers cache file 2026-03-14 16:44:28 +01:00
MacRimi 9515ccd816 Add files via upload 2026-03-14 16:43:44 +01:00
MacRimi 46622f5028 Add generate_helpers_cache_back.py script 2026-03-14 16:40:25 +01:00
MacRimi 1fd896fb72 Update oci_manager.py 2026-03-13 20:12:35 +01:00
MacRimi d081cc6c21 Update oci_manager.py 2026-03-13 19:19:32 +01:00
MacRimi 9190c8e5bf Update API_URL for JSON content retrieval 2026-03-13 17:15:01 +01:00
MacRimi 109498e2df Update API_URL to point to Frontend-Archive 2026-03-13 17:08:57 +01:00
MacRimi 21cfc63fc0 Update oci_manager.py 2026-03-13 00:13:52 +01:00
MacRimi a5f14146b9 Update oci_manager.py 2026-03-12 23:47:37 +01:00
MacRimi 2c18f6d975 update oci manager 2026-03-12 23:37:21 +01:00
MacRimi 84d9146c04 update oci manager 2026-03-12 22:50:20 +01:00
MacRimi 6d4006fd93 update oci manager 2026-03-12 22:13:56 +01:00
MacRimi b4a2e5ee11 Create oci manager 2026-03-12 21:30:44 +01:00
MacRimi 304b814bb1 Merge branch 'develop' of https://github.com/MacRimi/ProxMenux into develop 2026-03-12 19:40:31 +01:00
MacRimi c5e4774b29 Update notification service 2026-03-12 19:40:13 +01:00
MacRimi e90651b55b Update Node.js version and add environment variable 2026-03-12 19:37:25 +01:00
MacRimi 60d7c395bc Update Node.js version and add environment variable 2026-03-12 19:35:23 +01:00
MacRimi 2b5c9c2d61 Update health_persistence.py 2026-03-12 19:22:06 +01:00
MacRimi a703f1db73 Update health_persistence.py 2026-03-12 18:55:15 +01:00
MacRimi 4aaba7619e Update notification service 2026-03-12 18:12:04 +01:00
MacRimi 1a88dd801d Update proxmox-dashboard.tsx 2026-03-10 19:31:07 +01:00
MacRimi 83352ab9fe Update proxmox-dashboard.tsx 2026-03-10 19:25:03 +01:00
MacRimi 6e268a1bf4 Update proxmox-dashboard.tsx 2026-03-10 19:15:31 +01:00
MacRimi 4d65e54576 Update proxmox-dashboard.tsx 2026-03-10 19:06:28 +01:00
MacRimi c131ec722e Update proxmox-dashboard.tsx 2026-03-10 18:54:03 +01:00
MacRimi 574e12f336 Update proxmox-dashboard.tsx 2026-03-10 18:37:37 +01:00
MacRimi 1705868457 Update proxmox-dashboard.tsx 2026-03-10 18:22:59 +01:00
MacRimi 8392d111dc Update health-status-modal.tsx 2026-03-10 18:06:40 +01:00
MacRimi 8c5ccbadac Update notification service 2026-03-10 18:00:56 +01:00
MacRimi e4aa081e64 Update health-status-modal.tsx 2026-03-10 17:32:18 +01:00
MacRimi 8cc74eceb6 Update notification service 2026-03-10 17:22:27 +01:00
MacRimi 45365e3860 Update storage-overview.tsx 2026-03-08 23:22:18 +01:00
MacRimi 3739560956 Update notification service 2026-03-08 22:47:04 +01:00
MacRimi b8cff3e699 Update notification service 2026-03-08 20:01:02 +01:00
MacRimi d1d44afc9d Update notification service 2026-03-08 19:37:04 +01:00
ProxMenuxBot 782d847e54 Update helpers_cache.json 2026-03-08 18:04:29 +00:00
MacRimi be2bfa0087 Update health_monitor.py 2026-03-08 18:21:34 +01:00
MacRimi 1ea28d66df Update notification service 2026-03-08 18:15:36 +01:00
MacRimi 8c51957bfa Update notification service 2026-03-08 16:33:52 +01:00
MacRimi 858a1bba4f Update health_monitor.py 2026-03-08 10:23:53 +01:00
MacRimi 17e4227978 Update notification service 2026-03-08 10:15:55 +01:00
MacRimi f8b5e07518 Update notification service 2026-03-08 10:00:17 +01:00
ProxMenuxBot d96e4019aa Update helpers_cache.json 2026-03-08 00:15:29 +00:00
MacRimi acc9760690 Update health_monitor.py 2026-03-08 00:58:37 +01:00
MacRimi 56dab535c3 Update notification service 2026-03-08 00:53:35 +01:00
MacRimi 94670711e7 Update flask_server.py 2026-03-08 00:30:05 +01:00
MacRimi 673c206e02 Update latency-detail-modal.tsx 2026-03-07 23:47:24 +01:00
MacRimi decd3bd134 Update latency-detail-modal.tsx 2026-03-07 23:40:19 +01:00
MacRimi 6e5c7aeab5 Update notification service 2026-03-07 23:33:13 +01:00
MacRimi 2647550324 Update notification service 2026-03-07 23:18:25 +01:00
MacRimi 424a63011b Update notification service 2026-03-07 23:05:51 +01:00
MacRimi 0e6a125c60 update notification service 2026-03-07 22:55:42 +01:00
MacRimi 758cae4f86 Update latency-detail-modal.tsx 2026-03-07 22:36:20 +01:00
MacRimi 6e8368c62a Update latency-detail-modal.tsx 2026-03-07 22:14:47 +01:00
MacRimi a14e554323 Update latency-detail-modal.tsx 2026-03-07 21:58:57 +01:00
MacRimi 6435202fa1 Update latency-detail-modal.tsx 2026-03-07 21:37:24 +01:00
MacRimi cf8425ff14 update notification service 2026-03-07 21:17:13 +01:00
MacRimi 9bb1c1b233 Update notification service 2026-03-07 20:38:18 +01:00
MacRimi c9ccc5e27e update notification service 2026-03-07 20:17:40 +01:00
MacRimi 7115b2ff54 Update notification service 2026-03-07 20:06:59 +01:00
MacRimi b89e234ba4 Update latency-detail-modal.tsx 2026-03-07 19:45:03 +01:00
MacRimi 70fbaa0bfd Update latency-detail-modal.tsx 2026-03-07 19:27:31 +01:00
MacRimi f6cdd4ff36 Update latency-detail-modal.tsx 2026-03-07 19:02:14 +01:00
MacRimi 1857f46452 Update notification service 2026-03-07 18:44:51 +01:00
MacRimi f95a6f4fd7 Update latency-detail-modal.tsx 2026-03-07 17:57:57 +01:00
MacRimi b0bc66f548 Update latency-detail-modal.tsx 2026-03-07 17:21:44 +01:00
MacRimi 34b4a6c3d8 Update latency-detail-modal.tsx 2026-03-07 17:06:47 +01:00
MacRimi b4c7463226 Update latency-detail-modal.tsx 2026-03-07 16:59:13 +01:00
MacRimi ca5b33ef69 Update latency-detail-modal.tsx 2026-03-07 16:42:01 +01:00
MacRimi acd980091d Update latency-detail-modal.tsx 2026-03-07 16:23:11 +01:00
MacRimi de5317987e Update latency-detail-modal.tsx 2026-03-07 12:02:20 +01:00
ProxMenuxBot 6b438bc4aa Update helpers_cache.json 2026-03-07 00:14:59 +00:00
MacRimi 9b1495a490 Update notification service 2026-03-06 21:25:14 +01:00
MacRimi f638011d63 Update latency-detail-modal.tsx 2026-03-06 20:54:40 +01:00
MacRimi 8447a95c8a Update latency-detail-modal.tsx 2026-03-06 20:34:59 +01:00
MacRimi 4feceaa1d1 Update latency-detail-modal.tsx 2026-03-06 20:13:31 +01:00
MacRimi 8383e381d1 Update latency-detail-modal.tsx 2026-03-06 20:02:49 +01:00
MacRimi a064a7471e Update notification service 2026-03-06 19:32:10 +01:00
MacRimi f0e3d7d09a Update notification_events.py 2026-03-06 19:05:08 +01:00
MacRimi 2b7f4ccd6c Update notification_manager.py 2026-03-06 18:57:43 +01:00
MacRimi 46fa89233b Update notification service 2026-03-06 18:44:27 +01:00
MacRimi 591099e42b Update storage-overview.tsx 2026-03-06 15:24:18 +01:00
MacRimi d08398ea57 Update flask_server.py 2026-03-06 14:43:18 +01:00
MacRimi 83f49742b6 Update notification service 2026-03-06 14:32:23 +01:00
ProxMenuxBot 50d07f81fd Update helpers_cache.json 2026-03-06 12:08:42 +00:00
MacRimi 594ee21fcd Update notification_events.py 2026-03-06 12:16:06 +01:00
MacRimi ea2763c48c Update notification service 2026-03-06 12:06:53 +01:00
MacRimi 925fe1cce0 Update notification service 2026-03-05 21:28:48 +01:00
MacRimi d927b462b6 Update notification service 2026-03-05 20:44:51 +01:00
MacRimi 5a79556ab2 Update flask_server.py 2026-03-05 19:59:17 +01:00
MacRimi 260870ad8a Update notification service 2026-03-05 19:49:35 +01:00
ProxMenuxBot 7d69e64adc Update helpers_cache.json 2026-03-05 18:34:30 +00:00
MacRimi 5af51096d8 Update notification service 2026-03-05 19:25:05 +01:00
MacRimi 898392725a Update notification service 2026-03-05 17:29:07 +01:00
MacRimi 9089035f18 Update notification service 2026-03-04 19:11:38 +01:00
MacRimi 66d2a68167 Update notification service 2026-03-03 21:12:19 +01:00
MacRimi 4a41e40592 Update notification_channels.py 2026-03-03 20:59:43 +01:00
MacRimi 2a75b920a0 Update notification_channels.py 2026-03-03 20:49:36 +01:00
MacRimi 2851eae423 Update notification_channels.py 2026-03-03 20:21:43 +01:00
MacRimi efc2295b8d Update notification service 2026-03-03 20:09:38 +01:00
MacRimi 2bee28a1d8 Update notification service 2026-03-03 19:45:54 +01:00
MacRimi a8cc995558 Update notification service 2026-03-03 19:36:33 +01:00
MacRimi 9a11c41424 Update notification service 2026-03-03 19:19:56 +01:00
MacRimi 2a4d056b59 Update notification service 2026-03-03 18:48:54 +01:00
MacRimi 5a77a398bd Update notification-settings.tsx 2026-03-03 17:50:56 +01:00
MacRimi 4cf2238c99 Update notification-settings.tsx 2026-03-03 17:23:38 +01:00
MacRimi 58df4f1481 update notification service 2026-03-03 16:42:12 +01:00
MacRimi da3f99a254 Update notification service 2026-03-03 13:40:46 +01:00
ProxMenuxBot c2fa6095cc Update helpers_cache.json 2026-03-03 12:08:48 +00:00
MacRimi f0b8ed20a2 update notification service 2026-03-02 23:21:40 +01:00
ProxMenuxBot 0b8b72be5c Update helpers_cache.json 2026-03-02 18:12:26 +00:00
MacRimi 18c6455837 Update notification service 2026-03-02 18:55:02 +01:00
MacRimi e0477015c4 Update notification-settings.tsx 2026-03-02 18:42:58 +01:00
MacRimi e99a4e2b08 Update notification-settings.tsx 2026-03-02 18:36:50 +01:00
MacRimi c44d06b0dc Update notification-settings.tsx 2026-03-02 18:26:23 +01:00
MacRimi 0e8327c085 Update notification-settings.tsx 2026-03-02 18:12:47 +01:00
MacRimi 5c5a86c7fc Update notification-settings.tsx 2026-03-02 18:06:27 +01:00
MacRimi a785213cb2 Update notification service 2026-03-02 18:01:34 +01:00
MacRimi e041440c97 Update notification_events.py 2026-03-02 17:27:45 +01:00
MacRimi 688ca8a604 Update notification service 2026-03-02 17:16:22 +01:00
ProxMenuxBot fd6f0967b0 Update helpers_cache.json 2026-03-02 12:08:55 +00:00
MacRimi 9fe58935c4 Update notification service 2026-03-01 22:56:25 +01:00
MacRimi 0dfb35730f Update notification service 2026-03-01 22:29:58 +01:00
MacRimi dc52f4c692 Update notification service 2026-03-01 18:44:11 +01:00
MacRimi bcf5395868 update notification service 2026-03-01 17:24:13 +01:00
MacRimi 3e96a89adf update notification service 2026-02-28 20:32:58 +01:00
MacRimi c0a882251d Update notification service 2026-02-28 20:22:24 +01:00
MacRimi 6a53b895e5 Update health-status-modal.tsx 2026-02-28 19:40:14 +01:00
MacRimi c5354d014c Update notification service 2026-02-28 19:18:13 +01:00
MacRimi 5e9ef37646 Update notification service 2026-02-28 18:07:00 +01:00
MacRimi cb96bea73d Update health_monitor.py 2026-02-28 17:31:03 +01:00
MacRimi 95fa2440ce Update notification service 2026-02-28 17:18:03 +01:00
ProxMenuxBot ca9698f75d Update helpers_cache.json 2026-02-28 12:05:10 +00:00
MacRimi 0f1413f130 Update notification service 2026-02-28 00:01:01 +01:00
MacRimi 52a4b604dd Update notification service 2026-02-27 23:49:26 +01:00
MacRimi 3c64ee7af2 Update notification service 2026-02-27 23:45:18 +01:00
MacRimi 026719cd88 Update health_monitor.py 2026-02-27 23:28:27 +01:00
MacRimi 9bac00ee29 Update health-status-modal.tsx 2026-02-27 23:11:57 +01:00
MacRimi 828c0f66a6 Update health_monitor.py 2026-02-27 20:07:01 +01:00
MacRimi 9841e92634 Update health_monitor.py 2026-02-27 19:55:02 +01:00
MacRimi 171e7ddcae Update notification service 2026-02-27 19:47:36 +01:00
ProxMenuxBot 968a5bd789 Update helpers_cache.json 2026-02-27 18:10:15 +00:00
MacRimi be119a69af Update health_monitor.py 2026-02-27 18:42:44 +01:00
MacRimi 800c3c11be Update health_monitor.py 2026-02-27 18:36:51 +01:00
MacRimi 1242da5ed1 Update notification service 2026-02-27 18:27:24 +01:00
MacRimi 8bf4fa0cf1 Update notification service 2026-02-26 21:28:14 +01:00
MacRimi 0693acc07b Update notification service 2026-02-26 20:58:40 +01:00
MacRimi 17eecfca9d Update notification service 2026-02-26 20:31:44 +01:00
MacRimi 4d24d6d17b Update notification service 2026-02-26 18:21:01 +01:00
ProxMenuxBot 1fe4ee5b81 Update helpers_cache.json 2026-02-26 12:12:55 +00:00
ProxMenuxBot 137aeac91a Update helpers_cache.json 2026-02-25 18:21:39 +00:00
ProxMenuxBot ccb0b58a2d Update helpers_cache.json 2026-02-25 12:11:18 +00:00
MacRimi ffc202f6a3 Update notification_events.py 2026-02-24 20:11:29 +01:00
MacRimi f7fd728683 Update notification service 2026-02-24 19:52:27 +01:00
MacRimi 46c04e5a81 Update notification service 2026-02-24 19:27:43 +01:00
MacRimi f43feb825f Update notification service 2026-02-24 18:20:43 +01:00
MacRimi 05cd21d44e Update notification service 2026-02-24 18:10:12 +01:00
MacRimi 4182af75ff Update notification service 2026-02-24 17:55:03 +01:00
ProxMenuxBot 680123eb64 Update helpers_cache.json 2026-02-24 12:11:48 +00:00
ProxMenuxBot aec04f0b8c Update helpers_cache.json 2026-02-24 00:15:52 +00:00
ProxMenuxBot e75bbc0a22 Update helpers_cache.json 2026-02-23 18:20:58 +00:00
ProxMenuxBot 81fc625c5d Update helpers_cache.json 2026-02-23 12:10:39 +00:00
ProxMenuxBot f85683239f Update helpers_cache.json 2026-02-22 12:05:24 +00:00
MacRimi 507f769357 Update notification service 2026-02-21 22:36:58 +01:00
MacRimi e3f7e8c97a Update notification service 2026-02-21 22:19:45 +01:00
MacRimi 49e9e26bff Update flask_notification_routes.py 2026-02-21 21:55:37 +01:00
MacRimi fccd4c12ca Update notification service 2026-02-21 21:36:27 +01:00
MacRimi 06c9ff481e Update flask_notification_routes.py 2026-02-21 21:11:57 +01:00
MacRimi 50e5775062 Update flask_notification_routes.py 2026-02-21 20:55:18 +01:00
MacRimi 91da8db589 Update flask_notification_routes.py 2026-02-21 20:49:59 +01:00
MacRimi 0d854ae42b Update notification service 2026-02-21 20:33:22 +01:00
MacRimi ec21050fad Update notification service 2026-02-21 19:56:50 +01:00
MacRimi 67c61a5829 Update notification service 2026-02-21 18:47:15 +01:00
MacRimi e685668959 update notificacion service 2026-02-21 18:13:38 +01:00
MacRimi de13eb5b96 Update notification service 2026-02-21 17:23:03 +01:00
ProxMenuxBot c0f54c334e Update helpers_cache.json 2026-02-21 00:16:10 +00:00
MacRimi f134fcb528 Update notification service 2026-02-20 17:55:05 +01:00
MacRimi d5954a3a32 Update notification service 2026-02-19 20:51:54 +01:00
MacRimi bd28e312fc Update notification-settings.tsx 2026-02-19 20:30:11 +01:00
MacRimi 7208d5b2bf Update notification service 2026-02-19 19:56:20 +01:00
ProxMenuxBot 5c2d4e4718 Update helpers_cache.json 2026-02-19 18:16:51 +00:00
MacRimi 8cdeae6c3f Update notification service 2026-02-19 18:37:42 +01:00
MacRimi e7bc6d09f2 Update flask_notification_routes.py 2026-02-19 17:46:50 +01:00
MacRimi 4ce2699a48 Update notication service 2026-02-19 17:26:36 +01:00
MacRimi 7c5cdb9161 Update notification service 2026-02-19 17:02:02 +01:00
ProxMenuxBot 64a0aa6157 Update helpers_cache.json 2026-02-19 12:11:34 +00:00
MacRimi 34d04e57dd Update notification service 2026-02-18 17:24:26 +01:00
MacRimi 1317c5bddc Update health monitor 2026-02-17 20:01:45 +01:00
MacRimi 74b6f565e9 Update health monitor 2026-02-17 17:57:49 +01:00
MacRimi 08f49d4d0b Update health-status-modal.tsx 2026-02-17 17:24:47 +01:00
MacRimi 99605b6a55 Update health monitor 2026-02-17 17:09:00 +01:00
MacRimi beeeabc377 Update health monitor 2026-02-17 16:49:26 +01:00
ProxMenuxBot ff2e40d49a Update helpers_cache.json 2026-02-17 12:10:47 +00:00
MacRimi 31c5eeb6c3 Update health monitor 2026-02-17 11:35:11 +01:00
MacRimi 8004ee48c9 Update health monitor 2026-02-16 22:53:16 +01:00
MacRimi a1d48a28e9 Update health monitor 2026-02-16 22:26:43 +01:00
MacRimi 0f81f45c5f update health monitor 2026-02-16 22:07:10 +01:00
MacRimi 05f7957557 Update health_monitor.py 2026-02-16 18:39:36 +01:00
MacRimi 1ed8f5d124 Update health monitor 2026-02-16 18:19:29 +01:00
MacRimi 2ee5be7402 Update health monitor 2026-02-16 15:48:41 +01:00
ProxMenuxBot 1226e7bee1 Update helpers_cache.json 2026-02-16 12:10:34 +00:00
MacRimi dcbc52efc6 Update spinner 2026-02-16 12:11:37 +01:00
MacRimi 92b0a1478a Update log 2026-02-16 11:43:12 +01:00
MacRimi f27c7fdf31 Update system-logs.tsx 2026-02-16 10:55:26 +01:00
MacRimi 18a427b501 Update logs 2026-02-16 09:52:33 +01:00
MacRimi b7951b730d Update spinner 2026-02-16 09:48:03 +01:00
ProxMenuxBot 342203bb81 Update helpers_cache.json 2026-02-16 00:16:44 +00:00
ProxMenuxBot e4bc526a09 Update helpers_cache.json 2026-02-15 18:05:49 +00:00
MacRimi f75e30afd0 Update security.tsx 2026-02-14 18:34:36 +01:00
MacRimi 9f11238d43 Update security.tsx 2026-02-14 18:21:53 +01:00
MacRimi 070a1b47e5 Update security.tsx 2026-02-14 17:59:44 +01:00
MacRimi 3e8661f5ca Update temperature-detail-modal.tsx 2026-02-14 17:40:09 +01:00
MacRimi 9f8c27ddc1 Update modal 2026-02-14 17:28:35 +01:00
MacRimi bafaaf9c47 Update security.tsx 2026-02-14 17:01:14 +01:00
MacRimi 1ee5863da7 Update firewall 2026-02-14 16:49:08 +01:00
MacRimi ace4d83789 Update security.tsx 2026-02-14 16:39:24 +01:00
MacRimi 1da1c178d0 Update security.tsx 2026-02-14 16:08:11 +01:00
MacRimi c429cb2ed1 Update flask_server.py 2026-02-14 14:32:30 +01:00
MacRimi 40c40f81fc Update flask_auth_routes.py 2026-02-14 12:23:08 +01:00
MacRimi 6647a3b083 Update security.tsx 2026-02-14 12:07:51 +01:00
MacRimi 782eaef440 Update flask_server.py 2026-02-14 11:12:54 +01:00
MacRimi 6003310a39 Update system-overview.tsx 2026-02-13 20:34:09 +01:00
MacRimi 229ac5006b Update temperature-detail-modal.tsx 2026-02-13 20:23:56 +01:00
MacRimi 322687c658 Update modal temperature 2026-02-13 20:09:50 +01:00
MacRimi fe3963dfe2 Update temperature-detail-modal.tsx 2026-02-13 19:58:57 +01:00
MacRimi e4a57b97b7 Update modal temperature 2026-02-13 19:40:22 +01:00
MacRimi 9b48c498f5 new modal temperature 2026-02-13 19:07:24 +01:00
MacRimi 4228177920 Update auto_post_install.sh 2026-02-13 18:21:28 +01:00
MacRimi e98637321d Update auto_post_install.sh 2026-02-13 18:17:32 +01:00
ProxMenuxBot 5941bd4b68 Update helpers_cache.json 2026-02-13 12:08:41 +00:00
MacRimi a686360c1f Update memory speed 2026-02-13 12:30:27 +01:00
MacRimi 20ee9da1ec Update flask_auth_routes.py 2026-02-13 11:02:53 +01:00
MacRimi c89baf34a8 Update 2FA 2026-02-13 10:51:27 +01:00
MacRimi 00230d1b8f Update security_manager.py 2026-02-12 20:13:53 +01:00
MacRimi 4396d57e3d Update security_manager.py 2026-02-12 19:43:52 +01:00
MacRimi 86789f677a Update security_manager.py 2026-02-12 19:23:55 +01:00
MacRimi 8fb2deeab0 Update security_manager.py 2026-02-12 19:15:08 +01:00
MacRimi 2099bbe58f Update security_manager.py 2026-02-12 18:58:39 +01:00
ProxMenuxBot 1c95319608 Update helpers_cache.json 2026-02-11 12:14:33 +00:00
ProxMenuxBot eeea948844 Update helpers_cache.json 2026-02-11 00:20:59 +00:00
MacRimi c4b1820d08 Update security_manager.py 2026-02-10 19:32:49 +01:00
MacRimi 59cc2741b8 Update security_manager.py 2026-02-10 19:04:04 +01:00
MacRimi cc34d33090 Update security 2026-02-10 18:28:43 +01:00
ProxMenuxBot 59bb0070e9 Update helpers_cache.json 2026-02-10 12:15:22 +00:00
ProxMenuxBot ec2206ade0 Update helpers_cache.json 2026-02-09 18:16:40 +00:00
MacRimi 06a3e6b472 Update root access 2026-02-09 18:20:09 +01:00
MacRimi 9108882921 Update security.tsx 2026-02-09 17:18:42 +01:00
ProxMenuxBot 7796f7d3bc Update helpers_cache.json 2026-02-09 12:14:11 +00:00
MacRimi 00a0ae6561 Update security.tsx 2026-02-09 10:55:42 +01:00
MacRimi 6310293190 Update security.tsx 2026-02-08 20:54:04 +01:00
MacRimi 809930df9a Update security.tsx 2026-02-08 20:38:27 +01:00
MacRimi f1874d4ab1 Update security.tsx 2026-02-08 20:25:20 +01:00
MacRimi cc0f401855 Update Lynis 2026-02-08 20:00:23 +01:00
MacRimi 42626f3bce Update security_manager.py 2026-02-08 19:37:44 +01:00
MacRimi 22d570b024 Update lynis 2026-02-08 19:24:40 +01:00
MacRimi 6d0a07f212 Update security_manager.py 2026-02-08 18:52:18 +01:00
MacRimi a512b5a110 Update security 2026-02-08 18:30:18 +01:00
MacRimi bde3dade14 Update lynis 2026-02-08 17:51:20 +01:00
MacRimi de2058d966 Update fail2ban 2026-02-08 17:23:53 +01:00
MacRimi 7f191764be Update security 2026-02-08 17:01:49 +01:00
MacRimi 7f9da757aa update firewall 2026-02-08 16:19:48 +01:00
MacRimi f07e8cfe14 Update security 2026-02-08 13:19:24 +01:00
ProxMenuxBot b806bf80b1 Update helpers_cache.json 2026-02-08 12:05:47 +00:00
ProxMenuxBot 173ea58701 Update helpers_cache.json 2026-02-08 00:20:03 +00:00
MacRimi 3ad5b72ebf Update flask_server.py 2026-02-07 19:02:08 +01:00
MacRimi 567e2e5d6d Update flask_server.py 2026-02-07 18:53:21 +01:00
MacRimi 616bd0ac91 Backend SSL config manager and API endpoints 2026-02-07 18:36:14 +01:00
MacRimi 108a169e7c Update 2FA 2026-02-07 18:03:46 +01:00
MacRimi eab902d68e Update Log 2026-02-07 17:37:55 +01:00
MacRimi 985f6e89ec Update modal LXC 2026-02-07 11:45:28 +01:00
MacRimi 0480989fd2 Update virtual-machines.tsx 2026-02-07 11:25:33 +01:00
MacRimi 72ffe420b7 Update virtual-machines.tsx 2026-02-07 10:55:06 +01:00
ProxMenuxBot 775b6ff4fd Update helpers_cache.json 2026-02-07 00:15:38 +00:00
ProxMenuxBot b0f18461b3 Update helpers_cache.json 2026-02-06 18:13:29 +00:00
ProxMenuxBot b8ccbfd222 Update helpers_cache.json 2026-02-06 12:09:32 +00:00
ProxMenuxBot c2fa497137 Update helpers_cache.json 2026-02-05 18:16:47 +00:00
ProxMenuxBot bdcfa6929c Update helpers_cache.json 2026-02-05 12:09:37 +00:00
ProxMenuxBot 8470b58b60 Update helpers_cache.json 2026-02-04 18:14:29 +00:00
ProxMenuxBot 002413c067 Update helpers_cache.json 2026-02-04 12:08:26 +00:00
ProxMenuxBot ecce59e734 Update helpers_cache.json 2026-02-04 00:14:14 +00:00
MacRimi f5d169eaa2 Update flask_server.py 2026-02-03 23:17:44 +01:00
MacRimi b3b921e1ae Update flask_server.py 2026-02-03 23:02:19 +01:00
MacRimi 91f15b723e Update modal lxc 2026-02-03 22:53:37 +01:00
MacRimi 303dcb1eb6 Update flask_server.py 2026-02-03 22:30:55 +01:00
MacRimi 4eaeb1b020 Create textarea.tsx 2026-02-03 22:18:00 +01:00
MacRimi f13427ca27 Update modal lxc 2026-02-03 22:10:53 +01:00
MacRimi 458f2cdf16 Update virtual-machines.tsx 2026-02-03 18:29:00 +01:00
MacRimi df588f25bf Update virtual-machines.tsx 2026-02-03 18:14:40 +01:00
MacRimi bd0fdff29c Update virtual-machines.tsx 2026-02-03 18:12:37 +01:00
MacRimi 774da61da1 Update virtual-machines.tsx 2026-02-03 18:07:55 +01:00
MacRimi 42e67e01aa Update virtual-machines.tsx 2026-02-03 17:41:34 +01:00
MacRimi 36e201e824 Update virtual-machines.tsx 2026-02-03 17:33:18 +01:00
MacRimi 497233c9f1 Update virtual-machines.tsx 2026-02-03 17:10:19 +01:00
MacRimi 4b7c9a1bd3 Update virtual-machines.tsx 2026-02-03 17:00:43 +01:00
MacRimi 7c2d6d6618 Update virtual-machines.tsx 2026-02-03 16:57:50 +01:00
MacRimi c44e0afb81 Update virtual-machines.tsx 2026-02-03 16:50:29 +01:00
MacRimi d3ef3c7452 Update virtual-machines.tsx 2026-02-03 16:45:01 +01:00
ProxMenuxBot 1935c76f30 Update helpers_cache.json 2026-02-02 18:11:22 +00:00
MacRimi 71056d8f15 Update virtual-machines.tsx 2026-02-02 19:01:07 +01:00
MacRimi 8d34119e7a Update modal lxc 2026-02-02 18:49:18 +01:00
MacRimi f159ee77cd Update modal vm 2026-02-02 17:46:23 +01:00
MacRimi d336c4f5b7 Update modal vm 2026-02-02 17:29:14 +01:00
ProxMenuxBot 81b7a3e665 Update helpers_cache.json 2026-02-02 12:08:58 +00:00
MacRimi 1870f74f0c Update terminal-panel.tsx 2026-02-01 01:32:20 +01:00
MacRimi d19f9c6888 Create dropdown-menu.tsx 2026-02-01 01:18:20 +01:00
ProxMenuxBot a68bf6fc8f Update helpers_cache.json 2026-02-01 00:16:53 +00:00
MacRimi c2d2745777 Update terminal modal 2026-02-01 01:13:13 +01:00
MacRimi fc8bf841bf Update lxc-terminal-modal.tsx 2026-02-01 00:50:53 +01:00
MacRimi 08f435597a Update lxc-terminal-modal.tsx 2026-02-01 00:42:51 +01:00
MacRimi 58a4e475ad Update lxc-terminal-modal.tsx 2026-01-31 18:29:19 +01:00
MacRimi 5bfc911e1b Update lxc-terminal-modal.tsx 2026-01-31 18:17:50 +01:00
MacRimi d2c7362736 Update terminal modal 2026-01-31 18:10:55 +01:00
MacRimi 82cac690fa Update terminal panel 2026-01-31 17:40:08 +01:00
MacRimi 3c3c902087 Update terminal modal 2026-01-31 17:19:50 +01:00
MacRimi 964538eb43 Update lxc-terminal-modal.tsx 2026-01-31 17:02:11 +01:00
MacRimi 5e8b2bdb50 Update terminal panel 2026-01-31 16:48:22 +01:00
MacRimi e3d10495f3 Update terminal modal 2026-01-31 16:17:36 +01:00
MacRimi 6c3886ad24 Update virtual-machines.tsx 2026-01-31 15:57:59 +01:00
MacRimi ba727f53c4 Update flask_server.py 2026-01-31 15:53:56 +01:00
MacRimi 35a4737e43 Update node-metrics-charts.tsx 2026-01-31 15:34:11 +01:00
MacRimi abde8652b2 Update system-overview.tsx 2026-01-31 15:25:15 +01:00
MacRimi cadef0bf81 Update terminal-panel.tsx 2026-01-31 14:34:26 +01:00
MacRimi caac696244 Update terminal panel 2026-01-31 12:25:23 +01:00
MacRimi 6910a0b4bd Update intel_gpu_tools.sh 2026-01-31 11:52:34 +01:00
MacRimi 74e2584e4d Update flask_server.py 2026-01-31 11:44:32 +01:00
ProxMenuxBot 459dd2d9c7 Update helpers_cache.json 2026-01-31 00:14:20 +00:00
MacRimi 0f5c83c1c2 Update intel_gpu_tools.sh 2026-01-29 21:26:11 +01:00
ProxMenuxBot fed4cc2a97 Update helpers_cache.json 2026-01-29 19:56:29 +00:00
MacRimi c238711b3e Update hardware.tsx 2026-01-29 20:03:39 +01:00
MacRimi f42334917e Create intel_gpu_tools.sh 2026-01-29 19:59:12 +01:00
MacRimi 09004d4c09 Update share-common.func 2026-01-29 19:54:54 +01:00
MacRimi 68da9b2f69 Create amd_gpu_tools.sh 2026-01-29 19:41:48 +01:00
MacRimi 454ff37a72 Update gpu monitor 2026-01-29 19:04:52 +01:00
MacRimi ca13d18d7d Update backend monitor 2026-01-29 18:27:36 +01:00
MacRimi 1657a7dbe3 Update hardware_monitor.py 2026-01-29 18:21:23 +01:00
MacRimi 61e925eaab Update hardware_monitor.py 2026-01-29 18:07:49 +01:00
MacRimi 09d3313e15 Update flask_server.py 2026-01-29 17:59:04 +01:00
MacRimi a20d61037e Update backend monitor 2026-01-29 17:47:10 +01:00
ProxMenuxBot 7eaa692712 Update helpers_cache.json 2026-01-28 18:06:50 +00:00
ProxMenuxBot 691bae9a96 Update helpers_cache.json 2026-01-28 12:05:34 +00:00
ProxMenuxBot d5a8c9b7d1 Update helpers_cache.json 2026-01-26 18:05:37 +00:00
ProxMenuxBot 8c20e7c661 Update helpers_cache.json 2026-01-25 00:14:01 +00:00
ProxMenuxBot 47a2d28c6a Update helpers_cache.json 2026-01-24 00:12:27 +00:00
ProxMenuxBot 31f8961e27 Update helpers_cache.json 2026-01-23 18:04:28 +00:00
ProxMenuxBot 424bd0bc28 Update helpers_cache.json 2026-01-23 12:05:47 +00:00
ProxMenuxBot 9c078583dd Update helpers_cache.json 2026-01-22 12:05:55 +00:00
ProxMenuxBot ca27048679 Update helpers_cache.json 2026-01-22 00:13:22 +00:00
MacRimi 4e65663748 Update coral lxc 2026-01-20 20:17:53 +01:00
ProxMenuxBot c7c5cbde83 Update helpers_cache.json 2026-01-20 00:12:48 +00:00
ProxMenuxBot a4905ad207 Update helpers_cache.json 2026-01-19 18:04:06 +00:00
MacRimi bebf0e692a Update License 2026-01-19 17:15:00 +01:00
ProxMenuxBot 8ff9a87dfe Update helpers_cache.json 2026-01-19 12:06:36 +00:00
ProxMenuxBot 62f2d8ac16 Update helpers_cache.json 2026-01-19 00:12:53 +00:00
ProxMenuxBot 8fef2a6232 Update helpers_cache.json 2026-01-18 18:04:16 +00:00
ProxMenuxBot 94064fe78c Update helpers_cache.json 2026-01-17 12:05:12 +00:00
ProxMenuxBot 2ffcc43adc Update helpers_cache.json 2026-01-16 18:05:23 +00:00
ProxMenuxBot 3846fce73a Update helpers_cache.json 2026-01-16 12:05:44 +00:00
ProxMenuxBot ea950e9dbc Update helpers_cache.json 2026-01-15 18:07:27 +00:00
ProxMenuxBot f2639c4ff1 Update helpers_cache.json 2026-01-14 18:05:47 +00:00
ProxMenuxBot 32c1798eb8 Update helpers_cache.json 2026-01-13 12:05:29 +00:00
ProxMenuxBot 75e3167b65 Update helpers_cache.json 2026-01-12 18:04:41 +00:00
ProxMenuxBot ad07a61aa7 Update helpers_cache.json 2026-01-08 18:04:25 +00:00
ProxMenuxBot c91b6329f3 Update helpers_cache.json 2026-01-07 00:11:18 +00:00
ProxMenuxBot 9cc60efd5a Update helpers_cache.json 2026-01-03 12:04:28 +00:00
ProxMenuxBot 08eeea6b9c Update helpers_cache.json 2026-01-02 12:05:29 +00:00
ProxMenuxBot 8dea7335de Update helpers_cache.json 2025-12-30 18:04:18 +00:00
ProxMenuxBot 2ad6d43422 Update helpers_cache.json 2025-12-29 00:12:44 +00:00
ProxMenuxBot 12c2e7aefb Update helpers_cache.json 2025-12-28 12:04:17 +00:00
ProxMenuxBot 6b62e46950 Update helpers_cache.json 2025-12-28 00:13:42 +00:00
ProxMenuxBot 853c58e0a0 Update helpers_cache.json 2025-12-26 18:03:55 +00:00
ProxMenuxBot eb0abc425a Update helpers_cache.json 2025-12-26 12:05:50 +00:00
ProxMenuxBot c808e40bf6 Update helpers_cache.json 2025-12-25 12:29:08 +00:00
ProxMenuxBot f0bbb14f3f Update helpers_cache.json 2025-12-24 18:20:56 +00:00
ProxMenuxBot 95dd0ea6fb Update helpers_cache.json 2025-12-24 01:06:53 +00:00
ProxMenuxBot 7f34102ae6 Update helpers_cache.json 2025-12-23 01:07:03 +00:00
ProxMenuxBot 7623962da5 Update helpers_cache.json 2025-12-22 01:10:38 +00:00
ProxMenuxBot cfb34b59df Update helpers_cache.json 2025-12-21 12:25:59 +00:00
ProxMenuxBot e5004bb55e Update helpers_cache.json 2025-12-20 01:03:45 +00:00
ProxMenuxBot c0193fdf73 Update helpers_cache.json 2025-12-19 01:08:01 +00:00
ProxMenuxBot 6cbafd557c Update helpers_cache.json 2025-12-16 12:30:30 +00:00
ProxMenuxBot ee8ab75907 Update helpers_cache.json 2025-12-15 12:32:47 +00:00
MacRimi f2e93ad69e Update route.ts 2025-12-14 02:37:02 +01:00
MacRimi 5faf3fd61c Update route.ts 2025-12-14 02:20:07 +01:00
MacRimi 956a8f4864 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-12-14 02:12:40 +01:00
MacRimi d26bc56b5c Update route.ts 2025-12-14 02:12:26 +01:00
ProxMenuxBot 7457770ef8 Update helpers_cache.json 2025-12-14 01:11:59 +00:00
MacRimi 54af9073cb Update web 2025-12-14 01:57:36 +01:00
MacRimi a8dcf5e8f5 Update web 2025-12-14 01:38:07 +01:00
MacRimi 9e3334d75f Update web 2025-12-13 21:19:08 +01:00
MacRimi cca6e71911 Update web 2025-12-13 20:20:21 +01:00
ProxMenuxBot 7fbd377ab2 Update helpers_cache.json 2025-12-13 18:18:09 +00:00
MacRimi 24417feba3 Update next.config.mjs 2025-12-13 19:09:26 +01:00
ProxMenuxBot f8c24964e3 Update helpers_cache.json 2025-12-13 01:02:58 +00:00
MacRimi 1ae2ebfaf0 Update nvidia_installer.sh 2025-12-11 22:07:01 +01:00
MacRimi 4feea6d153 Update script-terminal-modal.tsx 2025-12-11 21:30:30 +01:00
MacRimi ec6b658685 Update script-terminal-modal.tsx 2025-12-11 21:22:15 +01:00
MacRimi fb0f05a08d Update script-terminal-modal.tsx 2025-12-11 19:56:20 +01:00
MacRimi 11bc477f1f Update script-terminal-modal.tsx 2025-12-11 19:06:53 +01:00
MacRimi 9760375855 Update script-terminal-modal.tsx 2025-12-11 18:44:56 +01:00
MacRimi a6e20bd9f0 Update script-terminal-modal.tsx 2025-12-11 18:27:11 +01:00
MacRimi 90fedbf9a2 Update script-terminal-modal.tsx 2025-12-11 18:19:38 +01:00
MacRimi eb03262abc Update script-terminal-modal.tsx 2025-12-11 18:13:37 +01:00
MacRimi 59eb6e5f1b Update script-terminal-modal.tsx 2025-12-11 18:06:24 +01:00
MacRimi edf513aca9 Update script-terminal-modal.tsx 2025-12-11 17:55:19 +01:00
MacRimi efed63519a Update script-terminal-modal.tsx 2025-12-11 17:44:23 +01:00
MacRimi d78f781506 Update script-terminal-modal.tsx 2025-12-11 17:30:12 +01:00
ProxMenuxBot 93fe269b09 Update helpers_cache.json 2025-12-11 12:31:23 +00:00
ProxMenuxBot 8cad6c4e56 Update helpers_cache.json 2025-12-11 01:07:29 +00:00
MacRimi f92049dc71 Update script-terminal-modal.tsx 2025-12-10 23:48:43 +01:00
MacRimi a3497a9d39 Update script-terminal-modal.tsx 2025-12-10 23:43:05 +01:00
MacRimi bfc0a2ed57 Update script-terminal-modal.tsx 2025-12-10 23:34:30 +01:00
MacRimi c49b45d262 Update script-terminal-modal.tsx 2025-12-10 23:26:11 +01:00
MacRimi 15678cf96a Update script-terminal-modal.tsx 2025-12-10 23:15:50 +01:00
MacRimi feeaaa7f2b Update script-terminal-modal.tsx 2025-12-10 23:06:03 +01:00
MacRimi 50df1a2212 Update script-terminal-modal.tsx 2025-12-10 22:57:19 +01:00
MacRimi ac9254d049 Update script-terminal-modal.tsx 2025-12-10 22:48:07 +01:00
MacRimi e15eeb36a5 Update script-terminal-modal.tsx 2025-12-10 22:30:11 +01:00
MacRimi e275e03d4e Update script-terminal-modal.tsx 2025-12-10 22:19:41 +01:00
MacRimi 41c8826ca8 Update script-terminal-modal.tsx 2025-12-10 22:10:41 +01:00
MacRimi d8af31ba5b Update script-terminal-modal.tsx 2025-12-10 21:57:16 +01:00
MacRimi 2eb7cb1687 Update AppImage 2025-12-10 21:47:52 +01:00
MacRimi 207e75f5b9 Update script-terminal-modal.tsx 2025-12-10 20:17:13 +01:00
MacRimi 7b9e1a71a3 Update script-terminal-modal.tsx 2025-12-10 20:12:03 +01:00
MacRimi 345838c6ce Update script-terminal-modal.tsx 2025-12-10 19:54:39 +01:00
MacRimi b02a60f4b3 Update script-terminal-modal.tsx 2025-12-10 19:44:39 +01:00
MacRimi ecd3a4e490 Update script-terminal-modal.tsx 2025-12-10 19:26:19 +01:00
MacRimi 8c0c9bd60a Update script-terminal-modal.tsx 2025-12-10 19:14:57 +01:00
MacRimi 943a8bf02d Update script-terminal-modal.tsx 2025-12-10 18:54:35 +01:00
MacRimi d3beb72652 Update script-terminal-modal.tsx 2025-12-10 18:36:31 +01:00
MacRimi c62dd2014e Update script-terminal-modal.tsx 2025-12-10 18:22:30 +01:00
MacRimi 62fee7827b Update script-terminal-modal.tsx 2025-12-10 18:15:45 +01:00
MacRimi 80b9d16494 Update AppImage 2025-12-10 18:09:21 +01:00
MacRimi cb5581c49f Update AppImage 2025-12-10 17:56:11 +01:00
MacRimi 0098000ae0 Update terminal-panel.tsx 2025-12-10 17:45:38 +01:00
MacRimi ddc8429499 Update AppImage 2025-12-10 17:35:43 +01:00
MacRimi 0424961d46 Update AppImage 2025-12-10 17:08:41 +01:00
MacRimi cbf510cfd1 Update script-terminal-modal.tsx 2025-12-10 17:01:17 +01:00
MacRimi cbb44ae253 Update script-terminal-modal.tsx 2025-12-10 16:56:56 +01:00
MacRimi 4dd4f045aa Update AppImage 2025-12-10 16:50:52 +01:00
ProxMenuxBot ab0d7f8dc6 Update helpers_cache.json 2025-12-09 12:29:33 +00:00
MacRimi 69f93fcb59 Actualizar select_nas_iso.sh 2025-12-08 22:52:34 +01:00
ProxMenuxBot de68e0d7c2 Update helpers_cache.json 2025-12-08 01:05:59 +00:00
MacRimi cdbcb451e1 Update script-terminal-modal.tsx 2025-12-06 23:41:34 +01:00
MacRimi 105c543a98 Update script-terminal-modal.tsx 2025-12-06 23:25:35 +01:00
MacRimi ab421e3184 Update AppImage 2025-12-06 23:06:18 +01:00
MacRimi d76b7a99b8 Apdate AppImage 2025-12-06 22:40:24 +01:00
MacRimi e8dae63e05 Update script-terminal-modal.tsx 2025-12-06 22:33:17 +01:00
MacRimi ea58b70435 Update script-terminal-modal.tsx 2025-12-06 22:26:42 +01:00
MacRimi f90f6f364a Update script-terminal-modal.tsx 2025-12-06 22:20:34 +01:00
MacRimi 7fc967c64c Update script-terminal-modal.tsx 2025-12-06 22:09:54 +01:00
MacRimi 8969a229d1 Update script-terminal-modal.tsx 2025-12-06 22:03:12 +01:00
MacRimi 9601e0428e Update script-terminal-modal.tsx 2025-12-06 21:55:31 +01:00
MacRimi 94fd91ce4a Update script-terminal-modal.tsx 2025-12-06 21:50:15 +01:00
MacRimi 310f972c7f Update script-terminal-modal.tsx 2025-12-06 21:37:43 +01:00
MacRimi 4378a5843c Update AppImage 2025-12-06 21:30:17 +01:00
MacRimi 9bd403ec51 Update script-terminal-modal.tsx 2025-12-06 21:20:23 +01:00
MacRimi 2f53786ca9 Update terminal-panel.tsx 2025-12-06 20:56:36 +01:00
MacRimi 07ed213c94 Update script-terminal-modal.tsx 2025-12-06 20:46:13 +01:00
MacRimi 05a2eca9a7 Update AppImage 2025-12-06 20:27:00 +01:00
MacRimi d30c836d04 Update AppImage 2025-12-06 20:10:24 +01:00
MacRimi 8c623adad8 Update AppImage 2025-12-06 19:43:41 +01:00
MacRimi 5191edfc0c Update AppImage 2025-12-06 19:31:07 +01:00
MacRimi ff99663d5c Update AppImage 2025-12-06 19:19:54 +01:00
MacRimi 360335a608 Update AppImage 2025-12-06 19:03:19 +01:00
MacRimi 1c83e5eeab Update AppImage 2025-12-06 18:36:34 +01:00
MacRimi 122ebb12f4 Update AppImage 2025-12-06 13:54:37 +01:00
MacRimi fed242315d Update AppImage 2025-12-06 13:48:46 +01:00
MacRimi 84e8e18ef8 Update script-terminal-modal.tsx 2025-12-06 13:38:19 +01:00
MacRimi 36a1916b5f Update script-terminal-modal.tsx 2025-12-06 13:28:56 +01:00
MacRimi a1089460d7 Update AppImage 2025-12-06 13:11:04 +01:00
MacRimi c62f0dea6f Update script-terminal-modal.tsx 2025-12-06 13:00:29 +01:00
MacRimi a6c121dc33 Update AppImage 2025-12-06 12:46:41 +01:00
MacRimi c627c65a7d Update AppImage 2025-12-06 12:25:57 +01:00
MacRimi 72006aff21 Update hardware.tsx 2025-12-06 12:14:08 +01:00
MacRimi 68338ebeff Update flask_terminal_routes.py 2025-12-06 12:06:12 +01:00
MacRimi 49b8503b64 Update AppImage 2025-12-06 11:54:36 +01:00
MacRimi 0fc41df7e7 Update flask_terminal_routes.py 2025-12-06 11:37:16 +01:00
MacRimi bb82c52747 Update AppImage 2025-12-06 11:30:49 +01:00
MacRimi a79367fb1c Update script-terminal-modal.tsx 2025-12-06 11:25:27 +01:00
MacRimi 4dbc6db6f0 Update script-terminal-modal.tsx 2025-12-06 11:19:26 +01:00
ProxMenuxBot a50cee62be Update helpers_cache.json 2025-12-05 18:18:53 +00:00
ProxMenuxBot 382aa5cb16 Update helpers_cache.json 2025-12-05 01:05:55 +00:00
ProxMenuxBot d1c2ff277b Update helpers_cache.json 2025-12-03 01:05:21 +00:00
ProxMenuxBot 92b08b5550 Update helpers_cache.json 2025-12-02 12:30:00 +00:00
ProxMenuxBot da85470fef Update helpers_cache.json 2025-12-02 01:04:45 +00:00
MacRimi 9d1e7d94cc Update script-terminal-modal.tsx 2025-12-01 01:40:04 +01:00
MacRimi 65438286ec Update script-terminal-modal.tsx 2025-12-01 01:26:29 +01:00
MacRimi ffa7d27148 Update script-terminal-modal.tsx 2025-12-01 01:22:04 +01:00
MacRimi 4a7d951d0d Update script-terminal-modal.tsx 2025-12-01 01:15:19 +01:00
MacRimi 89f1911a6e Update package.json 2025-12-01 01:07:41 +01:00
MacRimi b990bd1792 Update AppImage 2025-12-01 01:04:31 +01:00
MacRimi 88667416d8 Update hybrid-script-monitor.tsx 2025-12-01 00:38:42 +01:00
MacRimi 216491012e Update hybrid-script-monitor.tsx 2025-12-01 00:31:23 +01:00
MacRimi c88f3dcf75 Update flask_script_runner.py 2025-12-01 00:17:04 +01:00
MacRimi 6c3e21339d Update hybrid-script-monitor.tsx 2025-11-30 23:57:08 +01:00
MacRimi e7f9f9f13d Update hybrid-script-monitor.tsx 2025-11-30 23:47:33 +01:00
MacRimi 6b8d6da5be Update AppImage 2025-11-30 23:40:13 +01:00
MacRimi 8c73c5d662 Update hybrid-script-monitor.tsx 2025-11-30 23:27:32 +01:00
MacRimi f7dc2c9a9e Update hybrid-script-monitor.tsx 2025-11-30 23:20:34 +01:00
MacRimi eadf825b67 Update hybrid-script-monitor.tsx 2025-11-30 23:13:37 +01:00
MacRimi 150999d71b Update AppImage 2025-11-30 23:00:02 +01:00
MacRimi 7cd89a594e Update AppImage 2025-11-30 22:50:40 +01:00
MacRimi b67f1cb4b8 Update hardware.tsx 2025-11-30 22:12:51 +01:00
MacRimi 4678f8c7da Update hardware.tsx 2025-11-30 21:55:37 +01:00
MacRimi 0577f48437 Update hardware.tsx 2025-11-30 21:49:13 +01:00
MacRimi 0c079482f0 Update hardware.tsx 2025-11-30 21:38:49 +01:00
MacRimi 684fe3945d Update AppImage 2025-11-30 21:19:38 +01:00
MacRimi d91d325744 Update route.ts 2025-11-30 20:36:25 +01:00
MacRimi 040d7564ed Update AppImage 2025-11-30 20:25:44 +01:00
MacRimi d1db34445e Update AppImage 2025-11-30 19:40:42 +01:00
MacRimi 9639dd422a Update AppImage 2025-11-30 19:15:07 +01:00
MacRimi f60bfe8c54 Update nvidia installer 2025-11-30 18:29:05 +01:00
MacRimi fe53c11447 Update utils.sh 2025-11-30 18:05:56 +01:00
MacRimi 9bd17bdf6f Update utils.sh 2025-11-30 17:47:30 +01:00
MacRimi 4b64308951 Update utils.sh 2025-11-30 17:12:11 +01:00
MacRimi bb7dacea91 Update AppImage 2025-11-30 16:02:44 +01:00
MacRimi 0a369621a3 Update nvidia_installer.sh 2025-11-30 15:41:40 +01:00
MacRimi e0ee1a50ae new script nvidia 2025-11-30 15:31:19 +01:00
MacRimi 6b49fc4294 Update utils.sh 2025-11-30 12:57:29 +01:00
ProxMenuxBot ed20ea6af4 Update helpers_cache.json 2025-11-30 01:11:38 +00:00
ProxMenuxBot 73fe4dc7a0 Update helpers_cache.json 2025-11-29 18:18:19 +00:00
MacRimi c4967de530 Update utils.sh 2025-11-29 11:04:28 +01:00
ProxMenuxBot bcf3d36ba1 Update helpers_cache.json 2025-11-28 18:19:11 +00:00
MacRimi d52bd7f012 Update update-pve9_2.sh 2025-11-28 19:10:26 +01:00
MacRimi e6232be244 Update update-pve9_2.sh 2025-11-28 17:31:32 +01:00
MacRimi b33f313e2e Update update-pve9_2.sh 2025-11-28 17:06:59 +01:00
ProxMenuxBot 0b4372fe88 Update helpers_cache.json 2025-11-27 18:19:16 +00:00
MacRimi 4e07c7f2dc Update system-overview.tsx 2025-11-27 18:11:56 +01:00
MacRimi 941e194df3 Update system-overview.tsx 2025-11-27 17:50:26 +01:00
MacRimi 2b8f94f457 Update health_monitor.py 2025-11-27 17:30:19 +01:00
MacRimi 7ec8c0cea5 Update health_monitor.py 2025-11-27 14:56:12 +01:00
MacRimi c69384dabd Update health_monitor.py 2025-11-27 13:39:02 +01:00
MacRimi 8c92216a1d Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-27 13:29:27 +01:00
MacRimi 41537c0bad Update health_monitor.py 2025-11-27 13:29:15 +01:00
ProxMenuxBot c112f56b37 Update helpers_cache.json 2025-11-27 12:28:29 +00:00
MacRimi f22de50527 Update flask_server.py 2025-11-27 12:45:57 +01:00
MacRimi a22e08f39d Update AppImage 2025-11-27 12:34:51 +01:00
MacRimi 210d470473 Update AppImage 2025-11-27 12:17:52 +01:00
MacRimi 0eebb77438 Update AppImage 2025-11-27 11:58:20 +01:00
MacRimi f819cb9c5f Update hardware.tsx 2025-11-27 09:32:41 +01:00
MacRimi 240963f1f3 Update hardware.tsx 2025-11-27 09:29:12 +01:00
MacRimi 16819d98fa Update AppImage 2025-11-27 09:19:45 +01:00
MacRimi 8be7e0f0cb Update hardware.tsx 2025-11-26 21:33:25 +01:00
MacRimi 3a51daf51b Update hardware.tsx 2025-11-26 21:27:56 +01:00
MacRimi 7622e72b70 Update flask_server.py 2025-11-26 21:15:35 +01:00
MacRimi b59173cac4 Update flask_server.py 2025-11-26 20:46:56 +01:00
MacRimi 18411ee5bd Update AppImage 2025-11-26 20:31:09 +01:00
MacRimi 6e1c6fab2d Update flask_server.py 2025-11-26 20:22:10 +01:00
MacRimi 98eb2d8836 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-26 19:38:28 +01:00
MacRimi 504e32f922 Update flask_server.py 2025-11-26 19:38:24 +01:00
ProxMenuxBot c096054b1f Update helpers_cache.json 2025-11-26 18:17:30 +00:00
MacRimi ac2f198851 Update flask_server.py 2025-11-26 18:57:01 +01:00
MacRimi 9aed659f17 Update AppImage 2025-11-26 18:44:37 +01:00
MacRimi 0b8f5d3b22 Update AppImage 2025-11-26 18:00:01 +01:00
MacRimi 55c74e8891 Update AppImage 2025-11-26 17:36:23 +01:00
MacRimi 3a49aa6a67 Update hardware_monitor.py 2025-11-26 16:48:24 +01:00
MacRimi 10770b6fe1 Update AppImage 2025-11-26 12:27:25 +01:00
MacRimi c81ea08f42 Update terminal-panel.tsx 2025-11-25 22:54:27 +01:00
MacRimi 73b6ab4a18 Update terminal-panel.tsx 2025-11-25 22:42:14 +01:00
MacRimi 7497235d7b Update terminal-panel.tsx 2025-11-25 22:35:23 +01:00
MacRimi 27191e4234 Update terminal-panel.tsx 2025-11-25 22:23:02 +01:00
MacRimi 7b0110ce42 Update AppImage 2025-11-25 19:44:40 +01:00
MacRimi 117a635a1e Update sidebar.tsx 2025-11-25 19:31:22 +01:00
MacRimi 98c922fb3e Update AppImage 2025-11-25 19:26:50 +01:00
MacRimi bf84d04f1f Update AppImage 2025-11-25 19:08:00 +01:00
MacRimi f4e358b509 Update AppImge 2025-11-25 19:04:54 +01:00
MacRimi 060ad7966e Update proxmox-dashboard.tsx 2025-11-25 17:21:29 +01:00
MacRimi f0301fd1a4 Update AppImage 2025-11-25 17:21:20 +01:00
MacRimi ae8212a51d Update terminal-panel.tsx 2025-11-24 23:54:46 +01:00
MacRimi 393a0d5cdc Update terminal-panel.tsx 2025-11-24 23:26:36 +01:00
MacRimi 4cf43a8d74 Update terminal-panel.tsx 2025-11-24 23:10:50 +01:00
MacRimi 74b2f47e3a Update terminal-panel.tsx 2025-11-24 22:57:09 +01:00
ProxMenuxBot 1e727db09a Update helpers_cache.json 2025-11-24 18:21:47 +00:00
MacRimi 1daa120d06 Update terminal-panel.tsx 2025-11-24 19:11:11 +01:00
MacRimi a1d2445ae6 Update terminal-panel.tsx 2025-11-24 19:01:15 +01:00
MacRimi 4d4e35e24b Update terminal-panel.tsx 2025-11-24 18:41:42 +01:00
MacRimi 400cc599e3 Update terminal-panel.tsx 2025-11-24 18:28:06 +01:00
MacRimi e55352346b Update terminal-panel.tsx 2025-11-24 18:18:10 +01:00
MacRimi cca226dec0 Update terminal-panel.tsx 2025-11-24 17:50:41 +01:00
MacRimi fec95c91f8 Update terminal-panel.tsx 2025-11-24 17:29:38 +01:00
MacRimi 9955418a8e Remove 'Contributing' section from README 2025-11-24 15:43:10 +01:00
MacRimi 90c7539956 Remove contributing and development setup sections
Removed the contributing section and development setup instructions from the README.
2025-11-24 15:41:52 +01:00
MacRimi a751e45602 Update README.md 2025-11-24 15:40:49 +01:00
MacRimi b50d388f9e Update terminal-panel.tsx 2025-11-24 13:37:17 +01:00
MacRimi fd60292b5d Update terminal-panel.tsx 2025-11-24 13:25:20 +01:00
MacRimi 4ebb0c432e Update terminal-panel.tsx 2025-11-24 13:16:35 +01:00
MacRimi 897b2478e8 Update terminal-panel.tsx 2025-11-24 13:02:04 +01:00
MacRimi b8ebb7f6c4 Update terminal-panel.tsx 2025-11-24 12:24:16 +01:00
MacRimi f32dba72b4 Update terminal-panel.tsx 2025-11-24 12:10:07 +01:00
MacRimi 498ad280e0 Update terminal-panel.tsx 2025-11-24 11:49:20 +01:00
MacRimi 32358de718 Update AppImage 2025-11-24 11:37:00 +01:00
MacRimi 2474a6ce01 Update terminal-panel.tsx 2025-11-24 11:21:50 +01:00
MacRimi 1ba45200ee Update AppImage 2025-11-24 11:01:48 +01:00
MacRimi da793856ce Update terminal-panel.tsx 2025-11-23 23:46:34 +01:00
MacRimi d950588c36 Update terminal-panel.tsx 2025-11-23 23:42:19 +01:00
MacRimi 2b4a5d2ce7 Update terminal-panel.tsx 2025-11-23 23:29:49 +01:00
MacRimi 86daedc802 Update AppImage 2025-11-23 23:23:59 +01:00
MacRimi 3788487196 Update terminal-panel.tsx 2025-11-23 23:10:23 +01:00
MacRimi 25559b7e3e Update terminal-panel.tsx 2025-11-23 22:51:37 +01:00
MacRimi 246db33ee6 Update terminal-panel.tsx 2025-11-23 22:47:30 +01:00
MacRimi d435e9b58b Update terminal-panel.tsx 2025-11-23 22:45:09 +01:00
MacRimi 09ecc79050 Update terminal-panel.tsx 2025-11-23 22:39:46 +01:00
MacRimi 1914435707 Update terminal-panel.tsx 2025-11-23 22:34:50 +01:00
MacRimi f6c237afc5 Update terminal-panel.tsx 2025-11-23 22:30:14 +01:00
MacRimi a1f2579047 Update terminal-panel.tsx 2025-11-23 22:25:21 +01:00
MacRimi 1ea6617a5d Update terminal-panel.tsx 2025-11-23 22:21:24 +01:00
MacRimi 489175aa45 Update AppImage 2025-11-23 22:17:45 +01:00
MacRimi cb72f43b03 Update globals.css 2025-11-23 22:01:50 +01:00
MacRimi 4bbbcc7c39 Update terminal-panel.tsx 2025-11-23 21:43:17 +01:00
MacRimi af1e4884b7 Update AppImage 2025-11-23 21:39:45 +01:00
MacRimi 5213d6255a Update globals.css 2025-11-23 21:32:46 +01:00
MacRimi a9af689aa5 Update terminal-panel.tsx 2025-11-23 21:22:52 +01:00
MacRimi 407a9f7780 Update terminal-panel.tsx 2025-11-23 21:15:54 +01:00
MacRimi a0ca667ca7 Update terminal-panel.tsx 2025-11-23 21:08:14 +01:00
MacRimi c2f6f97c34 Update terminal-panel.tsx 2025-11-23 20:59:50 +01:00
MacRimi 2daefbe2f4 Update terminal-panel.tsx 2025-11-23 20:47:42 +01:00
MacRimi 84b0c9d4b7 Update terminal-panel.tsx 2025-11-23 20:26:26 +01:00
MacRimi 0d848569f0 Update terminal-panel.tsx 2025-11-23 20:19:07 +01:00
MacRimi 611f8397ca Update terminal-panel.tsx 2025-11-23 20:16:05 +01:00
MacRimi 11ed0a1367 Update terminal-panel.tsx 2025-11-23 20:06:33 +01:00
MacRimi ff51966fbb Update terminal-panel.tsx 2025-11-23 19:58:31 +01:00
MacRimi 5491d51eba Update terminal-panel.tsx 2025-11-23 19:53:39 +01:00
MacRimi 61a5a7e929 Update terminal-panel.tsx 2025-11-23 19:46:57 +01:00
MacRimi 3de000bc94 Update terminal-panel.tsx 2025-11-23 19:41:36 +01:00
MacRimi ef456e6ea0 Update terminal-panel.tsx 2025-11-23 19:30:01 +01:00
MacRimi 2a8b67e22a Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-23 19:25:58 +01:00
MacRimi c35b66f6e1 Update terminal-panel.tsx 2025-11-23 19:25:49 +01:00
ProxMenuxBot c8348dcaaa Update helpers_cache.json 2025-11-23 18:18:06 +00:00
MacRimi e38174110e Update terminal-panel.tsx 2025-11-23 19:14:18 +01:00
MacRimi a95130c01f Update terminal-panel.tsx 2025-11-23 19:10:22 +01:00
MacRimi 0e93417090 Update terminal-panel.tsx 2025-11-23 18:56:14 +01:00
MacRimi 07054bf55a Update terminal-panel.tsx 2025-11-23 18:51:46 +01:00
MacRimi 368eab476a Update AppImage 2025-11-23 18:41:58 +01:00
MacRimi 996679a2d2 Update AppImage 2025-11-23 18:34:47 +01:00
MacRimi 85a6943cd5 Update terminal-panel.tsx 2025-11-23 18:17:00 +01:00
MacRimi 0b96893f3b Update terminal-panel.tsx 2025-11-23 18:12:09 +01:00
MacRimi 846e2e27ba Update terminal-panel.tsx 2025-11-23 17:57:46 +01:00
MacRimi 43ea9b7696 Update globals.css 2025-11-23 17:42:00 +01:00
MacRimi 9dd4df2ca9 Update terminal-panel.tsx 2025-11-23 17:29:11 +01:00
MacRimi 2b4fb55526 Update globals.css 2025-11-23 17:17:26 +01:00
MacRimi 72cf16301f Update AppImage 2025-11-23 17:09:25 +01:00
MacRimi c512dde028 Update terminal-panel.tsx 2025-11-23 16:56:41 +01:00
MacRimi 1e13c7ab31 Update globals.css 2025-11-23 16:48:32 +01:00
MacRimi cdbab86dee Update AppImage 2025-11-23 16:32:01 +01:00
MacRimi fec03d1fd4 Update AppImage 2025-11-23 16:10:41 +01:00
MacRimi 6aa24e23c0 Update terminal-panel.tsx 2025-11-23 14:16:21 +01:00
MacRimi 78770d1da5 Update terminal-panel.tsx 2025-11-23 14:04:43 +01:00
MacRimi 6f72447e2e Update terminal-panel.tsx 2025-11-23 13:52:11 +01:00
MacRimi cb75a15a6f Update terminal-panel.tsx 2025-11-23 13:47:40 +01:00
MacRimi c3555237b3 Update terminal-panel.tsx 2025-11-23 13:39:54 +01:00
MacRimi e4a2cc7ac8 Update terminal-panel.tsx 2025-11-23 13:28:30 +01:00
MacRimi 3900d305b9 Update terminal-panel.tsx 2025-11-23 12:15:55 +01:00
MacRimi cb3d501649 Update terminal-panel.tsx 2025-11-23 12:02:34 +01:00
MacRimi 28323a486a Update terminal-panel.tsx 2025-11-23 11:56:38 +01:00
MacRimi dfcad4b9fd Update terminal-panel.tsx 2025-11-23 11:51:51 +01:00
MacRimi 6fb2869cd8 Update terminal-panel.tsx 2025-11-23 11:42:30 +01:00
MacRimi e764e39ba9 Update terminal-panel.tsx 2025-11-23 11:21:17 +01:00
MacRimi 128077dcbc Update AppImage 2025-11-23 10:57:28 +01:00
MacRimi 1c51107f1e Update AppImage 2025-11-22 23:59:55 +01:00
MacRimi d154cab054 Update proxmox-dashboard.tsx 2025-11-22 23:55:10 +01:00
MacRimi 7ed4368d5b Update appImage 2025-11-22 23:46:43 +01:00
MacRimi ee64df2376 Update AppImage 2025-11-22 23:34:09 +01:00
MacRimi b13f03eb97 Update terminal-panel.tsx 2025-11-22 23:19:10 +01:00
MacRimi 8d20829428 Update AppImage 2025-11-22 23:08:11 +01:00
MacRimi 97401f609e Update terminal-panel.tsx 2025-11-22 22:57:49 +01:00
MacRimi fe074729ea Update AppImage 2025-11-22 22:50:23 +01:00
MacRimi db5141e010 Update AppImage 2025-11-22 22:28:34 +01:00
MacRimi 4564fdc6aa Update AppImage 2025-11-22 22:15:39 +01:00
MacRimi a477b36a57 Update terminal-panel.tsx 2025-11-22 22:06:07 +01:00
MacRimi 3b8ae2c879 Update terminal-panel.tsx 2025-11-22 21:58:29 +01:00
MacRimi ebe3a51398 Update appImage 2025-11-22 21:43:14 +01:00
MacRimi 76d22f0cb5 Update AppImage 2025-11-22 21:35:22 +01:00
MacRimi c61d676dfb Update terminal-panel.tsx 2025-11-22 21:26:35 +01:00
MacRimi b1913e7204 Update terminal-panel.tsx 2025-11-22 21:12:05 +01:00
MacRimi b6609e0a14 Update AppImage 2025-11-22 21:06:44 +01:00
MacRimi 55fa759344 Update AppImage 2025-11-22 20:58:06 +01:00
MacRimi 8992a713cc Update AppImage 2025-11-22 20:50:05 +01:00
MacRimi c55dcec252 Update AppImage 2025-11-22 20:41:36 +01:00
MacRimi e3dd6cbef5 Update AppImage 2025-11-22 20:27:41 +01:00
MacRimi dd3e5ea368 Update globals.css 2025-11-22 20:20:57 +01:00
MacRimi ac2e77e0d6 Update globals.css 2025-11-22 20:17:10 +01:00
MacRimi 9f57622f54 Update globals.css 2025-11-22 20:13:02 +01:00
MacRimi cfed460eba Update globals.css 2025-11-22 20:09:36 +01:00
MacRimi 06f97b671f Update globals.css 2025-11-22 20:06:43 +01:00
MacRimi aebf83d735 Update AppImage 2025-11-22 20:02:42 +01:00
MacRimi 31894dd117 Update globals.css 2025-11-22 19:44:00 +01:00
MacRimi e041d802ec Update globals.css 2025-11-22 19:37:31 +01:00
MacRimi 82ea15388c Update globals.css 2025-11-22 19:33:58 +01:00
MacRimi bf9ed8ff00 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-22 19:27:57 +01:00
MacRimi c02606df6a Update Appimage 2025-11-22 19:27:45 +01:00
ProxMenuxBot 7372e2e385 Update helpers_cache.json 2025-11-22 18:17:41 +00:00
MacRimi ba86fa6d3e Update terminal-panel.tsx 2025-11-22 19:07:27 +01:00
MacRimi 0e434cbd1c Update AppImage 2025-11-22 18:15:12 +01:00
MacRimi c89300022a Update AppImage 2025-11-22 17:54:30 +01:00
MacRimi 1300756d6f Update AppImage 2025-11-22 17:40:47 +01:00
MacRimi c4ad02ff92 Update terminal-panel.tsx 2025-11-22 17:32:47 +01:00
MacRimi b3f47f140a Update AppImage 2025-11-22 17:16:18 +01:00
MacRimi 2206b3d5b5 Update terminal-panel.tsx 2025-11-22 11:56:29 +01:00
MacRimi b08f8a450d Update terminal-panel.tsx 2025-11-22 11:43:17 +01:00
MacRimi 37c8be8a6e Update terminal-panel.tsx 2025-11-22 11:21:11 +01:00
MacRimi ae58c265a0 Update AppImage 2025-11-22 11:04:21 +01:00
MacRimi 54e6d1aa16 Update AppImage 2025-11-22 10:33:35 +01:00
MacRimi 4ddb5f14d9 Update flask_terminal_routes.py 2025-11-21 20:12:07 +01:00
MacRimi 623aec495b Update proxmox-dashboard.tsx 2025-11-21 19:56:22 +01:00
MacRimi f6d2b9bad0 Update AppImage 2025-11-21 19:49:42 +01:00
MacRimi 08b5a278f3 Update proxmox-dashboard.tsx 2025-11-21 19:44:15 +01:00
MacRimi f62b30b50d Update AppImage 2025-11-21 19:32:27 +01:00
MacRimi 50e3b8e7d4 Update terminal-panel.tsx 2025-11-21 19:25:23 +01:00
MacRimi e26956dbe8 Update terminal-panel.tsx 2025-11-21 19:15:35 +01:00
MacRimi cff2c12d70 Update build_appimage.sh 2025-11-21 18:53:30 +01:00
MacRimi 5781d532a4 Update build_appimage.sh 2025-11-21 18:47:56 +01:00
MacRimi f161a593f8 Update page.tsx 2025-11-21 18:40:19 +01:00
MacRimi 5725d5a2fe Update AppImage 2025-11-21 18:36:09 +01:00
MacRimi 23280fd97b Update AppImage 2025-11-21 18:32:10 +01:00
MacRimi fe6679f16a Update update-pve9_2.sh 2025-11-21 18:01:34 +01:00
MacRimi 19a95a3670 Update network-traffic-chart.tsx 2025-11-19 22:54:41 +01:00
MacRimi 90cffb3791 Update format-network.ts 2025-11-19 22:47:24 +01:00
MacRimi 31168fbeca Update network-metrics.tsx 2025-11-19 22:33:28 +01:00
MacRimi c4cce5d184 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-19 22:05:49 +01:00
MacRimi 08b59dd082 Update network-metrics.tsx 2025-11-19 22:05:45 +01:00
ProxMenuxBot 4aaf1a5868 Update helpers_cache.json 2025-11-19 18:20:10 +00:00
MacRimi 6e78fa0b1f Update AppImage 2025-11-19 18:43:08 +01:00
MacRimi e1a42189a6 Update system-overview.tsx 2025-11-19 18:18:47 +01:00
MacRimi 386e0c9b6b Update format-network.ts 2025-11-19 18:09:52 +01:00
MacRimi 3b1b423936 Update AppImage 2025-11-19 17:58:03 +01:00
MacRimi 8e8e8161bb Update AppImage 2025-11-19 17:30:01 +01:00
MacRimi b368fde82d Update appImage 2025-11-19 17:15:32 +01:00
ProxMenuxBot 7267111083 Update helpers_cache.json 2025-11-19 12:28:30 +00:00
MacRimi d05dab6633 Update AppImage 2025-11-18 22:05:54 +01:00
MacRimi e1409a8045 Update AppImage 2025-11-18 21:27:24 +01:00
MacRimi ae69fec7ce Update AppImage 2025-11-18 21:11:56 +01:00
MacRimi a2862f22f6 Update AppImage 2025-11-18 20:56:15 +01:00
MacRimi 7db8e18bcc Update AppImage 2025-11-18 19:43:18 +01:00
MacRimi 0ffe1272fe Update virtual-machines.tsx 2025-11-18 19:24:23 +01:00
MacRimi 92b54075c4 Update AppImage 2025-11-18 19:11:14 +01:00
MacRimi ce5c679d6b Update AppImage 2025-11-18 19:00:51 +01:00
MacRimi 4f61386b21 Update flask_server.py 2025-11-18 18:33:57 +01:00
MacRimi 2738ae1abc Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-18 17:14:46 +01:00
MacRimi f5e43ff7b4 Update flask_server.py 2025-11-18 17:14:30 +01:00
MacRimi 63c499bf2c Merge pull request #93 from riri-314/networkGraph
Add option to change network unit
2025-11-18 17:00:23 +01:00
riri-314 9e72720bda revert code formating 2025-11-18 13:30:37 +01:00
ProxMenuxBot bbe10b2dab Update helpers_cache.json 2025-11-18 12:29:38 +00:00
riri-314 f3b0784651 Network metrics take network unit into acount 2025-11-18 13:19:23 +01:00
riri-314 9c0ea9b1c7 System oberview take network unit into account 2025-11-18 13:08:39 +01:00
riri-314 620a088c6c Removed debug code 2025-11-18 11:32:32 +01:00
riri-314 867a74cffb Added optional prop to display network traffic in bits or bytes 2025-11-18 11:26:49 +01:00
riri-314 f2316fdd3a Add setting to change network unit 2025-11-18 10:58:06 +01:00
MacRimi 7d49d4f948 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-17 19:26:44 +01:00
MacRimi f85b2b889c Update flask_server.py 2025-11-17 19:26:30 +01:00
ProxMenuxBot 9471ac4a52 Update helpers_cache.json 2025-11-17 18:20:20 +00:00
MacRimi db520c39e3 Update flask_server.py 2025-11-17 18:48:20 +01:00
MacRimi cc59fbe2ba Delete ProxMenux-1.0.2-deb.AppImage 2025-11-17 18:22:15 +01:00
MacRimi e260af58f2 Create ProxMenux-1.0.2-deb.AppImage 2025-11-17 17:53:50 +01:00
MacRimi 166fc6dad9 Update AppImage 2025-11-17 17:47:58 +01:00
MacRimi 959433d737 Update install_proxmenux.sh 2025-11-17 17:11:41 +01:00
MacRimi f9fa9ce6d8 Update install_proxmenux.sh 2025-11-17 16:58:45 +01:00
MacRimi 6b3a41dfe0 Update install_proxmenux.sh 2025-11-17 16:40:56 +01:00
ProxMenuxBot 37428ecca4 Update helpers_cache.json 2025-11-16 18:18:01 +00:00
MacRimi 6934df253f update menu 2025-11-16 10:19:50 +01:00
MacRimi 00782598a4 Update menu 2025-11-16 01:21:07 +01:00
MacRimi 565c500810 Update menu 2025-11-16 01:17:54 +01:00
MacRimi e3c16166e6 Update menu 2025-11-16 01:15:48 +01:00
MacRimi cfa8d1b689 Update menu 2025-11-16 01:13:24 +01:00
MacRimi a19397f9b5 Update menu 2025-11-16 01:11:49 +01:00
MacRimi ddfc80b45f Update menu 2025-11-16 01:07:25 +01:00
MacRimi 8591f9b2a1 Update menu 2025-11-16 01:04:16 +01:00
MacRimi cb26a55e65 Update menu 2025-11-16 00:59:54 +01:00
MacRimi ef92394685 Update menu 2025-11-16 00:56:04 +01:00
MacRimi d588ef438e Update menu 2025-11-16 00:53:40 +01:00
MacRimi 09cd363b11 Update menu 2025-11-16 00:51:12 +01:00
MacRimi 2d5c7fdbb5 Update menu 2025-11-16 00:47:26 +01:00
MacRimi 2f0e28368d Update menu 2025-11-16 00:44:00 +01:00
MacRimi f7f1a2a3b3 Update menu 2025-11-16 00:33:26 +01:00
MacRimi 30afb85260 Update menu 2025-11-16 00:23:14 +01:00
MacRimi 78d883a1b4 Update menu 2025-11-16 00:17:22 +01:00
MacRimi 7913b673a3 Update menu 2025-11-16 00:13:52 +01:00
MacRimi 5edc27297f Update menu 2025-11-16 00:02:52 +01:00
MacRimi ebc24c2476 Update menu 2025-11-15 23:58:26 +01:00
MacRimi ed7dd037e5 Update menu 2025-11-15 23:52:39 +01:00
MacRimi 277924c04d Update menu 2025-11-15 23:48:50 +01:00
MacRimi 26ea0feddb Update menu 2025-11-15 23:46:02 +01:00
MacRimi 63c1eab930 Update menu 2025-11-15 23:42:17 +01:00
MacRimi 813e7711df Update menu 2025-11-15 23:40:46 +01:00
MacRimi 6c1f50a230 Remove Nginx configuration example from README
Removed example Nginx configuration from README.
2025-11-15 16:17:31 +01:00
MacRimi 470b6359ba Remove onboarding image sections from README
Removed multiple image sections related to storage management, network monitoring, virtual machines, hardware information, and system logs from the README.
2025-11-15 16:16:30 +01:00
MacRimi 2f45233748 Fix formatting issues in menu file 2025-11-15 15:20:12 +01:00
MacRimi 82fd52f572 Add LOCAL_SCRIPTS variable to menu configuration 2025-11-15 14:30:34 +01:00
MacRimi ed6331e6a4 Update config_menu.sh 2025-11-14 22:17:11 +01:00
MacRimi 2ae9188535 Update version.txt 2025-11-14 21:44:50 +01:00
MacRimi 1a55a5394a Update menu 2025-11-14 21:43:38 +01:00
MacRimi 99d2f37cfc Update version.txt 2025-11-14 21:42:15 +01:00
MacRimi 09b531e0c1 Update version.txt 2025-11-14 21:40:38 +01:00
MacRimi 232e872c0d Update menu 2025-11-14 21:37:55 +01:00
MacRimi acdb0d2838 Update menu 2025-11-14 21:30:58 +01:00
MacRimi bd0ea1379f Update menu 2025-11-14 21:02:43 +01:00
MacRimi 5461ea1a3a Update menu 2025-11-14 20:48:38 +01:00
MacRimi 200ee075b5 Update menu 2025-11-14 20:38:07 +01:00
MacRimi 79e9e5fcf1 Update version.txt 2025-11-14 20:33:07 +01:00
MacRimi a2df23d562 Update version.txt 2025-11-14 20:23:30 +01:00
MacRimi 55af3d7f65 Update version.txt 2025-11-14 20:21:12 +01:00
MacRimi ef54f3fe59 Update ChangeLog 2025-11-14 20:14:03 +01:00
MacRimi 9d84ff6aa7 Update version.txt 2025-11-14 20:10:57 +01:00
MacRimi ee26006f3c Update CHANGELOG.md 2025-11-14 20:08:07 +01:00
MacRimi be03035574 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-14 19:35:39 +01:00
MacRimi 619f3ca700 Create ProxMenux_offline.png 2025-11-14 19:35:37 +01:00
ProxMenuxBot 8553e63338 Update helpers_cache.json 2025-11-14 18:19:26 +00:00
MacRimi be4d9fe24b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-14 18:54:33 +01:00
MacRimi 497f727b08 Update menu_Helper_Scripts.sh 2025-11-14 18:54:32 +01:00
ProxMenuxBot 74a7569f4c Update helpers_cache.json 2025-11-14 17:18:37 +00:00
MacRimi 66185e3b91 Update cache 2025-11-14 18:17:23 +01:00
MacRimi 1b2beda695 Update workflow 2025-11-14 18:15:56 +01:00
MacRimi feb3b5ef5f Update config_menu.sh 2025-11-14 17:44:19 +01:00
MacRimi b2439331b3 Update install_proxmenux.sh 2025-11-14 17:26:08 +01:00
MacRimi f1000afc27 Delete ProxMenux-1.0.0.AppImage 2025-11-14 17:06:58 +01:00
github-actions[bot] 5fc2a82423 Update AppImage build (2025-11-14 16:02:55) 2025-11-14 16:02:55 +00:00
MacRimi a27f884418 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-14 16:59:35 +01:00
MacRimi cae4b73226 Update AppImage 2025-11-14 16:59:18 +01:00
MacRimi 50ed293de2 Merge pull request #76 from c78-contrib/main
Proxmenux offline mode
2025-11-14 16:56:04 +01:00
cod378 616b772a45 chore: remove test installer script
- Delete install_proxmenux_test.sh (1048 lines)
- Test installer no longer needed after validation
2025-11-14 02:40:48 +00:00
cod378 c1d00e21db fix: suppress systemctl output in ProxMenux Monitor uninstaller
- Redirect stdout and stderr to /dev/null for stop, disable, daemon-reload, and reset-failed commands
- Maintain clean console output during uninstallation process
2025-11-14 02:37:43 +00:00
cod378 2e8e2b61d3 fix: use MONITOR_SERVICE constant instead of MONITOR_SERVICE_NAME in uninstall function 2025-11-14 02:30:19 +00:00
cod378 ba595c9719 feat: add test installer script with offline support and ProxMenux Monitor uninstaller 2025-11-14 02:21:00 +00:00
cod378 e392f6a2b7 feat: add cod378 to contributors list in config_menu.sh 2025-11-14 02:20:07 +00:00
cod378 7457e71776 Merge branch 'MacRimi:main' into main 2025-11-13 23:08:13 -03:00
cod378 982d0dd72e Merge branch 'main' of github.com:c78-contrib/ProxMenuxOffline 2025-11-14 01:49:58 +00:00
cod378 d345f96518 feat: add ProxMenux Monitor uninstallation to config menu
- Add uninstall_proxmenux_monitor() function with systemd service cleanup
- Stop and disable monitor service if active
- Remove systemd unit file and reload daemon
- Integrate monitor uninstallation into main uninstall_proxmenu() workflow
- Define MONITOR_UNIT_FILE constant for service file path
2025-11-14 01:49:39 +00:00
ProxMenuxBot 469874e975 Update helpers_cache.json 2025-11-14 01:37:35 +00:00
ProxMenuxBot 6ba817cd43 Update helpers_cache.json 2025-11-14 01:03:21 +00:00
MacRimi 42f2e69e3a Update AppImagen 2025-11-13 21:12:32 +01:00
MacRimi 12442b4bd3 Update settings.tsx 2025-11-13 20:59:36 +01:00
MacRimi 305d37a13b Update settings.tsx 2025-11-13 20:43:13 +01:00
MacRimi 4baf60174f Update settings.tsx 2025-11-13 20:36:35 +01:00
MacRimi 8cd1ac6a4b Update settings.tsx 2025-11-13 20:29:22 +01:00
MacRimi c65fef638e Update settings.tsx 2025-11-13 20:23:08 +01:00
MacRimi a030cd7e28 Update flask_auth_routes.py 2025-11-13 20:16:39 +01:00
MacRimi 59a3b7eac5 Update settings.tsx 2025-11-13 20:10:00 +01:00
MacRimi 2faac48adf Update settings.tsx 2025-11-13 20:01:08 +01:00
MacRimi 3883039764 Update AppImage 2025-11-13 19:51:42 +01:00
MacRimi 307ed0c637 Update AppImage 2025-11-13 19:43:17 +01:00
MacRimi 96ffdb65d0 Update README.md 2025-11-13 19:36:45 +01:00
MacRimi 5ca55798b2 Update README.md 2025-11-13 19:20:06 +01:00
MacRimi cd32e11c6d Update AppImage 2025-11-13 19:11:56 +01:00
MacRimi 774cbe4c9d Update AppImage 2025-11-13 18:32:44 +01:00
MacRimi 1d0bb20506 Update AppImage 2025-11-13 18:21:37 +01:00
MacRimi 8064e107f4 Update AppImage 2025-11-13 17:56:42 +01:00
MacRimi c1d1121ed1 Update AppImage 2025-11-13 17:46:07 +01:00
MacRimi 07603f11db Update api-config.ts 2025-11-13 17:25:51 +01:00
MacRimi ec22c857d5 Update api-config.ts 2025-11-13 17:20:31 +01:00
MacRimi 364e808261 Update api-config.ts 2025-11-13 17:14:47 +01:00
MacRimi 1d47ad0c4b Update AppImage 2025-11-13 16:58:45 +01:00
cod378 c9d0eac6cc Merge branch 'main' of github.com:c78-contrib/ProxMenuxOffline 2025-11-13 03:23:32 +00:00
cod378 97fc72b78a fix: add validation for missing ProxMenux Monitor AppImage
- Check if AppImage exists before attempting installation
- Display clear error message when AppImage is not found
- Update config to track installation failure state
2025-11-13 03:23:11 +00:00
cod378 7b1111430b chore: remove unused offline installer script 2025-11-13 03:22:52 +00:00
cod378 4f3306cd0f Merge branch 'MacRimi:main' into main 2025-11-12 23:56:22 -03:00
cod378 d3f7056ece Merge branch 'main' of github.com:c78-contrib/ProxMenuxOffline 2025-11-13 02:52:26 +00:00
cod378 9f3286c570 feat: migrate to offline installer with enhanced monitor deployment
- Restructured installer to use local repository files instead of remote downloads for improved reliability
- Added comprehensive logging functions (spinner, type_text, msg_* helpers) and dual logo support for SSH/noVNC terminals
- Implemented AppImage version detection, SHA256 verification, and systemd service management for ProxMenux Monitor
- Updated metadata to reflect toolkit positioning and added contributor attribution
2025-11-13 02:50:41 +00:00
ProxMenuxBot 16fc737b2d Update helpers_cache.json 2025-11-12 18:29:07 +00:00
ProxMenuxBot 3e0ae709d9 Update helpers_cache.json 2025-11-12 18:19:51 +00:00
ProxMenuxBot 39ddb7c8f9 Update helpers_cache.json 2025-11-12 12:41:44 +00:00
ProxMenuxBot 3c509ce0e4 Update helpers_cache.json 2025-11-12 12:38:40 +00:00
cod378 048cf2fb8f docs: update project name references from ProxMenuxDotDeb to ProxMenuxOffline 2025-11-12 05:21:33 +00:00
cod378 0a20821c41 refactor: remove verbose cleanup messages from temporary file removal 2025-11-12 05:00:06 +00:00
cod378 e0eaf6267f fix: suppress git clone output to reduce installation noise 2025-11-12 04:53:37 +00:00
cod378 3ddf98277f refactor: update utils script source URL to offline repository 2025-11-12 04:48:29 +00:00
cod378 85294bcd33 fix: correct utils.sh download URL format 2025-11-12 04:40:10 +00:00
cod378 acff4523f3 refactor: simplify utils.sh loading with inline sourcing
- Replaced conditional file check with direct curl sourcing using process substitution
- Streamlined error handling to single-line check
2025-11-12 04:29:40 +00:00
cod378 bf71e1f9b8 refactor: update comment for utils.sh loading 2025-11-12 04:23:43 +00:00
cod378 f0bcdc1c25 refactor: move utils.sh loading to script initialization because this is an installer dependency 2025-11-12 04:22:11 +00:00
cod378 43526c58bd refactor: reorganize installer to use git-based offline installation
- Changed from local script loading to cloning repository into temporary directory
- Added cleanup function with trap to ensure temporary files are removed on exit
- Added git as a required dependency for the installation process
2025-11-12 04:11:41 +00:00
cod378 ce3c7a545e feat: add GitHub authentication script to gitignore 2025-11-12 04:04:10 +00:00
cod378 9498e4e7eb Merge branch 'MacRimi:main' into main 2025-11-11 23:39:28 -03:00
ProxMenuxBot 4ec7c207f4 Update helpers_cache.json 2025-11-12 01:29:08 +00:00
ProxMenuxBot 000479463f Update helpers_cache.json 2025-11-12 01:04:12 +00:00
MacRimi 6b2065e43c Update AppImage 2025-11-11 22:00:44 +01:00
MacRimi e97e1363ae Update release-notes-modal.tsx 2025-11-11 21:37:39 +01:00
MacRimi 697a1f8e31 Update release-notes-modal.tsx 2025-11-11 21:30:57 +01:00
MacRimi 035f43311a Update release-notes-modal.tsx 2025-11-11 21:25:10 +01:00
MacRimi c597f1252e Update release-notes-modal.tsx 2025-11-11 21:12:09 +01:00
MacRimi cc1e7a715c Update settings.tsx 2025-11-11 19:48:02 +01:00
MacRimi 80057e3014 Update AppImage 2025-11-11 19:36:37 +01:00
MacRimi 79ffba873f Update AppImage 2025-11-11 19:20:59 +01:00
MacRimi 673e1cf212 Update virtual-machines.tsx 2025-11-11 18:21:30 +01:00
MacRimi 7e878ecff2 Update virtual-machines.tsx 2025-11-11 18:07:03 +01:00
MacRimi 88cf51a602 Update AppImage 2025-11-11 17:59:36 +01:00
MacRimi 1860fffe07 Update system-logs.tsx 2025-11-11 17:26:47 +01:00
MacRimi fa925543db Update AppImage 2025-11-11 17:12:56 +01:00
MacRimi 825e99c59b Update api-config.ts 2025-11-11 17:04:26 +01:00
MacRimi 955bed80fb Update AppImage 2025-11-11 17:01:25 +01:00
ProxMenuxBot 03b9ac3ec4 Update helpers_cache.json 2025-11-11 12:40:51 +00:00
ProxMenuxBot c255d9a5d8 Update helpers_cache.json 2025-11-11 12:27:59 +00:00
ProxMenuxBot 401d973a51 Update helpers_cache.json 2025-11-11 06:31:46 +00:00
ProxMenuxBot a507d559e1 Update helpers_cache.json 2025-11-11 01:29:31 +00:00
MacRimi 9225982ca5 Create ProxMenux-1.0.1-beta2.AppImage 2025-11-10 19:03:30 +01:00
MacRimi 6f831530cc Update system-logs.tsx 2025-11-10 18:38:33 +01:00
MacRimi e6b4443074 Update virtual-machines.tsx 2025-11-10 18:22:44 +01:00
MacRimi 1c800cbd8f Update storage-overview.tsx 2025-11-10 17:45:40 +01:00
MacRimi a65924799e Update storage-overview.tsx 2025-11-10 17:38:46 +01:00
MacRimi adbfa1e73e Update AppImge 2025-11-10 17:25:22 +01:00
cod378 44a4226ad2 Merge branch 'MacRimi:main' into main 2025-11-10 11:04:47 -03:00
ProxMenuxBot 07ca3f13a0 Update helpers_cache.json 2025-11-10 12:41:30 +00:00
ProxMenuxBot 87a052b89c Update helpers_cache.json 2025-11-10 12:27:40 +00:00
MacRimi 2216543ac3 Update storage-overview.tsx 2025-11-09 23:59:21 +01:00
MacRimi 4254d57d12 Update storage-overview.tsx 2025-11-09 23:46:32 +01:00
MacRimi 30d93898d8 Update storage-overview.tsx 2025-11-09 23:36:50 +01:00
MacRimi 4c7ed2c2c5 Update health_monitor.py 2025-11-09 21:50:10 +01:00
MacRimi 4fb327cef8 Create ProxMenux-1.0.1-beat1.AppImage 2025-11-09 21:37:01 +01:00
MacRimi 588af3613b Update AppImage 2025-11-09 21:20:39 +01:00
MacRimi 5b5f325a4e Update health_monitor.py 2025-11-09 21:03:00 +01:00
MacRimi ae62196dff Update AppImage 2025-11-09 20:52:39 +01:00
MacRimi 27e66ee770 Update health_monitor.py 2025-11-09 20:02:38 +01:00
MacRimi 8fb8134898 Update AppImage 2025-11-09 18:23:27 +01:00
MacRimi a59489f804 Update health_persistence.py 2025-11-09 18:11:55 +01:00
MacRimi cbf3938784 Update AppImage 2025-11-09 18:05:35 +01:00
MacRimi c45ebfe598 Update AppImage 2025-11-09 17:56:37 +01:00
MacRimi a75aad1fdc Update build_appimage.sh 2025-11-09 17:34:11 +01:00
MacRimi a0635a1026 Update AppImage 2025-11-09 17:28:20 +01:00
MacRimi 27353e160f Update AppImage 2025-11-09 16:43:45 +01:00
MacRimi b9619efbbf Update AppImage 2025-11-09 16:30:29 +01:00
MacRimi 1712d32ef7 Update AppImage 2025-11-09 15:44:35 +01:00
MacRimi 014deb2118 Update flask_server.py 2025-11-09 15:35:01 +01:00
MacRimi 6077cf81f2 Update system-overview.tsx 2025-11-09 15:12:32 +01:00
MacRimi 0422c38096 Update system-overview.tsx 2025-11-09 14:48:51 +01:00
MacRimi 8c902ae04d Update system-overview.tsx 2025-11-09 14:39:06 +01:00
MacRimi 0a0b916067 Update system-overview.tsx 2025-11-09 14:34:32 +01:00
MacRimi 6822635a0b Update flask_server.py 2025-11-09 13:55:09 +01:00
MacRimi f9b15fd110 Update node-metrics-charts.tsx 2025-11-09 13:31:16 +01:00
MacRimi 131a458e69 Update node-metrics-charts.tsx 2025-11-09 13:20:30 +01:00
MacRimi 7260807d78 Create use-mobile.tsx 2025-11-09 13:12:51 +01:00
MacRimi df83d8a3e5 Update node-metrics-charts.tsx 2025-11-09 13:07:42 +01:00
MacRimi 0f45424458 Update virtual-machines.tsx 2025-11-09 12:52:10 +01:00
MacRimi 60f92d019b Update virtual-machines.tsx 2025-11-09 12:26:55 +01:00
ProxMenuxBot 2189487982 Update helpers_cache.json 2025-11-08 06:27:04 +00:00
cod378 3a44997795 Merge branch 'MacRimi:main' into main 2025-11-07 21:53:57 -03:00
MacRimi 1f04134aac Update Appimagen 2025-11-07 21:14:56 +01:00
MacRimi ce44538240 Update AppImage 2025-11-07 21:07:33 +01:00
MacRimi 5fd53883be Update two-factor-setup.tsx 2025-11-07 21:02:20 +01:00
MacRimi f064cc89ba Update AppImage 2025-11-07 20:55:00 +01:00
MacRimi 5dd8b3ee36 Update AppImage 2025-11-07 20:36:46 +01:00
MacRimi f2f9c37ee2 Update ppImage 2025-11-07 20:19:55 +01:00
MacRimi 6836777629 Update AppImage 2025-11-07 20:05:29 +01:00
MacRimi beefdd280f Update proxmox-dashboard.tsx 2025-11-07 19:52:45 +01:00
MacRimi 4b9ad0da7a Update AppImage 2025-11-07 19:43:55 +01:00
ProxMenuxBot f9fdd1686c Update helpers_cache.json 2025-11-07 18:28:17 +00:00
MacRimi 25fc3d931e Update AppImage 2025-11-07 19:25:36 +01:00
MacRimi fc7d0f2cd5 Update AppImage 2025-11-07 18:49:37 +01:00
MacRimi 60c91d9fe4 Update AppImage 2025-11-07 17:35:45 +01:00
MacRimi cc2d6849a8 Update AppImage 2025-11-07 17:00:32 +01:00
MacRimi 4a5379ea42 Update flask_server.py 2025-11-07 15:36:51 +01:00
MacRimi ba84c644df Update flask_server.py 2025-11-07 14:33:27 +01:00
MacRimi 37217b4219 Update AppImage 2025-11-07 14:14:43 +01:00
MacRimi 41dab03a5f Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-07 13:42:03 +01:00
MacRimi 6e48bf2a71 Update AppImage 2025-11-07 13:41:39 +01:00
ProxMenuxBot 1c1c6f513c Update helpers_cache.json 2025-11-07 12:40:39 +00:00
ProxMenuxBot 49c54f5593 Update helpers_cache.json 2025-11-07 12:27:33 +00:00
MacRimi d083e49d0b Update proxmox-dashboard.tsx 2025-11-07 13:06:47 +01:00
MacRimi 8dc2b833f4 Update AppImage 2025-11-07 12:54:10 +01:00
MacRimi 7d5726be50 Update proxmox-dashboard.tsx 2025-11-07 12:43:31 +01:00
MacRimi 246c1674d1 Uppdate AppImage 2025-11-07 12:37:11 +01:00
MacRimi 06b81f2b64 Update AppImage 2025-11-07 12:21:37 +01:00
MacRimi ee57797890 Updete AppImage 2025-11-07 12:17:10 +01:00
MacRimi a94000e114 Update AppImage 2025-11-07 11:05:57 +01:00
MacRimi e6655b35f3 Update AppImage 2025-11-07 10:58:50 +01:00
MacRimi 696ffde184 Update issue template contact link description to English 2025-11-07 10:16:17 +01:00
MacRimi 9e74e99923 Update feature_request.md 2025-11-07 10:15:09 +01:00
MacRimi bc5c6dadfb Translate bug report template to English 2025-11-07 10:14:37 +01:00
MacRimi 0d173a0bfe Update ISO file name for XigmaNAS version 14.3.0.5 2025-11-06 12:10:12 +01:00
MacRimi cd78920edd Fix ISO URL for XigmaNAS version 14.3.0.5 2025-11-06 09:08:09 +01:00
MacRimi 9a7ec62cf9 Update select_nas_iso.sh 2025-11-06 09:06:52 +01:00
cod378 2b4580cfe8 Merge branch 'MacRimi:main' into main 2025-11-05 23:24:21 -03:00
MacRimi b790c06294 Merge pull request #62 from MrCaringi/main
Add Issue Templates and Configuration for Categorization
2025-11-05 21:21:33 +01:00
JFC 61d87b46d9 Create feature request issue template
Adds a feature request template for GitHub issues.
2025-11-05 12:37:45 -06:00
JFC 143cb4cbab Modify bug report template and assign to MacRimi
Updated bug report template to include mandatory screenshots and assigned to 'MacRimi'.
2025-11-05 12:36:50 -06:00
JFC 22709dac36 Add issue template configuration for GitHub 2025-11-05 12:35:11 -06:00
ProxMenuxBot d97be93449 Update helpers_cache.json 2025-11-05 18:29:25 +00:00
ProxMenuxBot 5864de7dea Update helpers_cache.json 2025-11-05 18:19:51 +00:00
MacRimi 4ea5890e92 Update health-status-modal.tsx 2025-11-05 18:46:19 +01:00
MacRimi 876d51b009 Update health-status-modal.tsx 2025-11-05 18:38:29 +01:00
MacRimi 5b0d55c1a2 Update health_monitor.py 2025-11-05 18:30:31 +01:00
cod378 3ddb1421c3 feat: add offline installer script for ProxMenux
- Clones ProxMenux repository to temporary location and executes installation
- Includes automatic cleanup of temporary files and git dependency check
- Adds colored output and error handling for better user experience
2025-11-04 22:47:52 +00:00
cod378 58f9a7bc02 refactor: simplify utils.sh loading error handling 2025-11-04 22:47:00 +00:00
MacRimi e8e4b728ce Update proxmox-dashboard.tsx 2025-11-04 23:00:37 +01:00
MacRimi 0a4868192d Update proxmox-dashboard.tsx 2025-11-04 22:55:41 +01:00
MacRimi 9d81ffffe8 Update proxmox-dashboard.tsx 2025-11-04 22:47:11 +01:00
MacRimi e6fe4a09e5 Update AppImage 2025-11-04 22:28:42 +01:00
MacRimi 77c5ad7b09 Update AppImage 2025-11-04 21:59:28 +01:00
MacRimi b850e9615a Update proxmox-dashboard.tsx 2025-11-04 21:48:54 +01:00
MacRimi c2ea307821 Update AppImage 2025-11-04 21:42:38 +01:00
MacRimi fb588c0d60 Update flask_auth_routes.py 2025-11-04 21:36:31 +01:00
MacRimi fecbdf6190 Update build_appimage.sh 2025-11-04 21:32:14 +01:00
MacRimi bbbbf6892f Update flask_server.py 2025-11-04 21:27:29 +01:00
MacRimi e1a11053a6 Update flask_server.py 2025-11-04 21:16:16 +01:00
MacRimi f0a62191ea Updae AppImage 2025-11-04 21:02:56 +01:00
MacRimi a8311923fb Update AppImage 2025-11-04 19:58:09 +01:00
MacRimi cd1d88760d Update proxmox-dashboard.tsx 2025-11-04 19:39:50 +01:00
MacRimi 004949d3a0 Update proxmox-dashboard.tsx 2025-11-04 19:21:31 +01:00
MacRimi f6d26042da Update AppImage 2025-11-04 19:13:47 +01:00
MacRimi 270a73a470 Update auth-setup.tsx 2025-11-04 18:48:27 +01:00
MacRimi 018e80e59d Update AppImage 2025-11-04 18:45:54 +01:00
MacRimi cb5cb1e594 Create checkbox.tsx 2025-11-04 18:11:42 +01:00
MacRimi 6c5eb156a1 Update AppImage 2025-11-04 18:07:13 +01:00
MacRimi 8abef33840 Update build_appimage.sh 2025-11-04 17:37:32 +01:00
MacRimi 1d6b8951e8 Update hardware.tsx 2025-11-04 15:28:27 +01:00
MacRimi 711d57d91f Update flask_server.py 2025-11-04 15:09:23 +01:00
MacRimi 65fd847251 Update flask_server.py 2025-11-04 14:24:34 +01:00
MacRimi 73a170a5f1 Update hardware.tsx 2025-11-04 14:00:01 +01:00
MacRimi 9a32d1c0f7 Update flask_server.py 2025-11-04 13:47:00 +01:00
MacRimi 59918032c6 Update hardware.tsx 2025-11-04 13:18:39 +01:00
MacRimi 55394cbf09 Update AppImage 2025-11-04 12:47:26 +01:00
MacRimi 83dcc0c4f2 Update storage-overview.tsx 2025-11-04 12:17:32 +01:00
MacRimi b4b93f0572 Update AppImage 2025-11-04 12:11:08 +01:00
MacRimi ab0e59215c Aupdate version ProxMenux Monitor 2025-11-04 11:34:46 +01:00
MacRimi 5669ce207c Update flask_server.py 2025-11-04 11:03:09 +01:00
MacRimi 37f6cd96a4 Update flask_server.py 2025-11-04 09:46:11 +01:00
MacRimi c0ec74fb12 Update AppImage 2025-11-04 09:14:29 +01:00
cod378 226dc45190 Merge branch 'MacRimi:main' into main 2025-11-03 21:24:30 -03:00
MacRimi 11e3f53a2f Update AppImage 2025-11-03 23:26:04 +01:00
MacRimi 31d7f7e3e9 Update appImage 2025-11-03 23:17:27 +01:00
MacRimi 128edc08e2 Update flask_server.py 2025-11-03 23:13:24 +01:00
MacRimi 5158c5f359 Update flask_server.py 2025-11-03 19:12:07 +01:00
MacRimi a70b33ce13 Update AppImage 2025-11-03 19:02:41 +01:00
MacRimi d787c3caa0 Update AppImage 2025-11-03 18:35:16 +01:00
MacRimi a554af939e Create build-appimage.yml 2025-11-03 18:24:43 +01:00
MacRimi 06604ff0d1 Add manual build workflow for AppImage 2025-11-03 18:15:17 +01:00
MacRimi 9490f79c6d Update build-appimage.yml 2025-11-03 18:14:13 +01:00
MacRimi 311a624698 Update customizable_post_install.sh 2025-11-03 18:08:28 +01:00
cod378 3e2e77f9fb Merge branch 'MacRimi:main' into main 2025-11-03 10:15:41 -03:00
cod378 b2e02cd0e7 refactor: switch from remote URL to local script execution 2025-11-03 12:32:19 +00:00
ProxMenuxBot 87ead71766 Update helpers_cache.json 2025-11-03 12:28:15 +00:00
cod378 b8517a5b3e Merge branch 'ProxMenux-Offline' 2025-11-03 03:54:10 +00:00
cod378 c29cdf44fb refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 02:12:25 +00:00
cod378 4b2ab2894a refactor: switch from remote URLs to local script execution 2025-11-03 02:11:15 +00:00
cod378 c9a01ab5ad refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 02:10:46 +00:00
cod378 90d1046312 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 02:10:18 +00:00
cod378 14e749a18d refactor: switch from remote URLs to local script execution 2025-11-03 02:09:56 +00:00
cod378 a4be1af0ef refactor: switch from remote URLs to local script execution 2025-11-03 02:08:36 +00:00
cod378 f4185d0a2a refactor: switch from remote URLs to local script execution 2025-11-03 02:07:20 +00:00
cod378 ffb8324b5a refactor: switch from remote URLs to local script execution 2025-11-03 02:06:43 +00:00
cod378 6df44f1632 refactor: switch from remote URLs to local script execution 2025-11-03 02:03:26 +00:00
cod378 9570819f59 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:56:34 +00:00
cod378 f2afc94ed2 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:56:23 +00:00
cod378 050b95946c refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:55:45 +00:00
cod378 e33ef92334 refactor: switch from remote URLs to local script execution 2025-11-03 01:54:52 +00:00
cod378 0d7ff46aec refactor: switch from remote URLs to local script execution 2025-11-03 01:53:10 +00:00
cod378 042913e080 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:57 +00:00
cod378 98bc8be642 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:43 +00:00
cod378 2c6d2f4255 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:24 +00:00
cod378 6293556837 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:51:06 +00:00
cod378 641721d199 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:48:35 +00:00
cod378 036a2b9014 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:47:52 +00:00
cod378 9ad092d340 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:47:17 +00:00
cod378 d24884f651 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:46:50 +00:00
cod378 fa1c498716 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:46:19 +00:00
cod378 25635239d4 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:45:43 +00:00
cod378 c816688de3 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:44:48 +00:00
cod378 43d79bd1e9 refactor: switch from remote URLs to local script execution (unused path) 2025-11-03 01:44:06 +00:00
cod378 ba88c7b0f6 refactor: switch from remote URLs to local script execution 2025-11-03 01:41:39 +00:00
cod378 4359d92ffe refactor: switch from remote URLs to local script execution 2025-11-03 01:40:38 +00:00
cod378 16c7513e82 refactor: switch from remote URLs to local script execution 2025-11-03 01:39:15 +00:00
cod378 4572478ad8 refactor: switch from remote URLs to local script execution 2025-11-03 01:38:05 +00:00
cod378 02b5cd61bd refactor: switch from remote URLs to local script execution 2025-11-03 01:36:57 +00:00
cod378 bff07311b2 refactor: switch from remote URLs to local script execution 2025-11-03 01:34:44 +00:00
cod378 44cc89b9d5 refactor: switch from remote URLs to local file paths 2025-11-03 01:33:04 +00:00
cod378 fa1e6c6c64 refactor: switch from remote URLs to local script execution 2025-11-03 01:27:37 +00:00
cod378 4ebbdb284b refactor: switch from remote URLs to local script execution 2025-11-03 01:26:28 +00:00
cod378 51302a7c5a refactor: switch from remote URLs to local script execution 2025-11-03 01:25:19 +00:00
cod378 ba984592ed refactor: replace remote script fetching with local file execution 2025-11-03 01:22:24 +00:00
cod378 60a97e5815 feat: replace remote script fetching with local file execution 2025-11-03 01:20:11 +00:00
cod378 3275a1ecb4 refactor: replace remote script loading with local paths 2025-11-03 01:17:21 +00:00
cod378 af72c7a2d3 refactor: switch from remote URLs to local script paths 2025-11-03 01:16:32 +00:00
cod378 c07ada1fc4 refactor: switch from remote to local script execution 2025-11-03 01:14:54 +00:00
cod378 c19c8f9c5d refactor: switch network menu from remote URL to local scripts 2025-11-03 01:10:44 +00:00
cod378 43fe7ae7db refactor: replace remote script fetching with local file execution 2025-11-03 01:09:12 +00:00
cod378 22916868df feat: switch script paths from remote repo to local directory 2025-11-03 01:06:04 +00:00
cod378 7d00ff8869 feat: switch menu scripts from remote URLs to local paths 2025-11-03 01:03:39 +00:00
cod378 4ea2088485 refactor: replace remote script loading with local file execution
- Changed script loading from curl-based remote fetching to local file execution for improved security and reliability
- Removed dependency on external repository access for core menu functionality
- Fixed missing semicolon in case statement default branch
2025-11-03 00:59:48 +00:00
cod378 e421b40093 refactor: switch from remote scripts to local execution 2025-11-03 00:50:18 +00:00
cod378 a9dd7562ac feat: switch from remote URLs to local script paths
- Changed script sourcing from GitHub URLs to local filesystem paths for improved reliability
- Added error handling for missing script files with descriptive messages
- Removed redundant utility file sourcing and consolidated into single conditional block
- Updated script execution to use direct paths instead of curl commands
- Removed unused start_vm_configuration function that was duplicated elsewhere
2025-11-03 00:46:23 +00:00
cod378 8f62ed67d3 refactor: switch from remote URL to local script paths 2025-11-03 00:22:59 +00:00
cod378 cfd89a14f7 refactor: update script paths to use local resources 2025-11-03 00:16:28 +00:00
cod378 55011842f5 refactor: update script paths to use local resources 2025-11-03 00:15:42 +00:00
cod378 3079a3f51c refactor: update script paths to use local resources 2025-11-03 00:15:08 +00:00
cod378 c4ec390ca0 refactor: update scripts paths to use local references 2025-11-03 00:13:19 +00:00
cod378 f99b7f3589 refactor: switch script to use local paths instead of remote URLs 2025-11-03 00:07:34 +00:00
cod378 887b170c0e refactor: switch script to use local paths instead of remote URLs 2025-11-03 00:06:12 +00:00
cod378 c696cfd8d8 refactor: switch update script to use local file paths 2025-11-03 00:04:47 +00:00
cod378 25966973a2 refactor: update script paths to use local resources 2025-11-03 00:03:21 +00:00
cod378 d0a57d4b7c refactor: update script paths to use local resources 2025-11-03 00:01:36 +00:00
cod378 9341b49fd1 refactor: update script paths to use local references 2025-11-03 00:00:52 +00:00
cod378 bbc3c922a6 refactor: switch from remote repo to local script paths 2025-11-02 23:58:48 +00:00
cod378 17b8d63e6c refactor: switch from remote URLs to local script paths 2025-11-02 23:55:37 +00:00
cod378 c751a8168a refactor: switch backup script from remote URL to local file paths 2025-11-02 23:36:13 +00:00
cod378 f2509dbe5d refactor: switch from remote to local script loading. 2025-11-02 23:26:45 +00:00
cod378 6d44c22982 refactor: switch from remote to local script loading. 2025-11-02 23:25:04 +00:00
cod378 4bed489610 refactor: switch backup scripts from remote URL to local paths 2025-11-02 23:08:34 +00:00
cod378 8edf488636 refactor: update script paths to use local references 2025-11-02 03:32:41 +00:00
cod378 8fe7d249f8 refactor: update script paths to use local resources 2025-11-02 03:32:02 +00:00
cod378 6ed14e1d3c feat: switch from remote to local script loading 2025-11-02 03:29:56 +00:00
cod378 a5459acdaf feat: switch help menu from remote to local script loading 2025-11-02 03:28:14 +00:00
cod378 61cd198d35 feat: switch disk passthrough script to use local scripts 2025-11-02 03:27:18 +00:00
cod378 49ea2b304d feat: switch disk passthrough script to use local scripts 2025-11-02 03:26:27 +00:00
cod378 27231d1764 feat: switch iGPU configuration to use local scripts 2025-11-02 03:25:41 +00:00
cod378 8744620220 feat: improve log2ram installation and system checks
- Enhanced log2ram size calculation to support both MB and GB configurations
- Updated RAM detection to use MB-level precision before converting to GB
- Fixed typos in status messages ("successfull" → "successful")
- Switched from remote repo URL to local scripts directory for better reliability
- Added registration of LVM repair tool after successful header checks
- Improved log2ram monitoring script to properly handle different size units (M
2025-11-02 03:24:26 +00:00
cod378 4590be6d42 fix: replace remote script loading with local file execution 2025-11-02 03:22:54 +00:00
cod378 fa93b43c32 feat: switch telegram notifier to use local scripts
- Changed script source from GitHub repository to local directory (/usr/local/share/proxmenux/scripts)
- Updated path configuration to ensure consistent local file access
- Removed dependency on external repository for improved reliability and security
2025-11-02 02:59:33 +00:00
cod378 3c47f84a24 refactor: switch from remote repo to local scripts path
- Changed repository URL reference from GitHub to local scripts directory (/usr/local/share/proxmenux/scripts)
- Fixed spacing in info2 message formatting by adding space after HOLD variable
- Simplified script dependencies to use local installation instead of remote fetching
2025-11-02 02:55:19 +00:00
cod378 8a371c26de refactor: switch from remote to local script execution
- Changed script loading from remote URL to local directory path for offline usage
- Updated REPO_URL to LOCAL_SCRIPTS path (/usr/local/share/proxmenux/scripts)
- Disabled check_updates function since it's not applicable for local version
- Added comments explaining update functionality will be handled via .deb package in future
2025-11-02 02:51:24 +00:00
ProxMenuxBot 088a594468 Update helpers_cache.json 2025-11-02 01:06:57 +00:00
MacRimi c551913551 Update uninstall-tools.sh 2025-11-02 00:53:04 +01:00
code78 05e81053e0 feat: switch to local file installation and improve monitor setup
- Replaced remote file downloads with local file copying for more reliable installation
- Added proper cleanup of existing monitor service before reinstallation
- Enhanced error handling and logging for monitor service startup
- Improved SHA256 verification for monitor AppImage
- Added copying of install script and all utility scripts to base directory
- Updated progress messages to be more descriptive and accurate
- Increased monitor
2025-11-01 23:47:45 +00:00
MacRimi 981c0ab980 Update remove-banner-pve-v3.sh 2025-11-02 00:42:22 +01:00
MacRimi 1f083b335f Update remove banner v3 2025-11-02 00:37:59 +01:00
code78 10603900df update: Progress status 2025-11-01 23:33:25 +00:00
MacRimi c22e36d219 Update script PVE 9 2025-11-01 17:22:51 +01:00
MacRimi 26fc2ae9db Update install_proxmenux.sh 2025-11-01 17:09:38 +01:00
code78 2a0b298ae5 feat: add comprehensive project documentation and analysis
- Created detailed documentation covering ProxMenux project structure, installation flow, and core components
- Added in-depth analysis of script architecture, execution patterns, and key functionalities
- Documented system configuration, translation mechanism, and component interactions
- Included detailed breakdown of file organization, menu system, and installation processes
- Added technical specifications for ProxMenux Monitor web dashboar
2025-11-01 03:01:51 +00:00
ProxMenuxBot 96f0a9bc5d Update helpers_cache.json 2025-11-01 01:06:00 +00:00
MacRimi 5054e78864 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-11-01 00:11:05 +01:00
MacRimi f1fa6b03d5 change license CC-BY-NC-4.0 2025-11-01 00:10:46 +01:00
MacRimi 8f15bf9668 change license to CC-BY-NC-4.0 2025-10-31 23:48:46 +01:00
MacRimi 4b8e7b19a3 change licente to CC-BY-NC-4.0 2025-10-31 23:47:17 +01:00
MacRimi 67bba1dd09 Update version.txt 2025-10-31 23:38:45 +01:00
MacRimi b826dec79d Update CHANGELOG.md 2025-10-31 23:34:21 +01:00
MacRimi 62b200c5d9 Update CHANGELOG.md 2025-10-31 23:27:11 +01:00
MacRimi c2ed772f34 Update coral TPU pve9 2025-10-31 22:55:13 +01:00
MacRimi bbce6d4ad0 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-31 22:45:01 +01:00
MacRimi 45f6a0ec02 Update Install ProxMenux 2025-10-31 22:44:59 +01:00
github-actions[bot] 6b681278e0 Update AppImage build (2025-10-31 20:13:11) 2025-10-31 20:13:11 +00:00
MacRimi 2281ff06c7 Update GitHub Actions workflow permissions
Added permissions for write access to contents.
2025-10-31 21:10:47 +01:00
MacRimi 572a81fd4e Enhance workflow to include SHA256 checksum generation
Added steps to generate SHA256 checksum and upload AppImage.
2025-10-31 21:04:28 +01:00
MacRimi 5fd7df69fd Update update-pve9_2.sh 2025-10-31 20:47:46 +01:00
MacRimi 3401c6305e Update remove-banner-pve-v3.sh 2025-10-31 20:25:55 +01:00
MacRimi 269f9ac52c Update update-pve9_2.sh 2025-10-31 20:20:24 +01:00
MacRimi 4712171d43 Updsate Post Install 2025-10-31 20:16:42 +01:00
MacRimi 632c7b91f4 Update proxmox_update.sh 2025-10-31 20:09:52 +01:00
MacRimi f72dd79dff Update remove-banner-pve-v3.sh 2025-10-31 20:07:57 +01:00
MacRimi 4d109c0481 Update Post Install 2025-10-31 20:03:34 +01:00
MacRimi c046b77223 Update auto_post_install.sh 2025-10-31 18:44:30 +01:00
MacRimi 56ba3b5e5f Update auto_post_install.sh 2025-10-31 18:39:11 +01:00
MacRimi fa99247cb7 Update update-pve.sh 2025-10-31 18:35:49 +01:00
MacRimi 653fd37c08 Update update-pve.sh 2025-10-31 18:31:02 +01:00
MacRimi 9d053beafb Update Post Install menu 2025-10-31 18:28:11 +01:00
MacRimi f33c451a19 Update Post Install 2025-10-31 17:57:09 +01:00
MacRimi 9543148887 remove subscription banner V3 2025-10-31 17:49:28 +01:00
MacRimi 029ee4ed2f Update onboarding-carousel.tsx 2025-10-31 17:02:09 +01:00
MacRimi 688e826e9d Update onboarding-carousel.tsx 2025-10-30 23:30:58 +01:00
MacRimi deea0c54d4 Updarte AppImage 2025-10-30 23:14:00 +01:00
MacRimi 028f62aa9c Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-30 21:08:31 +01:00
MacRimi d3aef9c1d1 Update AppImage 2025-10-30 21:08:08 +01:00
ProxMenuxBot 72edce511a Update helpers_cache.json 2025-10-30 12:27:00 +00:00
MacRimi 37fade8f7a Update AppImage 2025-10-29 22:16:40 +01:00
MacRimi 0d2aa6738c Create ProxMenux-rc2.AppImage 2025-10-29 21:39:35 +01:00
MacRimi e3e0e5cba8 Create Heriberto.AppImage 2025-10-29 21:33:09 +01:00
MacRimi e40de189c3 Update flask_server.py 2025-10-29 21:28:23 +01:00
MacRimi c9c99f7b2a Create ProxMenux-rc.AppImage 2025-10-29 21:07:28 +01:00
MacRimi e6400918c8 Update AppImage 2025-10-29 20:57:06 +01:00
MacRimi d08557ad0e Update flask_server.py 2025-10-29 20:42:11 +01:00
MacRimi ba84dce7a7 Update build_appimage.sh 2025-10-29 20:29:43 +01:00
MacRimi 7044725bf1 Update AppImage 2025-10-29 20:27:39 +01:00
MacRimi 1812966fe6 Update AppImage 2025-10-29 20:22:55 +01:00
MacRimi a9bac25c9b Update proxmox-dashboard.tsx 2025-10-29 20:14:30 +01:00
MacRimi 5c967f11f0 Update AppImage 2025-10-29 20:03:17 +01:00
MacRimi 1b5f080495 Update system-logs.tsx 2025-10-29 19:45:32 +01:00
MacRimi 50a27fa3f6 Update virtual-machines.tsx 2025-10-29 19:22:53 +01:00
MacRimi f8ed53c1b9 Update virtual-machines.tsx 2025-10-29 19:04:01 +01:00
MacRimi 2163830a54 Update virtual-machines.tsx 2025-10-29 18:41:01 +01:00
MacRimi cae5e3b99f Update virtual-machines.tsx 2025-10-29 18:27:00 +01:00
MacRimi cc0a7941ea Update AppImage 2025-10-29 18:14:09 +01:00
MacRimi 18901c0e2d Update flask_server.py 2025-10-29 17:47:53 +01:00
ProxMenuxBot d4ea239185 Update helpers_cache.json 2025-10-29 12:29:04 +00:00
MacRimi 813c6aab13 Update AppImage 2025-10-28 23:18:32 +01:00
MacRimi f606e131a7 Update AppImage 2025-10-28 23:07:22 +01:00
MacRimi ccefa61b3d Update virtual-machines.tsx 2025-10-28 23:01:51 +01:00
MacRimi 606cae411f Update virtual-machines.tsx 2025-10-28 22:51:54 +01:00
MacRimi 901e4012cc Update AppImage 2025-10-28 22:45:15 +01:00
MacRimi d03b667194 Update AppImage 2025-10-28 22:12:57 +01:00
MacRimi d30954167e Update AppImage 2025-10-28 21:44:39 +01:00
MacRimi 0ee514ea15 Update flask_server.py 2025-10-28 19:59:37 +01:00
MacRimi 7e60792be8 Update flask_server.py 2025-10-28 19:38:56 +01:00
MacRimi 244a325394 Update flask_server.py 2025-10-28 19:07:08 +01:00
MacRimi 53df16a7ca Update flask_server.py 2025-10-28 18:48:33 +01:00
MacRimi 420576da09 Update flask_server.py 2025-10-28 18:45:31 +01:00
MacRimi d5a9d8ffdb Update flask_server.py 2025-10-28 18:40:11 +01:00
MacRimi 1873ad1a02 Update flask_server.py 2025-10-28 18:28:37 +01:00
MacRimi 9dec238f41 Update flask_server.py 2025-10-28 17:53:06 +01:00
MacRimi b93a018dc1 Update flask_server.py 2025-10-28 17:47:37 +01:00
ProxMenuxBot 6b1d5bf7db Update helpers_cache.json 2025-10-28 12:26:47 +00:00
ProxMenuxBot 4580866281 Update helpers_cache.json 2025-10-28 01:00:36 +00:00
ProxMenuxBot 5b743772ac Update helpers_cache.json 2025-10-27 18:19:45 +00:00
MacRimi 0ecf08e8e6 Update flask_server.py 2025-10-27 00:18:23 +01:00
MacRimi 7ca53d30b2 Update flask_server.py 2025-10-27 00:15:26 +01:00
MacRimi 18a0ba1981 Update flask_server.py 2025-10-27 00:03:26 +01:00
MacRimi 11a35ed589 Update flask_server.py 2025-10-26 23:58:02 +01:00
MacRimi eb5aa12d7f Update flask_server.py 2025-10-26 23:33:17 +01:00
MacRimi 1065b67073 Update flask_server.py 2025-10-26 23:17:18 +01:00
MacRimi 0d7b278003 Update flask_server.py 2025-10-26 22:58:56 +01:00
MacRimi 05093a9d49 Update AppImage 2025-10-26 22:53:13 +01:00
MacRimi 87f9b2b72c Update AppImage 2025-10-26 22:30:14 +01:00
MacRimi 2d4833d199 Update AppImage 2025-10-26 22:11:02 +01:00
MacRimi 610e08e690 Update AppImage 2025-10-26 21:42:12 +01:00
MacRimi 1192224c15 Update proxmox-dashboard.tsx 2025-10-26 21:30:48 +01:00
MacRimi 4c3a9928e7 Update metrics-dialog.tsx 2025-10-26 21:11:55 +01:00
MacRimi 433a4359e6 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-26 21:00:02 +01:00
MacRimi f1854b5120 Update AppImage 2025-10-26 20:59:59 +01:00
ProxMenuxBot 6961b0c2f5 Update helpers_cache.json 2025-10-26 18:17:36 +00:00
MacRimi 8b26f30e37 Create ProxMenux-beta7.AppImage 2025-10-26 18:28:54 +01:00
MacRimi 071724949f Update AppImage 2025-10-26 18:03:09 +01:00
MacRimi 28898aa1db Update network-metrics.tsx 2025-10-26 17:39:19 +01:00
MacRimi 11fae19e33 Update AppImage 2025-10-26 16:23:46 +01:00
MacRimi 13b9dd0262 Update network-metrics.tsx 2025-10-26 16:17:23 +01:00
MacRimi b47520c938 Update flask_server.py 2025-10-26 15:39:52 +01:00
MacRimi f6869b9e1c Update AppImage 2025-10-26 15:36:53 +01:00
MacRimi 96046a5d1f Update AppImage 2025-10-26 15:18:54 +01:00
MacRimi 524d0b278b Update network-metrics.tsx 2025-10-26 14:44:41 +01:00
MacRimi 6e2348eb06 Update AppImage 2025-10-26 14:31:10 +01:00
MacRimi e4b57e6ca3 Updae AppImage 2025-10-26 14:28:35 +01:00
MacRimi 9640e558cd Update AppImage 2025-10-26 14:25:23 +01:00
MacRimi 07b13d1374 Update network-traffic-chart.tsx 2025-10-26 14:17:22 +01:00
MacRimi 7e4389abd9 Update network-traffic-chart.tsx 2025-10-26 14:08:42 +01:00
MacRimi 0f424e7f0d Update network-traffic-chart.tsx 2025-10-26 13:31:14 +01:00
MacRimi 455e5735ff Update network-traffic-chart.tsx 2025-10-26 12:44:18 +01:00
MacRimi 6577d2ae3c Update AppImage 2025-10-26 12:32:40 +01:00
MacRimi 56ed543dfb Update AppImage 2025-10-26 12:17:22 +01:00
MacRimi 5549e3a398 Update network-metrics.tsx 2025-10-26 12:00:42 +01:00
MacRimi 2ff7c111af Update network-metrics.tsx 2025-10-26 11:49:13 +01:00
MacRimi a7af072ca7 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-26 11:41:20 +01:00
MacRimi a4a455f31e Update network-metrics.tsx 2025-10-26 11:41:18 +01:00
ProxMenuxBot 99d55b4314 Update helpers_cache.json 2025-10-26 01:05:33 +00:00
MacRimi 811e2155a6 Update flask_server.py 2025-10-26 01:46:12 +02:00
MacRimi ab7a49351d Update network-metrics.tsx 2025-10-26 00:34:05 +02:00
MacRimi efdfba0575 Update AppImage 2025-10-26 00:21:33 +02:00
MacRimi af9b5f6ca4 Update network-metrics.tsx 2025-10-25 23:44:54 +02:00
MacRimi 65144b9a3d Update AppImage 2025-10-25 23:39:23 +02:00
MacRimi 621f57d702 Update AppImage 2025-10-25 23:33:18 +02:00
MacRimi a0444fbeee Update node-metrics-charts.tsx 2025-10-25 22:57:21 +02:00
MacRimi cff81eea14 Update AppImage 2025-10-25 22:43:17 +02:00
MacRimi 5738c90721 Update network-metrics.tsx 2025-10-25 22:31:13 +02:00
MacRimi a229231c0c Update AppImage 2025-10-25 22:10:08 +02:00
MacRimi 6bf5bd97b5 Update network-metrics.tsx 2025-10-25 21:47:04 +02:00
MacRimi 35c50a7c60 Update network-metrics.tsx 2025-10-25 21:36:46 +02:00
MacRimi 042e6584eb Update network-metrics.tsx 2025-10-25 21:19:43 +02:00
MacRimi bcce1b7ea8 Update network-traffic-chart.tsx 2025-10-25 21:04:21 +02:00
MacRimi 73181f9e33 Update network-metrics.tsx 2025-10-25 20:55:24 +02:00
MacRimi b0a7b6c7cd Update AppImage 2025-10-25 18:47:24 +02:00
MacRimi 09744818dc Update AppImage 2025-10-25 18:09:48 +02:00
MacRimi f93b3109b9 Update uninstall-tools.sh 2025-10-25 17:45:57 +02:00
MacRimi 48d4836f0a Update auto_post_install.sh 2025-10-25 17:33:05 +02:00
MacRimi 5d4f70e943 Update auto_post_install.sh 2025-10-25 17:22:52 +02:00
ProxMenuxBot 9e05197a9a Update helpers_cache.json 2025-10-25 12:22:51 +00:00
MacRimi 11671e884d Update auto_post_install.sh 2025-10-25 11:44:00 +02:00
MacRimi dcce818678 Update post Install 2025-10-25 11:28:07 +02:00
MacRimi f6c23bc9a0 Update virtual-machines.tsx 2025-10-24 23:35:08 +02:00
MacRimi 15eca895fb Update virtual-machines.tsx 2025-10-24 23:28:10 +02:00
MacRimi d5d5dd7855 Update hardware.tsx 2025-10-24 23:15:53 +02:00
MacRimi c79f5fd8a5 Update hardware.tsx 2025-10-24 23:11:24 +02:00
MacRimi 409e40f3b7 Update hardware.tsx 2025-10-24 23:04:03 +02:00
MacRimi 67a83cb164 Update hardware.tsx 2025-10-24 22:57:06 +02:00
MacRimi 908bdc7c86 Update hardware.tsx 2025-10-24 22:50:03 +02:00
MacRimi b1c2bd3d64 Update hardware.tsx 2025-10-24 22:42:08 +02:00
MacRimi ffd317aff0 Update hardware.tsx 2025-10-24 22:34:29 +02:00
MacRimi 84a10afea1 Update network-metrics.tsx 2025-10-24 22:00:16 +02:00
MacRimi 32036ef64d Update AppImage 2025-10-24 21:54:34 +02:00
MacRimi a8b8036311 Update network-metrics.tsx 2025-10-24 21:40:19 +02:00
MacRimi b813716f7c Update network-metrics.tsx 2025-10-24 21:24:57 +02:00
MacRimi 7bd6061a59 Update network-metrics.tsx 2025-10-24 21:06:45 +02:00
MacRimi 7682a6e708 Update AppImage 2025-10-24 20:55:45 +02:00
MacRimi fe9c592107 Update network-metrics.tsx 2025-10-24 20:31:56 +02:00
MacRimi 9ff24dc446 Update network-metrics.tsx 2025-10-24 20:22:24 +02:00
MacRimi 3036711fb4 Update network-metrics.tsx 2025-10-24 20:08:33 +02:00
MacRimi 53363f293b Update network-metrics.tsx 2025-10-24 19:58:04 +02:00
MacRimi e9791984ee Update AppImage 2025-10-24 19:30:10 +02:00
MacRimi ddca96a60e Update AppImage 2025-10-24 19:20:37 +02:00
MacRimi be3607dd4d Update AppImage 2025-10-24 18:56:39 +02:00
MacRimi 6000a7a60f Update proxmox-dashboard.tsx 2025-10-24 18:39:00 +02:00
MacRimi cc64c9f9d8 Update install_coral_pve9.sh 2025-10-24 18:23:09 +02:00
MacRimi 5c699d956c Delete 0001-fix-apex-group-and-udev-rules.patch 2025-10-24 18:17:46 +02:00
MacRimi e1757e5ac5 Update 0001-fix-apex-group-and-udev-rules.patch 2025-10-24 18:11:33 +02:00
MacRimi 31167234be Update 0001-fix-apex-group-and-udev-rules.patch 2025-10-24 18:05:27 +02:00
MacRimi 807638ca04 Update coral TPU 2025-10-24 18:00:26 +02:00
MacRimi ff6b78252c Create 0001-fix-apex-group-and-udev-rules.patch .sh 2025-10-24 17:58:37 +02:00
MacRimi 93bdcaab7f Update auto_post_install.sh 2025-10-24 17:26:32 +02:00
MacRimi d06c580bbc Update AppImage 2025-10-24 17:17:14 +02:00
MacRimi a5b32b356c Create ProxMenux-beta6.AppImage 2025-10-23 22:44:55 +02:00
MacRimi d117e666fd Update proxmox-dashboard.tsx 2025-10-23 22:40:00 +02:00
MacRimi 09da94b2ab Update proxmox-dashboard.tsx 2025-10-23 22:32:43 +02:00
MacRimi 9ebf5919a2 Update proxmox-dashboard.tsx 2025-10-23 22:25:59 +02:00
MacRimi 9c46452a4d Update proxmox-dashboard.tsx 2025-10-23 22:16:02 +02:00
MacRimi b34536491b Update proxmox-dashboard.tsx 2025-10-23 22:10:21 +02:00
MacRimi a7c1e240c1 Update proxmox-dashboard.tsx 2025-10-23 22:00:44 +02:00
MacRimi 44618d3d73 Update proxmox-dashboard.tsx 2025-10-23 21:54:22 +02:00
MacRimi 1f92af64f0 Update proxmox-dashboard.tsx 2025-10-23 21:41:55 +02:00
MacRimi 5bcd081e88 Update proxmox-dashboard.tsx 2025-10-23 21:36:02 +02:00
MacRimi b141622e75 Update proxmox-dashboard.tsx 2025-10-23 21:31:21 +02:00
MacRimi f96bdee71d Update proxmox-dashboard.tsx 2025-10-23 21:28:00 +02:00
MacRimi 0af70d3298 Update proxmox-dashboard.tsx 2025-10-23 21:20:56 +02:00
MacRimi e044c59627 Update proxmox-dashboard.tsx 2025-10-23 21:13:17 +02:00
MacRimi cce1c902e5 Update proxmox-dashboard.tsx 2025-10-23 21:09:01 +02:00
MacRimi d845474644 Update proxmox-dashboard.tsx 2025-10-23 21:02:56 +02:00
MacRimi cd67aba2ad Update proxmox-dashboard.tsx 2025-10-23 20:56:11 +02:00
MacRimi 1e47162357 Update proxmox-dashboard.tsx 2025-10-23 20:48:52 +02:00
MacRimi 230400846f Update proxmox-dashboard.tsx 2025-10-23 20:35:58 +02:00
MacRimi ebb29ad04b Update proxmox-dashboard.tsx 2025-10-23 20:29:39 +02:00
MacRimi 2cd603357d Update proxmox-dashboard.tsx 2025-10-23 20:22:12 +02:00
MacRimi bee26838e1 Update proxmox-dashboard.tsx 2025-10-23 20:12:56 +02:00
MacRimi 5b0572879d Update proxmox-dashboard.tsx 2025-10-23 20:04:56 +02:00
MacRimi 6e86275dce Update proxmox-dashboard.tsx 2025-10-23 19:57:11 +02:00
MacRimi 03a007b9b6 Update AppImage 2025-10-23 19:47:26 +02:00
MacRimi dadb215ce0 Update AppImage 2025-10-23 19:39:24 +02:00
MacRimi a1e3e12c6b Update globals.css 2025-10-23 19:34:30 +02:00
MacRimi 4274c817d3 Update AppImage 2025-10-23 19:29:19 +02:00
MacRimi 5abedc15dc Update proxmox-dashboard.tsx 2025-10-23 18:41:37 +02:00
MacRimi 947c9639e8 Update proxmox-dashboard.tsx 2025-10-23 18:31:44 +02:00
MacRimi c4cdf4a834 Update AppImage 2025-10-23 18:22:00 +02:00
MacRimi 44b9bfee68 Update system-overview.tsx 2025-10-23 17:56:56 +02:00
MacRimi ac9d43892b Update system-overview.tsx 2025-10-23 17:33:48 +02:00
MacRimi 9f62f8eff9 Update AppImage 2025-10-23 17:21:48 +02:00
MacRimi 13dd400795 Update hardware.tsx 2025-10-23 15:31:25 +02:00
MacRimi 37343a4114 Update hardware.tsx 2025-10-23 14:58:46 +02:00
MacRimi 3df6a4048a Update hardware.tsx 2025-10-23 14:38:17 +02:00
MacRimi c9fb87b571 Update ProxMenux-beta5.AppImage 2025-10-23 13:27:36 +02:00
MacRimi 44ca1d507d Update metrics-dialog.tsx 2025-10-23 12:55:31 +02:00
MacRimi 30b236548a Update metrics-dialog.tsx 2025-10-23 12:47:28 +02:00
MacRimi 21b7d1c3fb Update virtual-machines.tsx 2025-10-23 12:36:48 +02:00
MacRimi 8d5ea66ecc Update network-metrics.tsx 2025-10-23 12:25:17 +02:00
MacRimi b5ed10689d Update node-metrics-charts.tsx 2025-10-23 12:13:22 +02:00
MacRimi a55cdfd7fa Update node-metrics-charts.tsx 2025-10-23 11:56:51 +02:00
MacRimi 39a4c10ac9 Update node-metrics-charts.tsx 2025-10-23 11:38:03 +02:00
MacRimi c542cd4d7d Update node-metrics-charts.tsx 2025-10-23 11:00:36 +02:00
MacRimi 01de338a65 Update AppImage 2025-10-23 09:59:27 +02:00
MacRimi f23f7b1983 Updata AppImage 2025-10-23 09:32:46 +02:00
MacRimi a349ab62ec Update node-metrics-charts.tsx 2025-10-22 20:02:32 +02:00
MacRimi e620010f10 Update AppImage 2025-10-22 19:50:26 +02:00
MacRimi 70509355de Uodate AppImage 2025-10-22 19:38:04 +02:00
MacRimi f25654ead7 Updte AppImage 2025-10-22 19:25:04 +02:00
MacRimi f1741d4dac Update AppImage 2025-10-22 19:07:18 +02:00
MacRimi f3245d092b Update virtual-machines.tsx 2025-10-22 18:49:41 +02:00
MacRimi a039a8600e Update virtual-machines.tsx 2025-10-22 18:38:04 +02:00
MacRimi ee56f4a7a2 Update virtual-machines.tsx 2025-10-22 18:27:27 +02:00
MacRimi c40b6ca7f4 Update AppImage 2025-10-22 18:17:57 +02:00
MacRimi 2c0e1e498b Update globals.css 2025-10-22 18:10:05 +02:00
MacRimi 0262ea31eb Update AppImage 2025-10-22 18:05:33 +02:00
MacRimi 4b671d7fb0 Update virtual-machines.tsx 2025-10-22 17:50:36 +02:00
MacRimi d8f9419eb9 Update virtual-machines.tsx 2025-10-22 17:34:46 +02:00
MacRimi aa65bab486 Update flask_server.py 2025-10-22 17:19:42 +02:00
MacRimi 849c3967fd Update virtual-machines.tsx 2025-10-22 17:03:27 +02:00
MacRimi 83562cf7d8 Update virtual-machines.tsx 2025-10-22 16:49:15 +02:00
MacRimi 2631a44410 Update AppImage 2025-10-22 16:15:49 +02:00
MacRimi ddfea43b79 Update virtual-machines.tsx 2025-10-22 12:50:53 +02:00
MacRimi 68f19ffa5f Update virtual-machines.tsx 2025-10-22 12:37:52 +02:00
MacRimi 8800d42c32 Update virtual-machines.tsx 2025-10-22 12:19:03 +02:00
MacRimi 416a8a7cb2 Update virtual-machines.tsx 2025-10-22 12:03:51 +02:00
MacRimi 7088f249a5 Update virtual-machines.tsx 2025-10-22 11:45:06 +02:00
MacRimi a2301cc980 Update virtual-machines.tsx 2025-10-22 11:24:56 +02:00
MacRimi af545404e8 Update virtual-machines.tsx 2025-10-22 11:09:46 +02:00
MacRimi 2e96f19476 Update virtual-machines.tsx 2025-10-22 10:59:20 +02:00
MacRimi e3f26b7f75 Update virtual-machines.tsx 2025-10-22 10:27:14 +02:00
MacRimi f45c98a6a7 Update AppImage 2025-10-22 09:28:46 +02:00
ProxMenuxBot a565a3c909 Update helpers_cache.json 2025-10-22 01:02:42 +00:00
MacRimi b5e3dd6c06 Update AppImage 2025-10-21 20:47:04 +02:00
MacRimi 928f592d9c Update virtual-machines.tsx 2025-10-21 20:31:15 +02:00
MacRimi 8ee8edcd36 Update AppImage 2025-10-21 20:12:00 +02:00
MacRimi 1e128348e5 Update AppImage 2025-10-21 19:50:17 +02:00
MacRimi 6ef5655b7d Update virtual-machines.tsx 2025-10-21 19:16:44 +02:00
MacRimi f6ba5329ce Update AppImage 2025-10-21 19:05:38 +02:00
MacRimi 93cef0d580 Update update-pve.sh 2025-10-21 18:56:33 +02:00
MacRimi 797b088cc8 aupdate AppImage 2025-10-21 18:43:56 +02:00
MacRimi 6d23d3510f Update AppImage 2025-10-21 18:04:35 +02:00
MacRimi f2d7d0af43 Update AppImage 2025-10-21 17:57:05 +02:00
MacRimi e55c0461db Update virtual-machines.tsx 2025-10-21 17:45:12 +02:00
MacRimi 8eca511a53 Uodate AppImage 2025-10-21 17:33:53 +02:00
MacRimi f20e46dee0 Update AppImage 2025-10-21 17:20:16 +02:00
MacRimi b79f22f4fe Add log directories for pveproxy with permissions
Create directories for pveproxy logs and set permissions
2025-10-21 14:26:37 +02:00
MacRimi 3287dc77e2 Update auto_post_install.sh 2025-10-21 14:24:41 +02:00
MacRimi 78a08b35e7 Update auto_post_install.sh 2025-10-21 14:06:10 +02:00
MacRimi e86196999a Update auto_post_install.sh 2025-10-21 13:57:32 +02:00
MacRimi 4d50339041 Update journald configuration in auto_post_install.sh 2025-10-21 13:56:37 +02:00
MacRimi edc5a2c0f2 Enhance Log2RAM installation script
Refactor Log2RAM installation and configuration script to improve error handling, cleanup previous installations, and adjust systemd-journald limits based on Log2RAM size.
2025-10-21 09:29:58 +02:00
MacRimi 598b88b1f0 Update virtual-machines.tsx 2025-10-20 23:48:07 +02:00
MacRimi c22b9f8ff5 Update AppImage 2025-10-20 23:30:18 +02:00
MacRimi 3c654ab495 Update virtual-machines.tsx 2025-10-20 23:24:38 +02:00
MacRimi 2f0fabea7a Update virtual-machines.tsx 2025-10-20 23:16:56 +02:00
MacRimi 099b14efc3 Update virtual-machines.tsx 2025-10-20 23:02:52 +02:00
MacRimi ee42dee366 Update virtual-machines.tsx 2025-10-20 22:40:37 +02:00
MacRimi deae081cb3 Update AppImage 2025-10-20 22:15:08 +02:00
MacRimi 6479f14d3d Update AppImage 2025-10-20 20:56:54 +02:00
MacRimi 60707c3868 Update create VM 2025-10-20 20:22:31 +02:00
MacRimi e78d8e1ae6 Update metrics-dialog.tsx 2025-10-20 20:17:46 +02:00
MacRimi fee0d0aed9 Update metrics-dialog.tsx 2025-10-20 20:00:29 +02:00
MacRimi 178abc77ce Update metrics-dialog.tsx 2025-10-20 19:40:59 +02:00
MacRimi 7001f97d96 Update vm_creator.sh 2025-10-20 19:20:50 +02:00
MacRimi 55432e61ff Update metrics-dialog.tsx 2025-10-20 19:00:00 +02:00
MacRimi 4ee993ef3b Update metrics-dialog.tsx 2025-10-20 18:39:12 +02:00
MacRimi 64d471bb9b Update metrics-dialog.tsx 2025-10-20 17:30:39 +02:00
MacRimi 4dfcdcb0b2 Update metrics-dialog.tsx 2025-10-20 17:11:30 +02:00
MacRimi 8e69a84e7a Update metrics-dialog.tsx 2025-10-20 16:52:16 +02:00
MacRimi 8ec643d882 Update metrics-dialog.tsx 2025-10-20 16:43:07 +02:00
ProxMenuxBot f4d6192c80 Update helpers_cache.json 2025-10-20 12:28:47 +00:00
MacRimi c0aa2b85fc Update metrics-dialog.tsx 2025-10-19 17:53:39 +02:00
MacRimi 61a376fb6d Update virtual-machines.tsx 2025-10-19 17:46:47 +02:00
MacRimi 8786cb5180 Update AppImage 2025-10-19 17:40:13 +02:00
MacRimi c305ef1360 Update AppImage 2025-10-19 17:29:23 +02:00
MacRimi f662ce0b7a Update AppImage 2025-10-19 17:16:35 +02:00
MacRimi 71af9345a5 Update AppImage 2025-10-19 16:51:52 +02:00
MacRimi a819a19c77 Update virtual-machines.tsx 2025-10-19 16:19:47 +02:00
MacRimi c65fad06b7 Update virtual-machines.tsx 2025-10-19 16:06:19 +02:00
MacRimi 9e6e1931b1 Update customizable_post_install.sh 2025-10-19 09:48:38 +02:00
MacRimi 3b22273f5a Update virtual-machines.tsx 2025-10-18 18:57:14 +02:00
MacRimi fc5ff1782b Update virtual-machines.tsx 2025-10-18 18:48:01 +02:00
MacRimi 4d3b3d984d Update virtual-machines.tsx 2025-10-18 18:37:22 +02:00
MacRimi 514976561f Update hardware.tsx 2025-10-18 18:32:13 +02:00
MacRimi fb4998d21b Update auto_post_install.sh 2025-10-18 18:24:05 +02:00
MacRimi 0feec978d3 Update system-logs.tsx 2025-10-18 18:18:57 +02:00
MacRimi 5f1c39aba5 Update system-logs.tsx 2025-10-18 18:04:05 +02:00
MacRimi 17973619de Update system-logs.tsx 2025-10-18 17:54:06 +02:00
MacRimi 478d7a2d2d Update system-logs.tsx 2025-10-18 17:45:13 +02:00
MacRimi f2af0be1e1 Update system-logs.tsx 2025-10-18 17:36:08 +02:00
MacRimi d0725f5098 Update system-logs.tsx 2025-10-18 17:18:41 +02:00
MacRimi 646d614d94 Update update-pve.sh 2025-10-18 17:12:11 +02:00
MacRimi 4c337ef5e9 Update system-logs.tsx 2025-10-18 17:02:16 +02:00
MacRimi 2b633b8566 Update system-logs.tsx 2025-10-18 16:47:04 +02:00
MacRimi 6b16454217 Update system-logs.tsx 2025-10-18 16:36:31 +02:00
MacRimi b7086deeac Update zimaos.sh 2025-10-18 16:16:48 +02:00
MacRimi f021afb6a4 Update AppImage and ZimaOS 2025-10-18 16:11:28 +02:00
ProxMenuxBot 99622bd3d6 Update helpers_cache.json 2025-10-18 12:23:38 +00:00
MacRimi 50a76519ea Update system-logs.tsx 2025-10-18 12:39:26 +02:00
MacRimi 1e806054ab Update system-logs.tsx 2025-10-18 12:26:27 +02:00
MacRimi 89d7f335fc Update system-logs.tsx 2025-10-18 11:58:01 +02:00
MacRimi 7a664ec4ec Update system-logs.tsx 2025-10-18 11:41:49 +02:00
MacRimi a8a4d029f8 Update system-logs.tsx 2025-10-18 11:35:14 +02:00
MacRimi 501b5dce76 Update system-logs.tsx 2025-10-18 11:25:16 +02:00
MacRimi e9b3504370 Update system-logs.tsx 2025-10-18 11:13:01 +02:00
MacRimi 7b20c78e73 Update system-logs.tsx 2025-10-18 11:05:27 +02:00
MacRimi a343ce69aa Update system-logs.tsx 2025-10-18 10:58:08 +02:00
MacRimi d52ce400fb Update AppImage 2025-10-18 10:43:58 +02:00
MacRimi 0ee574eaaa Update system-logs.tsx 2025-10-18 10:34:34 +02:00
MacRimi 74c4392b6d Update AppImage 2025-10-18 10:28:04 +02:00
MacRimi d844c330e9 Update system-logs.tsx 2025-10-18 10:11:43 +02:00
MacRimi b50cb78fa6 Update system-logs.tsx 2025-10-18 09:56:53 +02:00
MacRimi 26c138f42c Update AppImage 2025-10-17 20:32:17 +02:00
MacRimi 0ec7e65926 Update proxmox-dashboard.tsx 2025-10-17 20:27:02 +02:00
MacRimi 3588cc4c03 Update proxmox-dashboard.tsx 2025-10-17 20:19:37 +02:00
MacRimi da8c7749c8 Update proxmox-dashboard.tsx 2025-10-17 20:09:26 +02:00
MacRimi 79fe999e77 Update proxmox-dashboard.tsx 2025-10-17 20:01:10 +02:00
MacRimi 439c65ad6d Update AppImage 2025-10-17 19:51:41 +02:00
MacRimi 81b3aa5ac1 Update system-logs.tsx 2025-10-17 19:44:19 +02:00
MacRimi c4beb9ae4d Update hardware.tsx 2025-10-17 19:34:35 +02:00
MacRimi 9d286d8378 Update network-metrics.tsx 2025-10-17 19:24:14 +02:00
MacRimi 19e7a43fe3 Update network-metrics.tsx 2025-10-17 19:16:55 +02:00
MacRimi ab59e2deac Update AppImage 2025-10-17 19:06:15 +02:00
MacRimi 043f22e6ec Update AppImage 2025-10-17 18:52:18 +02:00
MacRimi 4abb6af31e Update AppImagen 2025-10-17 18:30:18 +02:00
MacRimi a17ba4a81f Update virtual-machines.tsx 2025-10-17 18:15:46 +02:00
MacRimi bc8a6847e3 Update virtual-machines.tsx 2025-10-17 18:10:12 +02:00
MacRimi 18f97f9df2 Update AppImage 2025-10-17 18:03:01 +02:00
MacRimi 4a204d8d89 Update AppImage 2025-10-17 17:53:06 +02:00
MacRimi 062c6c2364 Update AppImage 2025-10-17 17:38:13 +02:00
MacRimi 477716ef67 Update virtual-machines.tsx 2025-10-17 17:22:10 +02:00
MacRimi ef973df7c9 Update flask_server.py 2025-10-17 17:04:55 +02:00
MacRimi c40bb6a4d5 Create rafa.AppImage 2025-10-16 21:29:45 +02:00
MacRimi 20e942dccd Update storage-overview.tsx 2025-10-16 21:19:03 +02:00
MacRimi 598cbc4d11 Update AppImage 2025-10-16 19:57:55 +02:00
MacRimi 70a3d5af07 Update flask_server.py 2025-10-16 19:34:45 +02:00
MacRimi 9cabb1afbd Update hardware.tsx 2025-10-16 19:23:41 +02:00
MacRimi 4267224f59 Create ProxMenux-beta5.AppImage 2025-10-16 09:41:04 +02:00
MacRimi 02d910f53c Update flask_server.py 2025-10-16 09:31:35 +02:00
MacRimi e81b7b5b9f Update AppImage 2025-10-16 09:07:39 +02:00
ProxMenuxBot 17e0a8eec1 Update helpers_cache.json 2025-10-16 01:00:35 +00:00
MacRimi eb322c9d41 Create ProxMenux-beta4.AppImage 2025-10-15 20:51:42 +02:00
MacRimi 751af92c21 Update proxmox-dashboard.tsx 2025-10-15 20:42:51 +02:00
MacRimi fb4962a41a Update proxmox-dashboard.tsx 2025-10-15 20:35:10 +02:00
MacRimi daf6598599 Update AppImage 2025-10-15 20:27:30 +02:00
MacRimi e1e4f71f3a Update virtual-machines.tsx 2025-10-15 20:13:01 +02:00
MacRimi a2fa7ec9c4 Update virtual-machines.tsx 2025-10-15 19:51:24 +02:00
MacRimi 5cd37b74b4 Update flask_server.py 2025-10-15 19:22:16 +02:00
MacRimi beed7e83f2 Update storage-overview.tsx 2025-10-15 19:06:33 +02:00
MacRimi a7726edca6 Update storage-overview.tsx 2025-10-15 18:56:02 +02:00
MacRimi eed0c21c41 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-15 18:41:21 +02:00
MacRimi 094a43157e Update storage-overview.tsx 2025-10-15 18:41:03 +02:00
MacRimi 4033958bf5 Merge pull request #37 from MrCaringi/main
🌶️ Contributors rocks! 🌶️
2025-10-15 18:23:37 +02:00
MacRimi f18784ecc1 Update flask_server.py 2025-10-15 18:22:04 +02:00
JFC 418494b7f3 Merge pull request #1 from MrCaringi/MrCaringi-contributors
Update README.md
2025-10-15 10:20:16 -06:00
JFC 343753ee2a Update README.md 2025-10-15 10:18:43 -06:00
MacRimi 1ceda066c4 Update hardware.tsx 2025-10-15 18:08:49 +02:00
MacRimi 3c13dea55f Update hardware.tsx 2025-10-15 17:54:04 +02:00
MacRimi 6ced2cbf7c Update flask_server.py 2025-10-15 17:24:06 +02:00
MacRimi b41f16a736 Update storage-overview.tsx 2025-10-15 17:04:10 +02:00
MacRimi 42fa02a887 Create ProxMenux-beta3.AppImage 2025-10-15 00:09:51 +02:00
MacRimi 2ce87cdac0 Update AppImage 2025-10-15 00:03:56 +02:00
MacRimi e4864d3871 Update AppImage 2025-10-14 23:57:46 +02:00
MacRimi b6e0052013 Update storage-overview.tsx 2025-10-14 23:42:00 +02:00
MacRimi 9ef83a59c7 Update flask_server.py 2025-10-14 23:21:09 +02:00
MacRimi 325724ff85 Update flask_server.py 2025-10-14 23:16:56 +02:00
MacRimi 998bfa0656 Update flask_server.py 2025-10-14 22:47:13 +02:00
MacRimi e476df5e7d Update flask_server.py 2025-10-14 22:36:50 +02:00
MacRimi 83a3601cdb Update flask_server.py 2025-10-14 22:28:28 +02:00
MacRimi 996dcc4b23 Update AppImage 2025-10-14 22:14:48 +02:00
MacRimi 04304f8283 Update storage-overview.tsx 2025-10-14 21:31:15 +02:00
MacRimi 3418f73390 Update storage-overview.tsx 2025-10-14 21:22:21 +02:00
MacRimi b07b6c8960 Update storage-overview.tsx 2025-10-14 21:11:34 +02:00
MacRimi 4f2cf37d73 Update storage-overview.tsx 2025-10-14 20:54:41 +02:00
MacRimi 408e017f2f Update virtual-machines.tsx 2025-10-14 20:31:53 +02:00
MacRimi 62b42266e8 Update virtual-machines.tsx 2025-10-14 20:22:55 +02:00
MacRimi 80e9e23965 Update virtual-machines.tsx 2025-10-14 20:04:18 +02:00
MacRimi c73154aeb1 Update virtual-machines.tsx 2025-10-14 19:48:57 +02:00
MacRimi 66c4786ec2 Update virtual-machines.tsx 2025-10-14 19:35:25 +02:00
MacRimi 143f5a2085 Update hardware.tsx 2025-10-14 19:00:24 +02:00
MacRimi 699c7df798 Update AppImage 2025-10-14 18:51:39 +02:00
MacRimi d3de7b95aa Update flask_server.py 2025-10-14 18:09:48 +02:00
MacRimi 2dc6f76da9 Update hardware.tsx 2025-10-14 17:38:26 +02:00
MacRimi 63ccf6b553 Update hardware.tsx 2025-10-14 17:23:59 +02:00
MacRimi f49ffe3cb0 Update AppImage 2025-10-14 15:34:19 +02:00
MacRimi c1b578350d Update flask_server.py 2025-10-14 15:19:18 +02:00
MacRimi 48e4af41ae Update flask_server.py 2025-10-14 14:28:08 +02:00
MacRimi 5e915f9c40 Update flask_server.py 2025-10-14 13:56:37 +02:00
MacRimi 70b5f91f82 Update AppImage 2025-10-14 13:37:48 +02:00
MacRimi 792df08c78 Update hardware.tsx 2025-10-14 12:58:53 +02:00
MacRimi 032b5d3580 Update AppImage 2025-10-14 12:40:27 +02:00
MacRimi 8f93e43bb3 Update AppImage 2025-10-14 11:37:30 +02:00
MacRimi 04f95e648b Update AppImage 2025-10-14 11:08:47 +02:00
MacRimi 511b8eb407 Update storage-metrics.tsx 2025-10-14 10:47:30 +02:00
MacRimi a6e6dd255d Update storage-metrics.tsx 2025-10-14 10:25:18 +02:00
MacRimi c3c53d4056 Update storage-metrics.tsx 2025-10-14 09:33:03 +02:00
MacRimi 2f700d9a4c Update AppImage 2025-10-14 09:15:44 +02:00
MacRimi e4da9f5afe Update storage-metrics.tsx 2025-10-14 09:06:17 +02:00
MacRimi 3f919813f2 Update AppImage 2025-10-14 08:54:56 +02:00
MacRimi 4063ffc163 Update storage-metrics.tsx 2025-10-14 00:20:15 +02:00
MacRimi 0d08b89853 Update storage-metrics.tsx 2025-10-14 00:13:10 +02:00
MacRimi 6c9da364d0 Update storage-metrics.tsx 2025-10-14 00:03:22 +02:00
MacRimi c1614e8241 Update AppImage 2025-10-13 23:50:31 +02:00
MacRimi 75a458f2be Update AppImage 2025-10-13 19:04:38 +02:00
MacRimi 602291736f Update flask_server.py 2025-10-13 17:53:34 +02:00
MacRimi 9f2d15e590 Update AppImage 2025-10-13 17:40:08 +02:00
MacRimi 5e88201d47 Update flask_server.py 2025-10-13 17:03:39 +02:00
MacRimi 598b8bd1cd Update AppImage 2025-10-13 15:22:19 +02:00
MacRimi 9186a44860 Update AppImage 2025-10-13 15:06:03 +02:00
MacRimi 61e3dae708 Update AppImage 2025-10-12 21:19:01 +02:00
MacRimi 94d46299d0 Update proxmox-dashboard.tsx 2025-10-12 21:10:24 +02:00
MacRimi 7070c05f2f Update proxmox-dashboard.tsx 2025-10-12 21:00:42 +02:00
MacRimi 0b7038cc65 Update proxmox-dashboard.tsx 2025-10-12 20:47:58 +02:00
MacRimi 4882d04ece Update system-logs.tsx 2025-10-12 20:36:12 +02:00
MacRimi 4b2d34491e Update system-logs.tsx 2025-10-12 20:28:10 +02:00
MacRimi 79d8230821 Update system-logs.tsx 2025-10-12 20:20:32 +02:00
MacRimi c7e3305a76 Update system-logs.tsx 2025-10-12 20:12:55 +02:00
MacRimi 81feccf0d2 Update AppImage 2025-10-12 20:03:40 +02:00
MacRimi 9666bee006 Update AppImage 2025-10-12 19:51:03 +02:00
MacRimi 44d54057d0 Update hardware.tsx 2025-10-12 19:40:35 +02:00
MacRimi beb4251688 Update network-metrics.tsx 2025-10-12 19:32:17 +02:00
MacRimi 598395cd38 Update system-overview.tsx 2025-10-12 19:20:05 +02:00
MacRimi 0a6913f5d0 Update system-overview.tsx 2025-10-12 19:11:39 +02:00
MacRimi fa36458303 Update AppImage 2025-10-12 18:51:38 +02:00
MacRimi 3f96f88027 Update system-overview.tsx 2025-10-12 18:19:50 +02:00
MacRimi 131ab714ba Update system-overview.tsx 2025-10-12 18:05:35 +02:00
MacRimi 333d0c933a Update system-overview.tsx 2025-10-12 17:50:15 +02:00
MacRimi 2a5c0e05cc Update flask_server.py 2025-10-12 17:37:51 +02:00
MacRimi 4bda9da860 Update AppImage 2025-10-12 17:24:13 +02:00
MacRimi 4c579cf862 Update flask_server.py 2025-10-12 17:09:01 +02:00
MacRimi 1b74ce7ac0 Update flask_server.py 2025-10-12 16:32:29 +02:00
MacRimi 29e3625c7b Update flask_server.py 2025-10-12 16:09:38 +02:00
MacRimi a41b9381a1 Update system-logs.tsx 2025-10-12 16:00:52 +02:00
MacRimi b4980a968c Update system-logs.tsx 2025-10-12 15:39:31 +02:00
MacRimi ea91751217 Update system-logs.tsx 2025-10-12 14:51:07 +02:00
MacRimi 1ceffc3391 Update system-logs.tsx 2025-10-12 03:12:15 +02:00
MacRimi 3de31427a3 Update system-logs.tsx 2025-10-12 02:58:11 +02:00
MacRimi 4abb9c2ea6 Update system-logs.tsx 2025-10-12 02:44:49 +02:00
MacRimi e7bfbe77c2 Update system-logs.tsx 2025-10-12 02:04:42 +02:00
MacRimi 776282ed6b Update system-logs.tsx 2025-10-12 01:46:48 +02:00
MacRimi d1621684df Update flask_server.py 2025-10-12 01:26:58 +02:00
MacRimi ba183e71e1 Update AppImage 2025-10-12 01:09:33 +02:00
MacRimi aac34d4fad Update system-logs.tsx 2025-10-12 00:54:23 +02:00
MacRimi 8e28e4ecbf Update AppImage 2025-10-12 00:45:38 +02:00
MacRimi 48665aa1ad Update AppImage 2025-10-12 00:41:15 +02:00
MacRimi f34968bcf5 Update calendar.tsx 2025-10-12 00:35:47 +02:00
MacRimi 4a5c1ed582 Update AppImage 2025-10-11 19:43:15 +02:00
MacRimi 6d87ab08e2 Update AppImage 2025-10-11 19:35:04 +02:00
MacRimi 5ae18bf4f9 Update calendar.tsx 2025-10-11 19:24:20 +02:00
MacRimi 1bac12259d Update system-logs.tsx 2025-10-11 19:16:44 +02:00
MacRimi 434dc408c3 Update calendar.tsx 2025-10-11 19:09:24 +02:00
MacRimi 6601ee3b12 Update AppImage 2025-10-11 19:04:02 +02:00
MacRimi fde1731365 Update AppImage 2025-10-11 18:51:50 +02:00
MacRimi 7725952776 Update AppImage 2025-10-11 18:37:26 +02:00
MacRimi e18ee08b70 Update AppImge 2025-10-11 18:13:35 +02:00
MacRimi 5aaaeb426c Update AppImage 2025-10-11 17:55:25 +02:00
MacRimi 1f55a0cbd8 Update AppImage 2025-10-11 17:34:10 +02:00
MacRimi 4ad026b398 Update flask_server.py 2025-10-11 17:29:17 +02:00
MacRimi d36825da52 Update AppImage 2025-10-11 17:18:52 +02:00
MacRimi bf2715c2be Update AppImage 2025-10-11 16:51:27 +02:00
MacRimi 80953a0148 Update AppImage 2025-10-11 16:25:22 +02:00
MacRimi bb9a08d00d Update system-logs.tsx 2025-10-11 16:04:42 +02:00
MacRimi 3e8fa7cba7 Update system-logs.tsx 2025-10-11 12:06:59 +02:00
MacRimi da8b88b6b2 Update flask_server.py 2025-10-11 11:42:02 +02:00
MacRimi 4bd21a1ccb Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-11 11:30:47 +02:00
MacRimi 29f7586b93 Update AppImage 2025-10-11 11:30:45 +02:00
ProxMenuxBot 39b6b725f5 Update helpers_cache.json 2025-10-11 00:56:17 +00:00
MacRimi 631c68029a Update AppImage 2025-10-11 01:36:42 +02:00
MacRimi 93aefc127f Update storage-overview.tsx 2025-10-11 00:21:22 +02:00
MacRimi 19ac88b560 Update flask_server.py 2025-10-11 00:11:12 +02:00
MacRimi 99e775a283 Update hardware.tsx 2025-10-11 00:00:21 +02:00
MacRimi 7fc05b96a2 Update flask_server.py 2025-10-10 23:52:06 +02:00
MacRimi 9b811da43d Update hardware.tsx 2025-10-10 23:39:31 +02:00
MacRimi 4199b609b4 Updata AppImage 2025-10-10 23:19:29 +02:00
MacRimi 958e6d8519 Update flask_server.py 2025-10-10 23:06:04 +02:00
MacRimi 9dea22ab05 Update hardware.tsx 2025-10-10 22:52:22 +02:00
MacRimi 3dc7c6b36f Update flask_server.py 2025-10-10 22:43:02 +02:00
MacRimi 44e76f36b4 Update flask_server.py 2025-10-10 22:33:45 +02:00
MacRimi 010333a190 Update flask_server.py 2025-10-10 21:43:43 +02:00
MacRimi ba833a265a Update AppImage 2025-10-10 21:38:19 +02:00
MacRimi e0bf156272 Update hardware.tsx 2025-10-10 21:18:49 +02:00
MacRimi a654e21b27 Update flask_server.py 2025-10-10 21:06:00 +02:00
MacRimi de6f149e3b Update flask_server.py 2025-10-10 17:37:30 +02:00
MacRimi 29893b89b3 Update flask_server.py 2025-10-10 17:09:23 +02:00
MacRimi 7783e9ed20 Update flask_server.py 2025-10-10 16:52:43 +02:00
MacRimi d93d1ed48a Update AppImage 2025-10-10 16:17:55 +02:00
MacRimi 32c461e93b Update AppImage 2025-10-10 15:40:41 +02:00
MacRimi d88e6153c1 Update flask_server.py 2025-10-10 12:45:08 +02:00
MacRimi 7b980ae4d4 Update flask_server.py 2025-10-10 12:40:58 +02:00
MacRimi b249d37bab Update flask_server.py 2025-10-10 12:29:47 +02:00
MacRimi fa34e081cc Update hardware.tsx 2025-10-10 01:06:17 +02:00
MacRimi 9f795d7256 Update flask_server.py 2025-10-10 00:48:56 +02:00
MacRimi c8d7d6be43 Update hardware.tsx 2025-10-10 00:38:57 +02:00
MacRimi c31124eb14 Update hardware.tsx 2025-10-10 00:27:22 +02:00
MacRimi e999b7a8f8 Update hardware.tsx 2025-10-10 00:13:54 +02:00
MacRimi 49353a5ec5 Update hardware.tsx 2025-10-09 23:56:43 +02:00
MacRimi 229fbdd306 Update hardware.tsx 2025-10-09 23:46:26 +02:00
MacRimi 4562dd08dc Update hardware.tsx 2025-10-09 23:34:44 +02:00
MacRimi f24f4ea8f9 Update hardware.tsx 2025-10-09 23:14:47 +02:00
MacRimi 6338d38ab6 Update flask_server.py 2025-10-09 23:00:25 +02:00
MacRimi 527d93c6b4 Update flask_server.py 2025-10-09 22:48:33 +02:00
MacRimi 3f5f8d9f57 Update flask_server.py 2025-10-09 22:37:02 +02:00
MacRimi 245c913ba1 Update flask_server.py 2025-10-09 22:17:09 +02:00
MacRimi 7d3ef52f03 Update flask_server.py 2025-10-09 21:16:55 +02:00
MacRimi 6cd7556bc5 Update flask_server.py 2025-10-09 20:58:43 +02:00
MacRimi 123f0594a3 Update flask_server.py 2025-10-09 20:40:29 +02:00
MacRimi 652cebc7d0 Update flask_server.py 2025-10-09 20:26:21 +02:00
MacRimi a4cb9a8923 Update flask_server.py 2025-10-09 20:18:49 +02:00
MacRimi eb954fb10d Update flask_server.py 2025-10-09 19:52:44 +02:00
MacRimi 845eab6f53 Update flask_server.py 2025-10-09 19:38:54 +02:00
MacRimi 4fe20db497 Update AppImage 2025-10-09 19:30:12 +02:00
MacRimi c40d503f6e Update flask_server.py 2025-10-09 19:24:39 +02:00
MacRimi 1ea843bde4 Update flask_server.py 2025-10-09 19:21:32 +02:00
MacRimi 9ed5d70250 Update flask_server.py 2025-10-09 19:08:43 +02:00
MacRimi f6209b97e2 Update AppImage 2025-10-09 19:00:58 +02:00
MacRimi 5221ad6da7 Update flask_server.py 2025-10-09 18:46:16 +02:00
MacRimi cd0bded428 Update flask_server.py 2025-10-09 18:09:59 +02:00
MacRimi ff5fddf353 Update flask_server.py 2025-10-09 18:01:46 +02:00
MacRimi 28b29ed086 Update flask_server.py 2025-10-09 17:03:01 +02:00
MacRimi 765b2b1d69 Update flask_server.py 2025-10-09 16:46:07 +02:00
MacRimi 599a434faa Update flask_server.py 2025-10-09 16:23:19 +02:00
MacRimi a2abee986d Update flask_server.py 2025-10-09 16:06:42 +02:00
MacRimi d57c0712b0 Update flask_server.py 2025-10-09 15:44:17 +02:00
MacRimi 4166d78e87 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-10-09 15:35:43 +02:00
MacRimi 5322291402 Update flask_server.py 2025-10-09 15:35:40 +02:00
ProxMenuxBot dc2ffd758d Update helpers_cache.json 2025-10-09 12:26:29 +00:00
MacRimi b73cdb2e7d Update flask_server.py 2025-10-09 10:29:21 +02:00
MacRimi 3a83e5d519 Update flask_server.py 2025-10-09 10:00:06 +02:00
MacRimi 3c0cdcadc0 Update flask_server.py 2025-10-09 09:48:18 +02:00
MacRimi 0a23ef8b5d Update flask_server.py 2025-10-08 18:26:28 +02:00
MacRimi c8a38ac709 Update flask_server.py 2025-10-08 18:19:19 +02:00
MacRimi 31d15fadbe Update flask_server.py 2025-10-08 18:15:38 +02:00
MacRimi 3fc481e302 Update hardware.tsx 2025-10-08 12:06:24 +02:00
MacRimi 803605c318 Update AppImage 2025-10-08 11:43:45 +02:00
MacRimi 1d138e3b4b Update flask_server.py 2025-10-07 23:42:15 +02:00
MacRimi 5b6c5326b6 Update AppImage 2025-10-07 23:36:13 +02:00
MacRimi 60a1c303da Update hardware.tsx 2025-10-07 23:04:44 +02:00
MacRimi b9f32da7b8 Update flask_server.py 2025-10-07 22:38:49 +02:00
MacRimi fde0b1d8bf Update AppImage 2025-10-07 22:26:49 +02:00
MacRimi cf07004fcd Update flask_server.py 2025-10-07 21:59:57 +02:00
MacRimi b41b52df84 Update flask_server.py 2025-10-07 21:49:29 +02:00
MacRimi 9632dd170a Update hardware.tsx 2025-10-07 21:32:54 +02:00
MacRimi 9dc334dea9 Update flask_server.py 2025-10-07 21:15:01 +02:00
MacRimi 0741079450 Update flask_server.py 2025-10-07 21:00:03 +02:00
MacRimi 562df0f48f Update flask_server.py 2025-10-07 20:26:05 +02:00
MacRimi 3b87c078f4 Update flask_server.py 2025-10-07 20:15:42 +02:00
MacRimi fc091665ff Update flask_server.py 2025-10-07 19:59:18 +02:00
MacRimi 3f2842a9a3 Update flask_server.py 2025-10-07 18:26:37 +02:00
MacRimi f2418c81d7 Update flask_server.py 2025-10-07 18:22:15 +02:00
MacRimi 3c85797cc9 Update flask_server.py 2025-10-07 18:15:33 +02:00
MacRimi 9187bf0b83 Update flask_server.py 2025-10-07 17:53:57 +02:00
MacRimi 5a2381d9dd Update flask_server.py 2025-10-07 17:43:12 +02:00
MacRimi 5fb8bb0dac Update flask_server.py 2025-10-07 17:22:14 +02:00
MacRimi 0d55da18d4 Update hardware.tsx 2025-10-07 12:08:49 +02:00
MacRimi 73148d65bb Update hardware.tsx 2025-10-07 11:22:51 +02:00
MacRimi e81438e49f Update AppImage 2025-10-07 11:10:20 +02:00
MacRimi e247d8095e Update AppImage 2025-10-07 10:49:43 +02:00
MacRimi 46ddb36c79 Update hardware.tsx 2025-10-07 03:03:45 +02:00
MacRimi a87fee906f Update flask_server.py 2025-10-07 02:51:10 +02:00
MacRimi 888d94131e Update AppImage 2025-10-07 02:46:35 +02:00
MacRimi 63dd018756 Update flask_server.py 2025-10-07 02:29:07 +02:00
MacRimi db38571646 Update flask_server.py 2025-10-07 02:23:55 +02:00
MacRimi 8111d96a20 Update flask_server.py 2025-10-07 02:02:14 +02:00
MacRimi a1b5b7c03c Update hardware.tsx 2025-10-07 01:37:43 +02:00
MacRimi 91e95b1ef2 Update flask_server.py 2025-10-07 01:31:29 +02:00
MacRimi 1491f35f5e Update AppImage 2025-10-07 01:24:02 +02:00
MacRimi 9bb127dda7 Update flask_server.py 2025-10-07 01:10:54 +02:00
MacRimi c4348b0cb2 Update flask_server.py 2025-10-07 00:56:41 +02:00
MacRimi a3fc0c7f96 Update hardware.tsx 2025-10-07 00:50:17 +02:00
MacRimi c7387068cc Update hardware.tsx 2025-10-07 00:44:26 +02:00
MacRimi 658ce390e2 Update AppImage 2025-10-07 00:35:23 +02:00
MacRimi f0b6f66be6 Update AppImage 2025-10-07 00:24:35 +02:00
MacRimi 304812e14f Update flask_server.py 2025-10-06 23:57:02 +02:00
MacRimi 92d8a05393 Update AppImage 2025-10-06 23:51:35 +02:00
MacRimi d96d98b8f4 Update AppImage 2025-10-06 23:40:54 +02:00
MacRimi 0d059187ec Update hardware.tsx 2025-10-06 23:26:26 +02:00
MacRimi 1b73b0b861 Update hardware.tsx 2025-10-06 23:16:31 +02:00
MacRimi 29f8d6b981 Update AppImage 2025-10-06 22:58:54 +02:00
MacRimi 7826de9d29 Update flask_server.py 2025-10-06 22:44:24 +02:00
MacRimi 78c56e4f28 Update AppImage 2025-10-06 22:39:37 +02:00
MacRimi 3ef7736e85 Update hardware.tsx 2025-10-06 22:23:56 +02:00
MacRimi 73e6194551 Update AppImage 2025-10-06 22:19:42 +02:00
MacRimi 7ceed3dfbc Update flask_server.py 2025-10-06 19:15:17 +02:00
MacRimi 7a0c2dc261 Update AppImage 2025-10-06 19:08:21 +02:00
MacRimi 5807c4d97f Update flask_server.py 2025-10-06 19:00:36 +02:00
MacRimi a689607e98 Update AppImage 2025-10-06 18:52:58 +02:00
MacRimi b6b3e27408 Update AppImage 2025-10-06 18:29:00 +02:00
MacRimi ac30bd6e51 Update hardware.ts 2025-10-06 18:15:56 +02:00
MacRimi 174fc4f72b Update flask_server.py 2025-10-06 18:09:07 +02:00
MacRimi 047ec982f4 Update AppImage 2025-10-06 18:02:40 +02:00
MacRimi e427f37f0e Update AppImage 2025-10-06 17:40:42 +02:00
MacRimi 810ac1fcfa Update AppImage 2025-10-06 17:25:08 +02:00
MacRimi 5ee3cc6712 Update build_appimage.sh 2025-10-06 17:00:52 +02:00
MacRimi 5ad3d5697e Update build_appimage.sh 2025-10-06 16:54:12 +02:00
MacRimi 874ab093d5 Update AppImage 2025-10-06 16:40:14 +02:00
MacRimi fb668859b0 Update build_appimage.sh 2025-10-06 14:53:10 +02:00
MacRimi be7a2d7f41 Update AppImage 2025-10-06 14:17:14 +02:00
MacRimi 154b6b9f74 Update AppImage 2025-10-06 14:12:28 +02:00
MacRimi 23c91386dc Update AppImage 2025-10-06 13:48:02 +02:00
MacRimi 741b6ce0d9 Update AppImage 2025-10-06 13:06:27 +02:00
MacRimi 600c2f6061 Update AppImagen 2025-10-06 12:09:43 +02:00
MacRimi 359de2dbe0 Update build_appimage.sh 2025-10-06 11:06:19 +02:00
MacRimi 84eec4655a Update AppImage 2025-10-06 11:02:00 +02:00
MacRimi 7e8c69a02d Update hardware.tsx 2025-10-05 22:46:14 +02:00
MacRimi 730d47f2f7 Update AppImage 2025-10-05 22:40:38 +02:00
MacRimi 5afb74e606 Update hardware.tsx 2025-10-05 22:34:30 +02:00
MacRimi 8a21547668 Update hardware.tsx 2025-10-05 22:27:20 +02:00
MacRimi 3ee3044270 Update flask_server.py 2025-10-05 22:15:32 +02:00
MacRimi 782dc24eba Update AppImage 2025-10-05 22:10:24 +02:00
MacRimi 475b96178e Update AppImage 2025-10-05 21:59:44 +02:00
MacRimi 4beba53675 Update AppImage 2025-10-05 21:44:22 +02:00
MacRimi efb7cad993 Update flask_server.py 2025-10-05 21:24:15 +02:00
MacRimi 7b7705866d Update proxmox-dashboard.tsx 2025-10-05 21:15:51 +02:00
MacRimi b7e06d51ea Create sheet.tsx 2025-10-05 21:02:20 +02:00
MacRimi 95476276ac Update proxmox-dashboard.tsx 2025-10-05 20:58:15 +02:00
MacRimi 2347e10458 Update AppImage 2025-10-05 20:45:54 +02:00
MacRimi 85051f1340 Update virtual-machines.tsx 2025-10-05 20:30:47 +02:00
MacRimi 42c6e70ebe Update virtual-machines.tsx 2025-10-05 20:24:30 +02:00
MacRimi b9fe83e7a8 Update virtual-machines.tsx 2025-10-05 20:12:33 +02:00
MacRimi 841108623f Update virtual-machines.tsx 2025-10-05 17:27:10 +02:00
MacRimi 8ce221e41b Update virtual-machines.tsx 2025-10-05 17:18:26 +02:00
MacRimi f8c41ab39f Update virtual-machines.tsx 2025-10-05 17:10:28 +02:00
MacRimi 79e7fd175e Update virtual-machines.tsx 2025-10-05 17:01:50 +02:00
MacRimi fbcf755591 Update virtual-machines.tsx 2025-10-05 16:28:12 +02:00
MacRimi 6168a47e24 Update network-metrics.tsx 2025-10-05 16:15:45 +02:00
MacRimi d788114be3 Update AppImage 2025-10-05 16:05:54 +02:00
MacRimi 497814f80c Update AppImage 2025-10-05 15:54:24 +02:00
MacRimi 7297edf16f Update AppImage 2025-10-05 15:44:19 +02:00
MacRimi 714407eb46 Update virtual-machines.tsx 2025-10-05 15:38:29 +02:00
MacRimi dd3523ddd7 Update virtual-machines.tsx 2025-10-05 15:18:50 +02:00
MacRimi 7739de5db9 Update AppImage 2025-10-05 15:00:42 +02:00
MacRimi 99c08026ee Update network-metrics.tsx 2025-10-05 14:33:47 +02:00
MacRimi 19f7ea70f0 Update AppImage 2025-10-05 14:16:21 +02:00
MacRimi 49050c042d Update AppImage 2025-10-05 13:50:29 +02:00
MacRimi 18ccff5759 Update AppImage 2025-10-05 13:15:44 +02:00
MacRimi 9f6f646e77 Update virtual-machines.tsx 2025-10-05 12:56:06 +02:00
MacRimi b8c0d8ef79 Update AppImage 2025-10-05 12:48:34 +02:00
MacRimi 2ccd41bfb9 Update AppImage 2025-10-05 12:32:09 +02:00
MacRimi fa64b51d4a Update AppImage 2025-10-05 12:03:47 +02:00
MacRimi f5ac194008 Update AppImage 2025-10-05 11:48:32 +02:00
MacRimi 816cf0141b Update AppImage 2025-10-04 20:23:42 +02:00
ProxMenuxBot 7baabc6d2c Update helpers_cache.json 2025-10-04 18:16:43 +00:00
MacRimi 37d1c7338b Update flask_server.py 2025-10-04 20:14:57 +02:00
MacRimi 404ea9d838 Update AppImage 2025-10-04 20:06:47 +02:00
MacRimi 98c5c5827c Update AppImage 2025-10-04 19:58:12 +02:00
MacRimi 79525284b1 Update network-metrics.tsx 2025-10-04 19:53:12 +02:00
MacRimi c14ea7afdf Update AppImage 2025-10-04 19:45:37 +02:00
MacRimi c437753d64 Update package.json 2025-10-04 19:28:13 +02:00
MacRimi 441cc35e5a Update AppImage 2025-10-04 19:25:28 +02:00
MacRimi dc03144773 Update AppImage 2025-10-04 19:05:39 +02:00
MacRimi 992921b24c Update storage-overview.tsx 2025-10-04 18:53:31 +02:00
MacRimi 28f38dca46 Update storage-overview.tsx 2025-10-04 18:46:12 +02:00
MacRimi 53155ccef0 Update AppImage 2025-10-04 18:36:15 +02:00
MacRimi ba6f0a1aab Update AppImage 2025-10-04 18:23:45 +02:00
MacRimi 2d89d06bcb Update AppImage 2025-10-04 17:48:10 +02:00
MacRimi 54ff50ce68 Update AppImage 2025-10-04 17:34:07 +02:00
MacRimi 22aa8cdd6c Update flask_server.py 2025-10-04 17:03:39 +02:00
MacRimi 06b0195d74 Update customizable_post_install.sh 2025-10-04 16:31:38 +02:00
MacRimi a99b4ded7f Update uninstall-tools.sh 2025-10-04 16:22:17 +02:00
MacRimi 2405a0e778 Update uninstall-tools.sh 2025-10-04 16:20:30 +02:00
MacRimi 84544b1e84 Update auto_post_install.sh 2025-10-04 16:11:45 +02:00
MacRimi 95fce39502 Update auto_post_install.sh 2025-10-04 16:04:09 +02:00
MacRimi 99c5b26241 Update uninstall-tools.sh 2025-10-04 10:26:06 +02:00
ProxMenuxBot 6e07e49c84 Update helpers_cache.json 2025-10-03 18:18:02 +00:00
ProxMenuxBot 0bcfea9d20 Update helpers_cache.json 2025-10-03 12:25:37 +00:00
MacRimi 2658331fd2 Update AppImage 2025-10-02 23:41:31 +02:00
MacRimi 2ab49cc545 Update AppImage 2025-10-02 23:20:59 +02:00
MacRimi a39fe5ff3b Update flask_server.py 2025-10-02 23:02:17 +02:00
MacRimi 01578b4e34 Update flask_server.py 2025-10-02 22:38:06 +02:00
MacRimi 95718c889d Update AppImage 2025-10-02 22:29:24 +02:00
MacRimi 6279cc9ec1 Update AppImage 2025-10-02 19:51:53 +02:00
MacRimi f7fb9034ef Update system-overview.tsx 2025-10-02 19:34:53 +02:00
MacRimi 15f3af2020 Update AppImage 2025-10-02 18:28:36 +02:00
MacRimi 97288ed6ce Update system-overview.tsx 2025-10-02 18:11:00 +02:00
MacRimi 5e168c2561 Update system-overview.tsx 2025-10-02 17:49:40 +02:00
MacRimi 358b3f96ae Update globals.css 2025-10-02 17:41:23 +02:00
MacRimi c0d9c3808a Update globals.css 2025-10-02 17:28:23 +02:00
MacRimi 7404bb8e64 Update globals.css 2025-10-02 17:21:04 +02:00
MacRimi 93eccd7dcf Update globals.css 2025-10-02 17:17:21 +02:00
MacRimi 05d9d41860 Update AppImage 2025-10-02 17:10:27 +02:00
MacRimi c47c41548f Update globals.css 2025-10-02 16:57:12 +02:00
ProxMenuxBot 013d1980a3 Update helpers_cache.json 2025-10-01 18:18:31 +00:00
MacRimi df9f4a23b4 Update AppImage 2025-10-01 18:14:58 +02:00
MacRimi c41da47a48 Update AppImage 2025-10-01 18:08:31 +02:00
MacRimi e7214ad8df Update globals.css 2025-10-01 18:04:38 +02:00
MacRimi d6671de842 Update AppImage 2025-10-01 18:01:54 +02:00
MacRimi aad218db5d Update globals.css 2025-10-01 17:44:00 +02:00
MacRimi 724ba1e271 Update AppImge 2025-10-01 17:39:43 +02:00
MacRimi 97d554f638 update AppImage 2025-10-01 17:27:05 +02:00
MacRimi c5a7655d26 Update AppImage 2025-10-01 17:10:37 +02:00
MacRimi 403e896e3e Update proxmox-dashboard.tsx 2025-10-01 17:03:56 +02:00
MacRimi 1a15f43cad Update proxmox-dashboard.tsx 2025-10-01 16:53:37 +02:00
MacRimi 399b460c53 Update globals.css 2025-09-30 00:11:24 +02:00
MacRimi acc0362180 Update AppImage 2025-09-30 00:09:11 +02:00
MacRimi 00db93e03f Update AppImage 2025-09-29 23:56:33 +02:00
MacRimi d1997794c8 Update globals.css 2025-09-29 23:40:45 +02:00
MacRimi aa1ebe69f2 Update globals.css 2025-09-29 23:11:40 +02:00
MacRimi 4e7f5f56f1 Update AppImage 2025-09-29 22:59:10 +02:00
MacRimi 28cb7359ce Update system-overview.tsx 2025-09-29 22:38:25 +02:00
ProxMenuxBot 91c272d21c Update helpers_cache.json 2025-09-29 18:19:17 +00:00
MacRimi 3c00125e83 Update flask_server.py 2025-09-29 19:19:35 +02:00
MacRimi f359848a2f Update AppRun 2025-09-29 19:12:56 +02:00
MacRimi 989769e5e8 Update AppRun 2025-09-29 19:07:35 +02:00
MacRimi 0f2f1b6211 Update system-overview.tsx 2025-09-29 19:02:59 +02:00
MacRimi ffe8f4acc6 Update AppImage 2025-09-29 18:58:53 +02:00
MacRimi edb09777de Update package.json 2025-09-29 18:47:18 +02:00
MacRimi 5262c7863e Update AppImage 2025-09-29 18:43:14 +02:00
MacRimi 54256826fe Update AppImage 2025-09-29 18:37:32 +02:00
MacRimi 3d3c224b3a Update flask_server.py 2025-09-29 18:31:09 +02:00
MacRimi 049eccb872 Update AppImage 2025-09-29 18:27:09 +02:00
MacRimi 269828c79e Update AppImage 2025-09-29 18:16:01 +02:00
MacRimi b4e25ae66d Update AppImage 2025-09-29 18:07:30 +02:00
MacRimi b20dd74d23 Update AppImage 2025-09-29 17:57:00 +02:00
MacRimi bc3e2ec358 Update AppImage 2025-09-29 17:46:37 +02:00
MacRimi 6133a6d6d8 Update build_appimage.sh 2025-09-29 17:32:24 +02:00
MacRimi 46a16c04e6 Update AppImage 2025-09-29 17:21:59 +02:00
MacRimi 8469b3b26f Update AppImage 2025-09-29 17:05:42 +02:00
ProxMenuxBot 2ed04f57fe Update helpers_cache.json 2025-09-29 12:26:48 +00:00
MacRimi b19bac679a Update flask_server.py 2025-09-29 00:00:01 +02:00
MacRimi 3c33d5982c Update AppImage 2025-09-28 23:51:06 +02:00
MacRimi 5b934eeb87 Update AppImage 2025-09-28 23:25:58 +02:00
MacRimi 795d96f8d5 Update AppImage 2025-09-28 23:09:31 +02:00
MacRimi a8e7119b4a Update AppImage 2025-09-28 23:05:59 +02:00
MacRimi 38569ff7fc Update AppImage 2025-09-28 22:57:15 +02:00
MacRimi e404557d62 Update AppImage 2025-09-28 22:53:42 +02:00
MacRimi 96cbc75a5e Update build_appimage.sh 2025-09-28 21:26:25 +02:00
MacRimi c989af6cf0 Update build_appimage.sh 2025-09-28 21:18:52 +02:00
MacRimi 4eac9d03ea Update build_appimage.sh 2025-09-28 21:15:18 +02:00
MacRimi 6292009b0b Update AppImage 2025-09-28 21:08:58 +02:00
MacRimi 3272be967d Update build_appimage.sh 2025-09-28 21:01:48 +02:00
MacRimi 1c015da440 Update build_appimage.sh 2025-09-28 20:57:23 +02:00
MacRimi 0d047cc956 Update build_appimage.sh 2025-09-28 20:46:55 +02:00
MacRimi e682070b85 update AppImage 2025-09-28 20:28:35 +02:00
MacRimi 9f08694d9b Update Appimage 2025-09-28 20:25:55 +02:00
MacRimi 70f0db73e5 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-28 20:20:46 +02:00
MacRimi 9dc8f44379 Create tsconfig.json 2025-09-28 20:20:31 +02:00
MacRimi 59f7ccd723 Update build-appimage.yml 2025-09-28 20:15:56 +02:00
MacRimi 0710e95a6d Update package.json 2025-09-28 20:15:33 +02:00
MacRimi 4d1b5e3919 Update build-appimage.yml 2025-09-28 20:12:21 +02:00
MacRimi 0cc2cb92dd Update build-appimage.yml 2025-09-28 20:10:08 +02:00
MacRimi dba4d168f7 Update build-appimage.yml 2025-09-28 20:08:02 +02:00
MacRimi d87ac7843c Update build-appimage.yml 2025-09-28 20:07:12 +02:00
MacRimi 040535b004 Update build-appimage.yml 2025-09-28 20:04:39 +02:00
MacRimi c8acd2c0b1 Update build-appimage.yml 2025-09-28 19:56:03 +02:00
MacRimi d67fecea6e Update build-appimage.yml 2025-09-28 19:46:03 +02:00
MacRimi 61f80f9ee6 Update build-appimage.yml 2025-09-28 19:44:51 +02:00
MacRimi 9da8f9a5d1 Update build-appimage.yml 2025-09-28 19:43:05 +02:00
MacRimi f381468d5a Create build-appimage.yml 2025-09-28 19:41:13 +02:00
MacRimi 6ae97266e4 Create AppImage 2025-09-28 19:40:23 +02:00
MacRimi 66060f345c Create jd2_2.sh 2025-09-27 20:21:15 +02:00
MacRimi c61f568170 Update nfs_lxc_server.sh 2025-09-27 18:50:50 +02:00
MacRimi dcd108bda3 Update update-pve.sh 2025-09-27 18:28:56 +02:00
MacRimi 9d89f98987 Update customizable_post_install.sh 2025-09-27 18:26:23 +02:00
MacRimi ca7b959fce Update auto_post_install.sh 2025-09-27 18:25:25 +02:00
MacRimi 4a30793595 Update uninstall-tools.sh 2025-09-27 18:24:02 +02:00
MacRimi 35e2d53f0f update remove subscription banner PVE 9 2025-09-27 18:16:12 +02:00
MacRimi 503efa4572 Create remove-banner-pve9_2.sh 2025-09-27 17:42:18 +02:00
ProxMenuxBot b0c33d9dff Update helpers_cache.json 2025-09-27 00:56:57 +00:00
MacRimi 012b156b46 Update install_coral_pve9.sh 2025-09-26 00:17:32 +02:00
MacRimi 25d0d3bf59 Create install_coral_pve9.sh 2025-09-25 23:45:50 +02:00
ProxMenuxBot 0f1babc82b Update helpers_cache.json 2025-09-25 18:20:06 +00:00
ProxMenuxBot e2b93ea785 Update helpers_cache.json 2025-09-24 18:19:23 +00:00
ProxMenuxBot b1cedfa81e Update helpers_cache.json 2025-09-24 12:27:12 +00:00
ProxMenuxBot 701ee36f6a Update helpers_cache.json 2025-09-23 12:25:52 +00:00
ProxMenuxBot 4e5db86434 Update helpers_cache.json 2025-09-21 12:23:25 +00:00
ProxMenuxBot f45e9e657c Update helpers_cache.json 2025-09-19 18:18:42 +00:00
ProxMenuxBot 4936fcdb1e Update helpers_cache.json 2025-09-18 18:18:59 +00:00
ProxMenuxBot 374e05c422 Update helpers_cache.json 2025-09-18 12:25:39 +00:00
ProxMenuxBot 9c00798373 Update helpers_cache.json 2025-09-17 18:18:09 +00:00
ProxMenuxBot db82fce925 Update helpers_cache.json 2025-09-15 12:26:37 +00:00
ProxMenuxBot acaa28e476 Update helpers_cache.json 2025-09-13 00:54:55 +00:00
ProxMenuxBot f297ce5809 Update helpers_cache.json 2025-09-12 12:24:43 +00:00
ProxMenuxBot 3dc3fc5f67 Update helpers_cache.json 2025-09-12 00:57:45 +00:00
ProxMenuxBot 4884fc4418 Update helpers_cache.json 2025-09-11 18:15:27 +00:00
MacRimi adc17842ec Update README.md 2025-09-11 18:18:42 +02:00
MacRimi daa48b0b7c Update README.md 2025-09-11 18:17:16 +02:00
MacRimi 17c0362df3 Update cache.json 2025-09-10 20:29:19 +02:00
MacRimi 29b9a63fc9 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-10 20:28:00 +02:00
MacRimi 2a9fae160e Update cache.json 2025-09-10 20:27:58 +02:00
ProxMenuxBot 0c49a1e3bd Update helpers_cache.json 2025-09-10 18:18:53 +00:00
MacRimi e896c41be1 Update main_menu.sh 2025-09-10 19:14:42 +02:00
MacRimi 187250fa24 update text ProxMenux 2025-09-10 19:06:04 +02:00
MacRimi 9035b18584 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-10 18:58:12 +02:00
MacRimi 4534d78978 update text ProxMenux 2025-09-10 18:57:49 +02:00
MacRimi f4ab0e982c Update README.md 2025-09-10 18:53:41 +02:00
MacRimi 3e7c6629a6 update text ProxMenux 2025-09-10 18:47:55 +02:00
MacRimi 3ea17331fe Update install_proxmenux.sh 2025-09-10 18:24:14 +02:00
MacRimi 1057fcc271 Update install_proxmenux.sh 2025-09-10 18:19:13 +02:00
MacRimi 5a31c36097 update menu share 2025-09-10 18:05:06 +02:00
MacRimi 1677a69bba Update version.txt 2025-09-10 17:52:17 +02:00
MacRimi 315c49165d Update CHANGELOG.md 2025-09-10 17:50:53 +02:00
MacRimi aae70e7ec0 Update CHANGELOG.md 2025-09-10 17:47:37 +02:00
MacRimi 5cb9e13ca7 Update CHANGELOG.md 2025-09-10 17:37:51 +02:00
MacRimi 0187010f94 Create main-menu.png 2025-09-10 16:28:47 +02:00
ProxMenuxBot 2c2ed21e59 Update helpers_cache.json 2025-09-10 12:24:52 +00:00
MacRimi f8b2ccec40 Update commands_share.sh 2025-09-10 13:48:17 +02:00
MacRimi e858dc582d Update commands_share.sh 2025-09-10 13:47:13 +02:00
MacRimi dd737f4b46 Update commands_share.sh 2025-09-10 13:46:02 +02:00
MacRimi f0bc238b6d Update commands_share.sh 2025-09-10 13:35:16 +02:00
MacRimi af55424850 Update lxc-mount-manager_minimal.sh 2025-09-10 13:16:54 +02:00
MacRimi 902534baff update menu shared 2025-09-10 12:57:17 +02:00
MacRimi 6daa630040 Update nfs_host.sh 2025-09-10 12:53:09 +02:00
MacRimi 0b2b86673b Update lxc-mount-manager_minimal.sh 2025-09-10 12:31:49 +02:00
MacRimi 6aa5b58208 Update share_menu.sh 2025-09-10 12:17:03 +02:00
MacRimi 4430201cd2 Update lxc-mount-manager_minimal.sh 2025-09-10 12:16:25 +02:00
MacRimi 7c7963a83e Create lxc-mount-manager_minimal.sh 2025-09-10 11:10:02 +02:00
ProxMenuxBot e2202cd2d8 Update helpers_cache.json 2025-09-09 18:17:13 +00:00
MacRimi a931be83bc Update share-common.func 2025-09-09 19:20:15 +02:00
MacRimi 7350bea345 Update auto_post_install.sh 2025-09-09 19:16:13 +02:00
MacRimi 9b1e39dbb4 Update update-pve.sh 2025-09-09 19:14:27 +02:00
MacRimi 15cd118845 Update auto_post_install.sh 2025-09-09 19:06:33 +02:00
MacRimi d58dff047c Update auto_post_install.sh 2025-09-08 19:24:49 +02:00
MacRimi a2f83c896c Update common-functions.sh 2025-09-08 19:14:41 +02:00
MacRimi 6ef77c731c Update guia.md 2025-09-08 17:44:17 +02:00
MacRimi 29b0f61958 Update guia.md 2025-09-08 17:43:39 +02:00
MacRimi e944b2ecdd Update guia.md 2025-09-08 17:24:23 +02:00
MacRimi 41819c46a3 Update guia.md 2025-09-08 17:21:58 +02:00
MacRimi 13f391a6f0 Update guia.md 2025-09-08 17:14:52 +02:00
MacRimi 85a3d44f2c Update guia.md 2025-09-08 17:05:26 +02:00
MacRimi 0792392058 Update guia.md 2025-09-08 16:42:46 +02:00
MacRimi ff5083ada0 Update guia.md 2025-09-08 16:20:10 +02:00
MacRimi 62841677bc Update guia.md 2025-09-08 16:06:50 +02:00
MacRimi 1761cf53a2 Update guia.md 2025-09-08 15:27:32 +02:00
MacRimi a771efc5fa Update guia.md 2025-09-08 15:08:02 +02:00
MacRimi ed049da76a Update guia.md 2025-09-08 15:03:10 +02:00
MacRimi 5d1d357a2e Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-08 15:01:02 +02:00
MacRimi 30d0706a1c Create guia.md 2025-09-08 15:01:00 +02:00
ProxMenuxBot e9667e1266 Update helpers_cache.json 2025-09-08 12:27:18 +00:00
MacRimi 73109483e7 Update share-common.func 2025-09-08 11:29:02 +02:00
MacRimi a9c1acf204 Update lxc-mount-manager.sh 2025-09-08 11:15:52 +02:00
MacRimi 81c4f5814c Update samba_host.sh 2025-09-08 10:40:59 +02:00
MacRimi c595f6d781 Update nfs_host.sh 2025-09-08 10:36:57 +02:00
MacRimi 24bb6b1d3d Update lxc-mount-manager.sh 2025-09-07 19:10:23 +02:00
MacRimi 49eeb6020d Update install_proxmenux.sh 2025-09-07 17:24:29 +02:00
MacRimi 7c272bd2a2 Update install_proxmenux.sh 2025-09-07 17:22:47 +02:00
MacRimi cfbd865937 Update share-common.func 2025-09-07 09:29:43 +02:00
MacRimi fe472f33ef Update lxc-mount-manager.sh 2025-09-07 09:28:09 +02:00
MacRimi 8d6b3d650f update menu share 2025-09-07 09:19:53 +02:00
MacRimi 3b0d5b5eb7 Update nfs_lxc_server.sh 2025-09-07 09:16:22 +02:00
MacRimi 875e8a99bd Update samba_client.sh 2025-09-07 09:06:47 +02:00
MacRimi 6c19d81844 Update samba_client.sh 2025-09-07 09:05:23 +02:00
MacRimi ba535a931f Update nfs_client.sh 2025-09-07 09:04:13 +02:00
MacRimi 45dca5218d Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-07 09:00:39 +02:00
MacRimi da3cb9971b Update share-common.func 2025-09-07 09:00:37 +02:00
ProxMenuxBot b39270dc1e Update helpers_cache.json 2025-09-07 01:03:30 +00:00
MacRimi ae8a7d0de9 Update commands_share.sh 2025-09-06 23:59:23 +02:00
MacRimi 2d501415bf Update commands_share.sh 2025-09-06 23:50:57 +02:00
MacRimi da639ccaac Update commands_share.sh 2025-09-06 23:35:46 +02:00
MacRimi a352770e2d Update share_menu.sh 2025-09-06 23:20:59 +02:00
MacRimi e3e1899466 update menu share 2025-09-06 23:18:11 +02:00
MacRimi e67288e623 Update lxc-mount-manager.sh 2025-09-06 22:55:28 +02:00
MacRimi 4019e49b07 Update share-common.func 2025-09-06 22:51:57 +02:00
MacRimi cd8711f3bc Update lxc-mount-manager.sh 2025-09-06 22:50:30 +02:00
MacRimi 0d119379de Update lxc-mount-manager.sh 2025-09-06 22:46:52 +02:00
MacRimi aa2b6ff112 Update lxc-mount-manager.sh 2025-09-06 22:44:09 +02:00
MacRimi 3482f7dc98 Update share-common.func 2025-09-06 22:42:17 +02:00
MacRimi 16c321f114 Update lxc-mount-manager.sh 2025-09-06 22:33:19 +02:00
MacRimi a81e7f3c44 Update lxc-mount-manager.sh 2025-09-06 22:25:50 +02:00
MacRimi d7cc001521 Update lxc-mount-manager.sh 2025-09-06 22:20:34 +02:00
MacRimi eb11962231 Update share-common.func 2025-09-06 22:19:52 +02:00
MacRimi 9f73b8f159 Update lxc-mount-manager.sh 2025-09-06 22:17:08 +02:00
MacRimi 873a4abe24 Update share-common.func 2025-09-06 22:16:40 +02:00
MacRimi 56bc584f5e Update lxc-mount-manager.sh 2025-09-06 22:10:01 +02:00
MacRimi 2a9f2f3c2e Update lxc-mount-manager.sh 2025-09-06 22:09:11 +02:00
MacRimi ee719cdd39 Update share-common.func 2025-09-06 22:06:02 +02:00
MacRimi a571b57b30 Update share-common.func 2025-09-06 21:48:39 +02:00
MacRimi 5ee7a23bea Update share-common.func 2025-09-06 21:47:20 +02:00
MacRimi fe159ea195 Update share-common.func 2025-09-06 21:42:30 +02:00
MacRimi 8fcdf6176b Update share-common.func 2025-09-06 21:31:03 +02:00
MacRimi 715166bbca Update share-common.func 2025-09-06 21:22:40 +02:00
MacRimi 1d58072c70 Update share-common.func 2025-09-06 21:21:39 +02:00
MacRimi d667cde699 Update lxc-mount-manager.sh 2025-09-06 21:19:55 +02:00
MacRimi 4cd8889c38 Update share-common.func 2025-09-06 21:14:54 +02:00
MacRimi 93896f6fb7 Update share-common.func 2025-09-06 21:07:12 +02:00
MacRimi 3b3f0387bb Update share-common.func 2025-09-06 21:05:32 +02:00
MacRimi 2875c9af95 Update lxc-mount-manager.sh 2025-09-06 20:59:03 +02:00
MacRimi 93ef1bfccc update share menu 2025-09-06 20:57:04 +02:00
MacRimi a886af1d87 Update samba_client.sh 2025-09-06 20:48:37 +02:00
MacRimi d731ff3ae6 Update samba_host.sh 2025-09-06 20:40:45 +02:00
MacRimi d44864637d Update nfs_host_auto.sh 2025-09-06 20:37:23 +02:00
MacRimi 674ee34ec6 Update nfs_host_auto.sh 2025-09-06 20:30:04 +02:00
MacRimi a93eeda243 Update share-common.func 2025-09-06 19:56:24 +02:00
MacRimi 80fd92e2a1 Update share-common.func 2025-09-06 19:55:01 +02:00
MacRimi d4ff2da473 Update share-common.func 2025-09-06 19:16:15 +02:00
MacRimi 9b7b271580 update menu shared 2025-09-06 19:13:52 +02:00
MacRimi e1b340966a Update share-common.func 2025-09-06 18:56:10 +02:00
MacRimi 7ec4c331af Update nfs_client.sh 2025-09-06 18:39:35 +02:00
MacRimi 3102d596ee Update nfs_lxc_server.sh 2025-09-06 18:37:28 +02:00
MacRimi af56dc546e Update nfs_host_auto.sh 2025-09-06 18:26:13 +02:00
MacRimi 15d47499fa Update nfs_host_auto.sh 2025-09-06 18:22:02 +02:00
MacRimi 53a34d0470 update nfs menu 2025-09-06 18:20:20 +02:00
MacRimi 3ee675cefe Update nfs_lxc_server.sh 2025-09-06 18:08:48 +02:00
MacRimi d98c7bdc03 Update share_menu.sh 2025-09-06 16:48:07 +02:00
MacRimi bb4f1ebed6 Update share_menu.sh 2025-09-06 16:44:58 +02:00
MacRimi c8f73ea23b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-06 16:41:55 +02:00
MacRimi 8292b12787 Create nfs_lxc_server.sh 2025-09-06 16:41:54 +02:00
ProxMenuxBot 0f518e3c35 Update helpers_cache.json 2025-09-06 12:22:12 +00:00
MacRimi 1c2f67d43d Update lxc-mount-manager.sh 2025-09-06 11:55:23 +02:00
MacRimi a5560a3123 Update share-common.func 2025-09-06 11:50:48 +02:00
MacRimi 1332096360 Update share-common.func 2025-09-06 11:39:17 +02:00
MacRimi 80381a6375 Update share-common.func 2025-09-06 11:32:07 +02:00
MacRimi acf92bd005 Update share-common.func 2025-09-06 11:30:23 +02:00
MacRimi da4f8a3a19 Update share-common.func 2025-09-06 11:11:19 +02:00
MacRimi 3a332192e3 Update share-common.func 2025-09-06 11:08:23 +02:00
MacRimi 1fdb1d87cc Update share-common.func 2025-09-06 11:01:35 +02:00
MacRimi b99aa55d7a Update share-common.func 2025-09-06 10:46:22 +02:00
MacRimi de20da2dad Update lxc-mount-manager.sh 2025-09-06 10:10:37 +02:00
MacRimi 9444f0a68b Update nfs_host_auto.sh 2025-09-06 08:51:41 +02:00
MacRimi 48fd223a28 Update samba.sh 2025-09-04 20:38:11 +02:00
MacRimi 0845efe419 Update samba_client.sh 2025-09-04 20:37:18 +02:00
MacRimi 57b7ba91bc Update share_menu.sh 2025-09-04 20:34:43 +02:00
MacRimi 97af8a4892 Update share_menu.sh 2025-09-04 20:31:33 +02:00
MacRimi d6f237e289 Update share_menu.sh 2025-09-04 20:30:36 +02:00
MacRimi aba7109b35 Update samba_host.sh 2025-09-04 20:28:16 +02:00
MacRimi d3ec71052e Update samba_client.sh 2025-09-04 20:26:51 +02:00
MacRimi 1be63f396b Update nfs_client.sh 2025-09-04 20:26:26 +02:00
MacRimi 9308742146 Update samba_client.sh 2025-09-04 20:23:00 +02:00
MacRimi b32241082d Update share_menu.sh 2025-09-04 20:12:24 +02:00
MacRimi 1f8504d685 update shared 2025-09-04 20:04:08 +02:00
MacRimi 97c5c48150 Update nfs_host.sh 2025-09-03 23:13:21 +02:00
MacRimi afe84dc46a Update nfs_client.sh 2025-09-03 23:10:01 +02:00
MacRimi ffafd42f03 Update nfs.sh 2025-09-03 23:06:47 +02:00
MacRimi 7dca715c91 Update nfs.sh 2025-09-03 23:04:46 +02:00
MacRimi 7695e1d8dd Update nfs.sh 2025-09-03 22:55:45 +02:00
MacRimi 84b86d1db7 Update nfs.sh 2025-09-03 22:14:29 +02:00
MacRimi bae3ef6460 Update nfs.sh 2025-09-03 22:06:34 +02:00
MacRimi 97c6ec8875 Update share-common.func 2025-09-03 16:47:03 +02:00
MacRimi d33128dc26 Update share_menu.sh 2025-09-03 12:27:59 +02:00
MacRimi 10bdecabb6 Update share_menu.sh 2025-09-03 12:25:35 +02:00
MacRimi de88f530c8 Update share_menu.sh 2025-09-03 12:23:54 +02:00
MacRimi fb511b7596 Update share_menu.sh 2025-09-03 12:22:49 +02:00
MacRimi 322665ce91 Update share_menu.sh 2025-09-03 12:21:21 +02:00
MacRimi baeca1fcfb Update share-common.func 2025-09-03 11:35:38 +02:00
MacRimi 095b98c36a Update share-common.func 2025-09-03 11:28:37 +02:00
MacRimi 29bb7e7608 Update share-common.func 2025-09-03 11:16:34 +02:00
MacRimi e3d137efba Update share-common.func 2025-09-02 22:56:46 +02:00
MacRimi 207e915393 Update share-common.func 2025-09-02 22:45:33 +02:00
MacRimi 614e629a2b Update share-common.func 2025-09-02 22:44:42 +02:00
MacRimi f35de5c749 Update share-common.func 2025-09-02 21:34:13 +02:00
MacRimi c1623bd4df Update share-common.func 2025-09-02 21:23:57 +02:00
ProxMenuxBot 8690da5017 Update helpers_cache.json 2025-09-02 18:17:01 +00:00
MacRimi 696adcdc24 Update share-common.func 2025-09-02 18:48:57 +02:00
MacRimi 2756bd06c1 Update share-common.func 2025-09-02 18:48:20 +02:00
MacRimi 4893f6ea00 Update lxc-mount-manager.sh 2025-09-02 18:45:53 +02:00
MacRimi 35a7348197 Update lxc-mount-manager.sh 2025-09-02 18:45:04 +02:00
MacRimi cdd6333d0a Update share_menu.sh 2025-09-02 18:43:27 +02:00
MacRimi 54399b5b5d Update share-common.func 2025-09-02 18:25:39 +02:00
MacRimi f6b192cc1e Update lxc-mount-manager.sh 2025-09-02 18:22:34 +02:00
MacRimi cd231b90d8 Update share-common.func 2025-09-02 17:21:09 +02:00
ProxMenuxBot 87fe788358 Update helpers_cache.json 2025-09-02 12:26:27 +00:00
MacRimi 3e9bd21ea8 update share menu 2025-09-02 00:02:16 +02:00
MacRimi b6d4029797 Update local-shared-manager.sh 2025-09-01 19:10:46 +02:00
MacRimi ec65e96148 Update share_menu.sh 2025-09-01 19:08:48 +02:00
MacRimi 926f1f971f update share menu 2025-09-01 19:06:52 +02:00
MacRimi 5d69fad73f Update share_menu.sh 2025-09-01 18:45:14 +02:00
MacRimi a796761023 Update share-common.func 2025-09-01 17:39:45 +02:00
MacRimi 5d1338e485 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-01 17:21:41 +02:00
MacRimi ce25a167f1 Update auto_post_install.sh 2025-09-01 17:21:39 +02:00
MacRimi 1c44969580 Update share_menu.sh 2025-09-01 14:43:46 +02:00
MacRimi b6e04e3ede Update share-common.func 2025-09-01 14:30:16 +02:00
ProxMenuxBot 84c26be703 Update helpers_cache.json 2025-09-01 12:26:47 +00:00
MacRimi d201160722 Update share-common.func 2025-09-01 14:08:10 +02:00
MacRimi e112361b43 Update share-common.func 2025-09-01 13:53:54 +02:00
MacRimi 3e69795c9d Update lxc-mount-manager.sh 2025-09-01 13:33:48 +02:00
MacRimi b11baf2e5d Update auto_post_install.sh 2025-09-01 12:42:46 +02:00
MacRimi 233770b553 Update auto_post_install.sh 2025-09-01 12:41:26 +02:00
MacRimi 187db73798 Update zimaos.sh 2025-09-01 12:20:45 +02:00
MacRimi 0e3fc6f682 Update zimaos.sh 2025-09-01 12:15:30 +02:00
MacRimi d11e3a4ac4 Update auto_post_install.sh 2025-08-31 23:45:37 +02:00
MacRimi d3b4ca3e66 Update customizable_post_install.sh 2025-08-31 21:46:28 +02:00
MacRimi f37fbbfb8b Update menu share 2025-08-30 18:56:49 +02:00
MacRimi 52b7aac424 Update share-common.func 2025-08-30 18:05:02 +02:00
MacRimi d42f3f8f0c Update share-common.func 2025-08-30 18:02:20 +02:00
MacRimi 91b5c7c9bc Update share-common.func 2025-08-30 17:51:45 +02:00
MacRimi 48feebc092 Update share-common.func 2025-08-30 16:59:46 +02:00
MacRimi 14e2d66d96 Update share-common.func 2025-08-30 11:43:02 +02:00
MacRimi 10d844a195 Update share-common.func 2025-08-30 11:36:52 +02:00
MacRimi bbf91ae5d6 Update main_menu.sh 2025-08-30 09:40:42 +02:00
ProxMenuxBot cb82eda49a Update helpers_cache.json 2025-08-30 00:57:37 +00:00
MacRimi bc1dbb1c27 Update help_info_menu.sh 2025-08-30 00:17:51 +02:00
ProxMenuxBot 9496a7f1ce Update helpers_cache.json 2025-08-28 18:19:20 +00:00
ProxMenuxBot 7241fa31b4 Update helpers_cache.json 2025-08-28 12:26:37 +00:00
MacRimi fed7216436 Update share-common.func 2025-08-27 18:15:26 +02:00
MacRimi ffe7d7c4c6 Create group_manager.sh 2025-08-27 10:54:03 +02:00
MacRimi f430ac8d6c Update upgrade_pve8_to_pve9.sh 2025-08-26 19:40:38 +02:00
MacRimi 70dfd7c9a3 Update upgrade_pve8_to_pve9.sh 2025-08-26 19:25:59 +02:00
MacRimi ed3140932b Update upgrade_pve8_to_pve9.sh 2025-08-26 19:24:41 +02:00
MacRimi 3cd2bd6ce8 Update share-common.func 2025-08-26 17:46:48 +02:00
MacRimi 982bf45fc4 Update share-common.func 2025-08-26 17:37:09 +02:00
MacRimi aaba8569fc Update share-common.func 2025-08-26 17:19:11 +02:00
MacRimi 4111e15eb9 Update share-common.func 2025-08-26 17:15:47 +02:00
MacRimi 2012478f26 Update share-common.func 2025-08-26 17:05:17 +02:00
MacRimi 88869d3239 Update share-common.func 2025-08-26 17:02:24 +02:00
ProxMenuxBot f3c2549b18 Update helpers_cache.json 2025-08-26 12:28:24 +00:00
MacRimi 57e3b839d0 Update share-common.func 2025-08-26 13:56:00 +02:00
MacRimi faf3f43413 Create share-common.func 2025-08-26 13:26:22 +02:00
ProxMenuxBot 52e5bb3386 Update helpers_cache.json 2025-08-26 01:02:34 +00:00
ProxMenuxBot 89405f6670 Update helpers_cache.json 2025-08-25 18:20:02 +00:00
ProxMenuxBot 73111c4139 Update helpers_cache.json 2025-08-25 12:27:14 +00:00
ProxMenuxBot 04e9c5db8c Update helpers_cache.json 2025-08-24 12:24:09 +00:00
MacRimi 69278902de Update customizable_post_install.sh 2025-08-24 10:55:25 +02:00
MacRimi efa95b0858 Update customizable_post_install.sh 2025-08-24 10:29:00 +02:00
MacRimi 660128cd5c Update customizable_post_install.sh 2025-08-24 10:28:14 +02:00
MacRimi ef1e052e47 Update customizable_post_install.sh 2025-08-24 10:06:20 +02:00
ProxMenuxBot 0b346bc343 Update helpers_cache.json 2025-08-24 06:19:28 +00:00
MacRimi 2272eaf833 Update lxc-unprivileged-to-privileged.sh 2025-08-22 19:08:03 +02:00
MacRimi 4adee98bce new menu lxc 2025-08-22 19:05:36 +02:00
MacRimi cbdb2c0705 Rename lxc-manual-guide.sh to lxc-manual-guide.sh 2025-08-22 19:04:10 +02:00
MacRimi 4f438aabbf update manual lxc guide 2025-08-22 19:03:08 +02:00
MacRimi b6ccc06963 Update lxc_menu.sh 2025-08-22 18:58:10 +02:00
MacRimi 5b89a15bfc menu lxc 2025-08-22 18:57:00 +02:00
MacRimi 5596ae551d Update storage_menu.sh 2025-08-22 18:34:12 +02:00
MacRimi 1360df592a Create backup_host4.sh 2025-08-22 18:17:01 +02:00
MacRimi 13684ff83c Update share_menu.sh 2025-08-22 18:10:34 +02:00
MacRimi ae88f7870e Update share_menu.sh 2025-08-22 18:08:20 +02:00
MacRimi 810b6da60c Share menu 2025-08-22 18:05:14 +02:00
ProxMenuxBot 7bdf3e08f9 Update helpers_cache.json 2025-08-21 18:19:22 +00:00
MacRimi fdad2a087f Update zimaos.sh 2025-08-21 19:39:50 +02:00
MacRimi c437a8c426 Update zimaos.sh 2025-08-21 19:36:28 +02:00
MacRimi ef861e6d1d Update zimaos.sh 2025-08-21 19:02:58 +02:00
MacRimi 928a008688 Update zimaos.sh 2025-08-21 18:59:54 +02:00
MacRimi 638a124adb Update zimaos.sh 2025-08-21 18:31:32 +02:00
MacRimi c2a63ae9bb Update zimaos.sh 2025-08-21 18:30:43 +02:00
MacRimi 28cf31e6e7 Update zimaos.sh 2025-08-21 18:27:30 +02:00
MacRimi 3cf416167d Update select_nas_iso.sh 2025-08-21 18:22:57 +02:00
MacRimi ebf03923a0 Update select_nas_iso.sh 2025-08-21 18:14:58 +02:00
MacRimi 82797d2421 Update select_nas_iso.sh 2025-08-21 18:05:45 +02:00
MacRimi 52b6be946c Create zimaos.sh 2025-08-21 18:00:58 +02:00
MacRimi dc46724d7b Update select_linux_iso.sh 2025-08-20 23:02:38 +02:00
MacRimi ed7d43b6a9 Update select_linux_iso.sh 2025-08-20 22:58:14 +02:00
MacRimi 6f3fc51278 Update system_utils.sh 2025-08-20 22:40:42 +02:00
MacRimi a446acc282 Update customizable_post_install.sh 2025-08-20 22:39:28 +02:00
MacRimi d987d639ab Update version.txt 2025-08-20 21:38:45 +02:00
MacRimi e7e180e468 Update CHANGELOG.md 2025-08-20 21:38:20 +02:00
MacRimi 76770f82cd Update system_utils.sh 2025-08-20 21:22:49 +02:00
MacRimi 4079d4fd7c Update customizable_post_install.sh 2025-08-20 21:21:13 +02:00
MacRimi ac48178369 Update lxc-privileged-to-unprivileged.sh 2025-08-20 21:10:05 +02:00
MacRimi c2e9f038ee Update lxc-privileged-to-unprivileged.sh 2025-08-20 21:04:50 +02:00
MacRimi 70220d9829 Update lxc-privileged-to-unprivileged.sh 2025-08-20 21:03:57 +02:00
MacRimi b9a1f378ec Update lxc_menu.sh 2025-08-20 20:59:58 +02:00
MacRimi f6bc090a98 Update lxc_menu.sh 2025-08-20 20:55:43 +02:00
MacRimi be519f3932 Create lxc_menu.sh 2025-08-20 20:46:51 +02:00
MacRimi 0a46f77555 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-20 20:43:56 +02:00
MacRimi 0e6cc0c7e5 Create lxc-privileged-to-unprivileged.sh 2025-08-20 20:43:54 +02:00
ProxMenuxBot 11cd425162 Update helpers_cache.json 2025-08-20 18:19:44 +00:00
MacRimi aa269688d6 Update utilities_menu.sh 2025-08-20 19:51:54 +02:00
MacRimi 4c9e94768e Update upgrade_pve8_to_pve9.sh 2025-08-20 19:48:25 +02:00
MacRimi 581157fa82 Update upgrade_pve8_to_pve9.sh 2025-08-20 19:46:30 +02:00
MacRimi e748e479cc Update utilities_menu.sh 2025-08-20 19:44:16 +02:00
MacRimi 5c9e4eea1e Update upgrade_pve8_to_pve9.sh 2025-08-20 19:41:09 +02:00
MacRimi 0c1189b233 Update pve8to9_check.sh 2025-08-20 19:37:14 +02:00
MacRimi 5ec9b82b4a Update upgrade_pve8_to_pve9.sh 2025-08-20 19:22:22 +02:00
MacRimi c84ec533da Create pve8to9_check.sh 2025-08-20 19:22:12 +02:00
MacRimi fb80c6ad7a Update upgrade_pve8_to_pve9.sh 2025-08-20 19:09:52 +02:00
ProxMenuxBot 2e3bfff6a4 Update helpers_cache.json 2025-08-19 18:18:30 +00:00
ProxMenuxBot e96ce30891 Update helpers_cache.json 2025-08-19 12:26:48 +00:00
MacRimi 10de5b2e5f Update customizable_post_install.sh 2025-08-19 14:15:34 +02:00
MacRimi 1966081239 Update auto_post_install.sh 2025-08-19 14:13:56 +02:00
MacRimi b48d806d53 Update system_utils.sh 2025-08-19 13:42:42 +02:00
MacRimi 97784d74e7 Update customizable_post_install.sh 2025-08-19 13:38:59 +02:00
ProxMenuxBot c42e92b07d Update helpers_cache.json 2025-08-19 01:03:35 +00:00
MacRimi 2c52943b54 Update customizable_post_install.sh 2025-08-18 23:36:30 +02:00
MacRimi 4ccb1902cb Update customizable_post_install.sh 2025-08-18 23:30:59 +02:00
MacRimi 349b0572cd Update customizable_post_install.sh 2025-08-18 23:23:35 +02:00
MacRimi 87fae8a9eb Update customizable_post_install.sh 2025-08-18 23:18:26 +02:00
ProxMenuxBot a77a097f47 Update helpers_cache.json 2025-08-18 18:20:28 +00:00
MacRimi a84d81143e Update auto_post_install.sh 2025-08-18 15:10:23 +02:00
MacRimi d9cee50ef3 Update auto_post_install.sh 2025-08-18 15:08:56 +02:00
MacRimi 0fc414e5e9 Update customizable_post_install.sh 2025-08-18 15:08:25 +02:00
ProxMenuxBot e18f20ce4c Update helpers_cache.json 2025-08-18 12:29:04 +00:00
MacRimi c12af4060c Update system_utils.sh 2025-08-18 14:20:47 +02:00
MacRimi 9992ea0dee Update customizable_post_install.sh 2025-08-18 14:15:19 +02:00
MacRimi b8310f1c5d Update main_menu.sh 2025-08-18 09:37:06 +02:00
MacRimi 78f66af702 Update auto_post_install.sh 2025-08-17 15:16:25 +02:00
MacRimi 4d7564094e Update auto_post_install.sh 2025-08-17 15:14:46 +02:00
MacRimi 370f4694d1 Update customizable_post_install.sh 2025-08-17 15:14:20 +02:00
MacRimi fc7c740691 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-17 15:05:42 +02:00
MacRimi 8e1f955519 Update auto_post_install.sh 2025-08-17 15:04:27 +02:00
MacRimi aa3d16d981 Update auto_post_install.sh 2025-08-17 15:02:42 +02:00
MacRimi 5447f0e4df Update auto_post_install.sh 2025-08-17 15:00:06 +02:00
MacRimi 97294df208 Update customizable_post_install.sh 2025-08-17 14:21:37 +02:00
MacRimi c6f53629da Update auto_post_install.sh 2025-08-17 14:20:58 +02:00
MacRimi fcba907658 Update upgrade_pve8_to_pve9.sh 2025-08-17 13:24:32 +02:00
MacRimi f481df7b8d Update configure_igpu_lxc.sh 2025-08-17 13:08:39 +02:00
MacRimi 81079a35d9 Update uninstall-tools.sh 2025-08-17 11:23:13 +02:00
MacRimi bda344c382 Update uninstall-tools.sh 2025-08-17 11:01:21 +02:00
MacRimi c4ebc396af Update customizable_post_install.sh 2025-08-17 10:36:03 +02:00
MacRimi 4cdbf1231b Update customizable_post_install.sh 2025-08-16 18:32:19 +02:00
MacRimi 92db58a9f6 Update auto_post_install.sh 2025-08-16 18:31:10 +02:00
MacRimi 5f07f47308 Update configure_igpu_lxc.sh 2025-08-16 18:12:14 +02:00
MacRimi 2132ae79a6 Update configure_igpu_lxc.sh 2025-08-16 17:34:34 +02:00
MacRimi bda7834a4f Update disk-passthrough_ct.sh 2025-08-16 17:22:33 +02:00
MacRimi 7693f313c4 Update disk-passthrough_ct.sh 2025-08-16 17:13:45 +02:00
MacRimi d2200a64e0 Update upgrade_pve8_to_pve9.sh 2025-08-16 17:04:59 +02:00
MacRimi a5c46ab837 Update upgrade_pve8_to_pve9.sh 2025-08-16 16:43:44 +02:00
MacRimi a389282e23 Update upgrade_pve8_to_pve9.sh 2025-08-16 11:54:11 +02:00
MacRimi 1f90b5b739 Update upgrade_pve8_to_pve9.sh 2025-08-16 11:46:09 +02:00
MacRimi 2c59500046 Update configure_igpu_lxc.sh 2025-08-16 11:15:41 +02:00
MacRimi a59a056e12 Update configure_igpu_lxc.sh 2025-08-16 11:11:43 +02:00
ProxMenuxBot 235364013b Update helpers_cache.json 2025-08-16 01:03:53 +00:00
MacRimi 1049ac6eac Update upgrade_pve8_to_pve9.sh 2025-08-15 12:15:42 +02:00
MacRimi 04dc7af25c Create remove-banner-pve9.sh 2025-08-15 12:06:54 +02:00
MacRimi f62ea3ad04 update remove-banner-pve9.sh 2025-08-15 12:05:24 +02:00
MacRimi 14c75f2cd9 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-15 11:15:17 +02:00
MacRimi 9ae68b9653 Update customizable_post_install.sh 2025-08-15 10:22:57 +02:00
MacRimi b7ab4c4568 Update customizable_post_install.sh 2025-08-15 10:13:44 +02:00
MacRimi 7d0b3a0c87 Update system_utils.sh 2025-08-15 09:55:22 +02:00
MacRimi 0d38f7f290 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-15 09:54:02 +02:00
MacRimi 12e5ef4231 Update customizable_post_install.sh 2025-08-15 09:54:00 +02:00
ProxMenuxBot f3aa1f7414 Update helpers_cache.json 2025-08-15 01:06:47 +00:00
MacRimi f2eaec6e02 Update upgrade_pve8_to_pve9.sh 2025-08-14 20:08:22 +02:00
MacRimi 0654a3ed55 Update upgrade_pve8_to_pve9.sh 2025-08-14 20:03:47 +02:00
MacRimi 9a27138d96 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-14 19:46:13 +02:00
MacRimi b3c9f71c02 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-14 19:41:22 +02:00
MacRimi e9c9b957db Delete upgrade_pve8_topve9.sh 2025-08-14 19:39:36 +02:00
MacRimi 29cdf6fa48 Create upgrade_pve8_to_pve9.sh 2025-08-14 19:39:12 +02:00
MacRimi 8466a8e21e Update upgrade_pve8_topve9.sh 2025-08-14 19:37:15 +02:00
MacRimi 1523b6b8a8 Update proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-14 19:34:42 +02:00
MacRimi 33205e1008 Create upgrade_pve8_topve9.sh 2025-08-14 19:24:29 +02:00
MacRimi 537af385f8 Update main menu PVE 9 2025-08-14 17:58:59 +02:00
MacRimi 7259b0a850 Update install_proxmenux.sh 2025-08-14 17:52:57 +02:00
MacRimi 11fbfda6bf Update cache.json 2025-08-13 22:46:58 +02:00
MacRimi 4f0353d0fb Create proxmox-upgrade-pve8-to-pve9-manual-guide.sh 2025-08-13 22:46:38 +02:00
MacRimi a605d68d73 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-13 20:29:26 +02:00
MacRimi 237b7fbf1b Update cache.json 2025-08-13 20:29:24 +02:00
ProxMenuxBot 4a7e21f6b4 Update helpers_cache.json 2025-08-13 18:19:42 +00:00
MacRimi b7017573b8 Update update-pve8.sh 2025-08-13 19:45:31 +02:00
MacRimi a98b087c5d Update log2RAM 2025-08-13 15:55:22 +02:00
MacRimi 161c840136 Update utils.sh 2025-08-11 08:57:26 +02:00
MacRimi 4dd2abc202 Update utils.sh 2025-08-10 21:56:07 +02:00
MacRimi cc0e9f61a7 Update utils.sh 2025-08-10 21:43:05 +02:00
MacRimi 21a658f1f4 Update utils.sh 2025-08-10 21:23:19 +02:00
MacRimi b99f391c2a Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-10 21:09:25 +02:00
MacRimi 9abe25b91a Update README.md 2025-08-10 21:09:01 +02:00
ProxMenuxBot 2531fc6dac Update helpers_cache.json 2025-08-09 12:25:42 +00:00
ProxMenuxBot e5551cb179 Update helpers_cache.json 2025-08-09 01:05:21 +00:00
MacRimi 4728b7a8b7 Update utils.sh 2025-08-08 18:26:32 +02:00
ProxMenuxBot 2863921e15 Update helpers_cache.json 2025-08-08 12:29:54 +00:00
ProxMenuxBot b93668edfe Update helpers_cache.json 2025-08-08 06:23:01 +00:00
ProxMenuxBot 8ae91b8c31 Update helpers_cache.json 2025-08-07 18:21:56 +00:00
MacRimi 894a23d701 Update common-functions.sh 2025-08-07 20:10:36 +02:00
MacRimi 3598906cbd Update common-functions.sh 2025-08-07 19:37:28 +02:00
MacRimi 75c12e2d4b update common funtions 2025-08-07 19:24:39 +02:00
MacRimi d6e0519a3d Update common-functions.sh 2025-08-07 19:08:44 +02:00
MacRimi 41efc71626 Update update-pve.sh 2025-08-07 19:05:52 +02:00
MacRimi 6e3d97f472 Update common-functions.sh 2025-08-07 19:05:23 +02:00
MacRimi 9d58d02522 Update proxmox_update.sh 2025-08-07 18:11:32 +02:00
MacRimi fe86396b21 Update proxmox_update.sh 2025-08-07 18:08:37 +02:00
MacRimi 97994f7632 Update update-pve.sh 2025-08-07 17:58:24 +02:00
MacRimi 33d63457b3 Update update-pve8.sh 2025-08-07 17:58:00 +02:00
MacRimi ed36d9e953 Update proxmox_update.sh 2025-08-07 15:49:22 +02:00
MacRimi 9a478d74d2 Update proxmox_update.sh 2025-08-07 15:45:46 +02:00
MacRimi 72d72544a4 Update update-pve.sh 2025-08-07 15:31:03 +02:00
MacRimi 4bbbe81182 Update update-pve.sh 2025-08-07 15:05:54 +02:00
MacRimi a0af0c2492 Update update-pve.sh 2025-08-07 14:47:58 +02:00
MacRimi ce7d3e4702 Update update-pve.sh 2025-08-07 14:30:48 +02:00
MacRimi 1bb4ca8541 Update common-functions.sh 2025-08-07 14:23:33 +02:00
MacRimi ea65445772 Update update-pve.sh 2025-08-07 14:02:02 +02:00
MacRimi 972db8fcea Update update-pve8.sh 2025-08-07 13:04:05 +02:00
MacRimi a3c12631f0 Update auto_post_install.sh 2025-08-07 11:16:24 +02:00
MacRimi 3cadfd08d8 Update install_proxmenux.sh 2025-08-07 08:04:04 +02:00
MacRimi 104f3de013 Update version.txt 2025-08-06 23:10:08 +02:00
MacRimi 713b41bd52 Update CHANGELOG.md 2025-08-06 23:09:36 +02:00
MacRimi 253093fa2f Update update-pve.sh 2025-08-06 23:08:53 +02:00
MacRimi f36af5af64 Update update-pve.sh 2025-08-06 22:37:13 +02:00
MacRimi 97b6c0e44d Delete common-functions_.sh 2025-08-06 22:14:12 +02:00
MacRimi c4f6dabd4d Update system_utils.sh 2025-08-06 22:02:40 +02:00
MacRimi d1c8aeb25d update pve 8 2025-08-06 22:01:33 +02:00
MacRimi 6e1cb2e0fe Update common-functions.sh 2025-08-06 21:47:53 +02:00
MacRimi da9762f60e Update update-pve.sh 2025-08-06 21:45:09 +02:00
MacRimi 27affdec14 Update common-functions.sh 2025-08-06 21:17:06 +02:00
MacRimi 433a19e46a Update customizable_post_install.sh 2025-08-06 20:10:38 +02:00
MacRimi da9db9d3d1 Update update-pve.sh 2025-08-06 20:09:01 +02:00
MacRimi ed4b0eba2f Update auto_post_install.sh 2025-08-06 20:04:52 +02:00
MacRimi 615aecf80f Update common funtions 2025-08-06 20:04:12 +02:00
MacRimi 4622d1a610 Update update-pve8.sh 2025-08-06 19:31:15 +02:00
MacRimi f1b80d8f57 Update customizable_post_install.sh 2025-08-06 19:22:11 +02:00
MacRimi e76e303383 Update update-pve8.sh 2025-08-06 19:20:03 +02:00
MacRimi 97133c3fcb Update auto_post_install.sh 2025-08-06 19:17:45 +02:00
MacRimi 450610b6e6 Update update-pve8.sh 2025-08-06 19:14:32 +02:00
MacRimi 4dc3fd92cc Update update-pve8.sh 2025-08-06 19:04:32 +02:00
MacRimi f4b5e7c044 Create update-pve8.sh 2025-08-06 18:47:41 +02:00
MacRimi 6c0b2a468d Update common-functions.sh 2025-08-06 18:47:16 +02:00
MacRimi e33f724f1b Create common-functions.sh 2025-08-06 18:17:51 +02:00
MacRimi b0d5562917 Update remove-banner-pve8.sh 2025-08-06 17:45:22 +02:00
MacRimi eecf7a2194 Update remove subscription banner PVE 8.4.9 2025-08-06 17:17:00 +02:00
MacRimi 54fd8a0332 Update remove-banner-pve8.sh 2025-08-06 17:12:16 +02:00
MacRimi b6ca91980b global update 2025-08-06 16:50:16 +02:00
MacRimi 6af7e2d749 Update remove-banner-pve8.sh 2025-08-06 15:27:26 +02:00
MacRimi 86d334c204 Update remove-banner-pve9.sh 2025-08-06 15:21:31 +02:00
MacRimi 585a4fa449 Update remove-banner-pve9.sh 2025-08-06 15:19:56 +02:00
MacRimi 7438073e7e Update remove-banner-pve8.sh 2025-08-06 15:19:33 +02:00
MacRimi 6e808ae35a Update remove-banner-pve8.sh 2025-08-06 14:32:14 +02:00
MacRimi 99ec64e852 Create remove-banner-pve9.sh 2025-08-06 14:31:19 +02:00
MacRimi eeac63c0a5 Create remove-banner-pve8.sh 2025-08-06 14:23:22 +02:00
MacRimi 5d5a3c3301 update funtions to pve9 2025-08-05 20:30:29 +02:00
ProxMenuxBot 31e9730236 Update helpers_cache.json 2025-08-05 12:31:00 +00:00
MacRimi 69b32a02ff Update utils.sh 2025-08-05 09:46:30 +02:00
ProxMenuxBot a222df8176 Update helpers_cache.json 2025-08-04 12:30:37 +00:00
MacRimi 7f4c99be60 Update customizable_post_install.sh 2025-08-04 11:29:43 +02:00
MacRimi ccff657a62 Update auto_post_install.sh 2025-08-04 11:29:28 +02:00
MacRimi fb258499e1 Update customizable_post_install.sh 2025-08-04 11:22:10 +02:00
MacRimi 79c6d6c742 Update customizable_post_install.sh 2025-08-04 11:20:38 +02:00
MacRimi 80d9d5480c Update customizable_post_install.sh 2025-08-04 11:19:41 +02:00
MacRimi 958a553922 Update auto_post_install.sh 2025-08-04 11:19:27 +02:00
MacRimi a44bbc3513 Update customizable_post_install.sh 2025-08-04 11:16:52 +02:00
MacRimi d7f2f4a3e7 Update customizable_post_install.sh 2025-08-04 11:15:59 +02:00
MacRimi 073566a23e Update auto_post_install.sh 2025-08-04 11:11:14 +02:00
MacRimi 590aecfcf1 Update customizable_post_install.sh 2025-08-04 11:11:00 +02:00
MacRimi 77ab52310e Update auto_post_install.sh 2025-08-04 11:03:31 +02:00
MacRimi 2c2ccddbe4 Update customizable_post_install.sh 2025-08-04 11:02:23 +02:00
MacRimi 87062db9d5 Update customizable_post_install.sh 2025-08-04 10:44:58 +02:00
MacRimi b74701dbc5 Update system_utils.sh 2025-08-04 08:52:55 +02:00
MacRimi a88db8830b Update customizable_post_install.sh 2025-08-04 08:34:40 +02:00
ProxMenuxBot 36cd83c796 Update helpers_cache.json 2025-08-04 01:16:40 +00:00
MacRimi a039c93c05 Update customizable_post_install.sh 2025-08-03 23:13:36 +02:00
MacRimi 57b4ade3be Update auto_post_install.sh 2025-08-03 23:12:32 +02:00
MacRimi 87ce6cfa98 Update auto_post_install.sh 2025-08-03 23:08:06 +02:00
MacRimi 6a99c8c81d Update customizable_post_install.sh 2025-08-03 23:06:31 +02:00
ProxMenuxBot 46e8188d5a Update helpers_cache.json 2025-08-02 18:19:25 +00:00
MacRimi 3c990df1fe Actualizar main_menu.sh 2025-08-02 12:02:10 +02:00
ProxMenuxBot 8969bc5aa6 Update helpers_cache.json 2025-08-02 01:07:27 +00:00
MacRimi 45e8ca8d42 Update customizable_post_install.sh 2025-08-01 08:28:12 +02:00
MacRimi 39930153c9 Update customizable_post_install.sh 2025-08-01 07:25:44 +02:00
MacRimi 5890e46db3 Update auto_post_install.sh 2025-08-01 06:57:21 +02:00
MacRimi c5c06a08ba Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-08-01 06:50:09 +02:00
MacRimi 2a0e677a89 Update proxmox_update.sh 2025-08-01 06:50:07 +02:00
ProxMenuxBot 8f7a968dc9 Update helpers_cache.json 2025-07-31 18:21:21 +00:00
MacRimi 20cfc50448 Update select_linux_iso.sh 2025-07-31 19:39:03 +02:00
MacRimi 4bf019ec7e Update jd2.sh 2025-07-31 19:11:18 +02:00
MacRimi 57fe45484c Update jd2.sh 2025-07-31 18:00:30 +02:00
MacRimi 7744f4ed76 Update jd2.sh 2025-07-31 17:43:14 +02:00
MacRimi a43e81e229 Update jd2.sh 2025-07-31 15:14:00 +02:00
MacRimi f6ad7e250b Update jd2.sh 2025-07-31 14:45:02 +02:00
MacRimi 0f2b0482ec Create jd2.sh 2025-07-31 14:27:12 +02:00
MacRimi 21d850d39e Rename jd2.sh to jd2_.sh 2025-07-31 14:26:46 +02:00
MacRimi d3f2e42301 Create jd2.sh 2025-07-31 12:50:19 +02:00
MacRimi df68154f10 Update disk-passthrough_ct.sh 2025-07-30 18:43:54 +02:00
MacRimi f8ebf03afd Update system_utils.sh 2025-07-30 18:18:13 +02:00
MacRimi 23f8b97319 Update customizable_post_install.sh 2025-07-30 17:59:12 +02:00
MacRimi d712054353 Update customizable_post_install.sh 2025-07-30 17:55:51 +02:00
MacRimi 58da896b14 Update system_utils.sh 2025-07-30 17:54:15 +02:00
MacRimi 8db57bda6e Update install utilities 2025-07-30 17:34:44 +02:00
MacRimi 53f29ec710 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-29 22:22:38 +02:00
MacRimi 43a8fc0e86 Update customizable_post_install.sh 2025-07-29 22:22:36 +02:00
ProxMenuxBot 7f2adb068e Update helpers_cache.json 2025-07-29 18:21:40 +00:00
MacRimi 218ae9f9bf Update uninstall-tools.sh 2025-07-29 20:15:55 +02:00
MacRimi 350c03874d Update customizable_post_install.sh 2025-07-29 20:09:54 +02:00
MacRimi 575c0e5bf9 Update customizable_post_install.sh 2025-07-29 19:58:49 +02:00
ProxMenuxBot 3a890ba2c7 Update helpers_cache.json 2025-07-29 12:30:07 +00:00
ProxMenuxBot 2a345f4869 Update helpers_cache.json 2025-07-28 12:30:05 +00:00
ProxMenuxBot 658ebbd84d Update helpers_cache.json 2025-07-27 12:26:49 +00:00
ProxMenuxBot 0c54ade367 Update helpers_cache.json 2025-07-26 17:08:21 +00:00
ProxMenuxBot f16ba64026 Update helpers_cache.json 2025-07-25 12:28:04 +00:00
ProxMenuxBot d72127aaeb Update helpers_cache.json 2025-07-25 01:09:29 +00:00
ProxMenuxBot 40e0b1291c Update helpers_cache.json 2025-07-24 18:20:25 +00:00
MacRimi 0fae7f0166 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-22 22:34:12 +02:00
MacRimi 6f916a4c32 Update customizable_post_install.sh 2025-07-22 22:33:58 +02:00
ProxMenuxBot 41979d5389 Update helpers_cache.json 2025-07-22 18:20:56 +00:00
MacRimi 0d51205bd6 Update customizable_post_install.sh 2025-07-22 19:10:35 +02:00
MacRimi 416dd52a30 Update fastfetch 2025-07-22 19:03:41 +02:00
MacRimi 850e45b9a5 Update uninstall-tools.sh 2025-07-22 18:54:14 +02:00
MacRimi 040d2ca7f6 Update customizable_post_install.sh 2025-07-22 18:39:48 +02:00
ProxMenuxBot e173072622 Update helpers_cache.json 2025-07-22 12:29:23 +00:00
MacRimi 991dd80382 Update select_linux_iso.sh 2025-07-22 12:02:38 +02:00
MacRimi 5fc5d02134 Update auto_post_install.sh 2025-07-22 09:27:40 +02:00
MacRimi 0f51256add Update auto_post_install.sh 2025-07-22 09:22:59 +02:00
ProxMenuxBot a78860dbc4 Update helpers_cache.json 2025-07-21 18:20:56 +00:00
MacRimi f655a3c52d Update auto_post_install.sh 2025-07-20 20:03:12 +02:00
MacRimi b69aebd5be update post-install menu 2025-07-20 19:36:59 +02:00
MacRimi 769a7c391f Update customizable_post_install.sh 2025-07-20 14:18:06 +02:00
MacRimi 9a4d55aa36 Update customizable_post_install.sh 2025-07-20 12:34:21 +02:00
MacRimi e776acfbab Update CHANGELOG.md 2025-07-20 12:08:37 +02:00
MacRimi b4f58286b4 Update network_menu.sh 2025-07-20 11:50:04 +02:00
MacRimi 59779cc931 Update network_menu.sh 2025-07-20 11:46:26 +02:00
MacRimi 567bcecc80 Update network_menu.sh 2025-07-20 11:42:25 +02:00
MacRimi 0ca87dc29b Update network_menu.sh 2025-07-20 11:38:54 +02:00
MacRimi 6e0824e357 Update customizable_post_install.sh 2025-07-20 11:27:08 +02:00
MacRimi 9a8a620658 Update uninstall-tools.sh 2025-07-20 11:17:33 +02:00
MacRimi eb1db3120d Update auto_post_install.sh 2025-07-20 10:55:04 +02:00
MacRimi 98a9225c32 Update customizable_post_install.sh 2025-07-20 02:27:46 +02:00
MacRimi 1b8fb766a8 Update auto_post_install.sh 2025-07-20 02:26:26 +02:00
MacRimi c28ef3ec3b Update auto_post_install.sh 2025-07-20 02:12:27 +02:00
MacRimi fab3f0630c Update customizable_post_install.sh 2025-07-20 02:09:11 +02:00
MacRimi d2499ad157 Update customizable_post_install.sh 2025-07-20 01:55:47 +02:00
MacRimi 724a37bbf4 Update auto_post_install.sh 2025-07-20 01:51:14 +02:00
MacRimi c5f1c30b1c Update disk-passthrough_ct.sh 2025-07-19 18:28:28 +02:00
MacRimi e90363df71 Update customizable_post_install.sh 2025-07-19 17:43:31 +02:00
MacRimi 0932008619 Update auto_post_install.sh 2025-07-19 17:40:52 +02:00
MacRimi a24e00ad5a Update auto_post_install.sh 2025-07-19 17:25:43 +02:00
MacRimi fab938055f Update auto_post_install.sh 2025-07-19 17:19:06 +02:00
MacRimi b2026b0dac Update auto_post_install.sh 2025-07-19 17:06:33 +02:00
MacRimi c1c742084e Update auto_post_install.sh 2025-07-19 17:02:38 +02:00
MacRimi 7140f590cf Update uninstall-tools.sh 2025-07-19 16:52:13 +02:00
MacRimi dcc26ad666 Update customizable_post_install.sh 2025-07-19 16:43:14 +02:00
MacRimi 70f1ecad49 Update customizable_post_install.sh 2025-07-19 16:38:55 +02:00
ProxMenuxBot 03c7383b54 Update helpers_cache.json 2025-07-17 18:20:17 +00:00
ProxMenuxBot d3734971cc Update helpers_cache.json 2025-07-14 18:20:49 +00:00
ProxMenuxBot cff8358b3e Update helpers_cache.json 2025-07-14 12:28:40 +00:00
ProxMenuxBot 42d691c4ce Update helpers_cache.json 2025-07-13 18:18:07 +00:00
MacRimi 33dcbe8b5a Update install_coral_lxc.sh 2025-07-12 18:34:49 +02:00
MacRimi 980c7a4390 Update uupdump_creator.sh 2025-07-11 21:20:18 +02:00
MacRimi 1b7f881d5a Update uupdump_creator.sh 2025-07-11 20:20:32 +02:00
MacRimi 8635e2cf67 Update uupdump_creator.sh 2025-07-11 20:13:33 +02:00
MacRimi e36bc8bab2 Update uupdump_creator.sh 2025-07-11 20:09:16 +02:00
MacRimi 693da7733f Update uupdump_creator.sh 2025-07-11 19:59:34 +02:00
MacRimi 6b6128d92d Update uupdump_creator.sh 2025-07-11 17:28:02 +02:00
MacRimi 64586a44b4 Update customizable_post_install.sh 2025-07-10 19:25:50 +02:00
MacRimi 2d28ecca32 Update network_menu.sh 2025-07-10 19:16:16 +02:00
MacRimi 92f3edb337 Update network_menu.sh 2025-07-10 19:11:32 +02:00
MacRimi 32f8edecdd Update network_menu.sh 2025-07-10 17:09:59 +02:00
MacRimi 14b061cd28 Update cache.json 2025-07-10 12:37:59 +02:00
MacRimi aeb90cbdd2 Update network_menu.sh 2025-07-10 12:31:51 +02:00
MacRimi e2a0b627b2 Update cache.json 2025-07-09 22:29:03 +02:00
MacRimi de5eb0d914 Update version.txt 2025-07-09 22:07:26 +02:00
MacRimi fc97499504 New version 2025-07-09 21:55:23 +02:00
MacRimi 681f5622c6 Update install_proxmenux.sh 2025-07-09 20:48:54 +02:00
MacRimi 323812710e Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-09 20:48:02 +02:00
MacRimi d421c0c62c Update install_proxmenux.sh 2025-07-09 20:47:17 +02:00
MacRimi 8cbcd74f1a update menu 2025-07-09 20:46:24 +02:00
ProxMenuxBot 328cdad26e Update helpers_cache.json 2025-07-09 18:19:15 +00:00
MacRimi 25b83cb86e Update menu 2025-07-09 19:50:55 +02:00
MacRimi d3c5e4c376 Update emergency_repair.sh 2025-07-09 19:36:54 +02:00
MacRimi bf518f548c Update cache.json 2025-07-09 19:18:12 +02:00
MacRimi 3999416366 Update cache.json 2025-07-09 19:13:34 +02:00
MacRimi 707811cc4a update script coral TPU 2025-07-09 18:59:26 +02:00
MacRimi 05a7f4ba83 Update install_coral_lxc.sh 2025-07-09 18:11:45 +02:00
MacRimi be339435b8 Update menu_post_install.sh 2025-07-09 14:55:05 +02:00
MacRimi ea7adb9b5b Update auto_post_install.sh 2025-07-09 14:42:47 +02:00
ProxMenuxBot 3583cb007a Update helpers_cache.json 2025-07-09 12:28:00 +00:00
MacRimi cc06a4e1b1 Update proxmox_update.sh 2025-07-09 08:44:29 +02:00
MacRimi 6e0c09816b Update proxmox_update.sh 2025-07-09 08:36:00 +02:00
MacRimi 50a4514954 Update network_menu.sh 2025-07-09 08:26:30 +02:00
MacRimi 91bca917c2 update menu network 2025-07-09 08:19:07 +02:00
MacRimi 0550aa9bd2 Update main menu 2025-07-08 23:21:11 +02:00
MacRimi c9d172d301 update network repair 2025-07-08 22:58:00 +02:00
MacRimi 77c950bc1e Update network_menu.sh 2025-07-08 22:42:12 +02:00
ProxMenuxBot d0bbfa82d1 Update helpers_cache.json 2025-07-08 01:06:28 +00:00
ProxMenuxBot 53d657ea0e Update helpers_cache.json 2025-07-07 12:27:17 +00:00
ProxMenuxBot bf8e677b55 Update helpers_cache.json 2025-07-07 06:21:40 +00:00
MacRimi 8deebf83ee Update network_menu.sh 2025-07-06 23:09:27 +02:00
MacRimi 31ec2a0b3a Update network_menu.sh 2025-07-06 17:45:33 +02:00
MacRimi e6b568d7d6 update menu netwokr 2025-07-06 17:44:01 +02:00
MacRimi dfbfea5169 update cache 2025-07-06 16:47:50 +02:00
MacRimi 66862c9bd3 Update cache.json 2025-07-06 16:38:25 +02:00
MacRimi 84c217717f Update customizable_post_install.sh 2025-07-06 16:33:23 +02:00
MacRimi 67f309f32e Update post-install 2025-07-06 16:30:16 +02:00
MacRimi 6e7e7cc7fa Update auto_post_install.sh 2025-07-06 14:31:04 +02:00
MacRimi 5f39a1e08c delete scripts 2025-07-06 14:27:46 +02:00
MacRimi b175ddb0ca Update cache.json 2025-07-06 14:26:03 +02:00
MacRimi 4f36b2eab9 Update cache.json 2025-07-06 14:22:26 +02:00
MacRimi 853c86c40b Update cache.json 2025-07-06 14:17:17 +02:00
MacRimi 242dfa6c9e update post install automated 2025-07-06 14:02:24 +02:00
MacRimi d98b68ca65 Update menu_post_install.sh 2025-07-06 13:49:52 +02:00
MacRimi a41df16f14 Update auto_post_install.sh 2025-07-06 13:06:57 +02:00
MacRimi 48b545d731 new script post-install automated 2025-07-06 12:21:29 +02:00
MacRimi 707b6a6f1b Update config_menu.sh 2025-07-05 17:31:18 +02:00
MacRimi c21b374b49 Update config_menu.sh 2025-07-05 17:26:31 +02:00
MacRimi e09749c6f2 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-05 16:31:26 +02:00
MacRimi b7876d1774 Update install_proxmenux.sh 2025-07-05 16:31:24 +02:00
MacRimi d0590600f3 Update cache.json 2025-07-05 16:28:15 +02:00
MacRimi fe5b30b4c6 Update cache.json 2025-07-05 16:20:49 +02:00
MacRimi b7ce73d338 Update cache.json 2025-07-05 15:51:09 +02:00
MacRimi 8139eb607c Update utils.sh 2025-07-05 11:55:41 +02:00
MacRimi 96c08e6563 Update utils.sh 2025-07-05 11:52:20 +02:00
MacRimi 38977af9d3 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-05 11:48:36 +02:00
MacRimi 0584081c33 Update utils.sh 2025-07-05 11:48:33 +02:00
MacRimi ea5e471bf8 Update cache.json 2025-07-05 11:28:51 +02:00
MacRimi 230847dace Update install Proxmenux 2025-07-04 22:37:16 +02:00
MacRimi 5a91810e9a Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-04 21:44:21 +02:00
MacRimi 49ff63fad4 Update proxmox_update.sh 2025-07-04 21:44:19 +02:00
MacRimi 96cc87cde8 Update cache.json 2025-07-04 21:15:25 +02:00
MacRimi baddd82ecc Update main_menu.sh 2025-07-04 20:58:43 +02:00
MacRimi a4b127a8f3 Update main_menu.sh 2025-07-04 20:50:07 +02:00
MacRimi 2851fd20b8 Update main_menu.sh 2025-07-04 20:36:19 +02:00
MacRimi dd278fe3d3 Update install ProxMenux 2025-07-04 20:30:19 +02:00
MacRimi 196c29da17 Update menu 2025-07-04 19:40:38 +02:00
ProxMenuxBot a1f7fb57d9 Update helpers_cache.json 2025-07-04 12:26:57 +00:00
ProxMenuxBot 27acd37a2c Update helpers_cache.json 2025-07-03 18:18:48 +00:00
ProxMenuxBot 6fb7202e89 Update helpers_cache.json 2025-07-02 18:19:27 +00:00
MacRimi dddd12a036 update script iso creator 2025-07-02 18:47:08 +02:00
MacRimi f4c0211cba Update utilities menu 2025-07-02 18:36:30 +02:00
MacRimi 57d291a13c Update uup_dump_iso_creator.sh 2025-07-02 18:05:38 +02:00
MacRimi 5b2011cf82 Update uup_dump_iso_creator.sh 2025-07-02 18:02:42 +02:00
MacRimi d14f0e9295 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-07-02 17:57:28 +02:00
MacRimi f2a15f84a8 Updates menus 2025-07-02 17:57:25 +02:00
ProxMenuxBot d86831467b Update helpers_cache.json 2025-07-02 01:06:29 +00:00
MacRimi d12365667f Update proxmox_update.sh 2025-07-01 20:56:57 +02:00
MacRimi dcc07d4590 Create proxmox_update.sh 2025-07-01 20:49:36 +02:00
MacRimi c996149476 Update uup_dump_iso_creator.sh 2025-07-01 19:03:35 +02:00
MacRimi 39ff81dee4 Update uup_dump_iso_creator.sh 2025-07-01 18:13:32 +02:00
ProxMenuxBot 22aa3aef96 Update helpers_cache.json 2025-06-30 18:47:59 +00:00
MacRimi 3faff72519 utilities create menu 2025-06-30 18:54:16 +02:00
MacRimi bac3245eb7 Update customizable_post_install.sh 2025-06-30 17:28:26 +02:00
MacRimi ec9e8e348f Update customizable_post_install.sh 2025-06-30 15:25:10 +02:00
MacRimi 131da2ec60 Update customizable_post_install.sh 2025-06-30 15:12:31 +02:00
MacRimi 38d44deef6 Update uninstall-tools.sh 2025-06-29 18:20:15 +02:00
MacRimi 71c31b182a Update uninstall-tools.sh 2025-06-29 17:59:45 +02:00
MacRimi 02baeba8b9 Update uninstall-tools.sh 2025-06-29 17:43:48 +02:00
MacRimi 1d6ce959c8 Update customizable_post_install.sh 2025-06-29 16:58:34 +02:00
MacRimi f599015473 Update customizable_post_install.sh 2025-06-29 16:44:14 +02:00
MacRimi e98d804902 update menu nas 2025-06-29 15:40:36 +02:00
MacRimi 18f5ac1ead update menus nas and linux 2025-06-29 15:02:13 +02:00
MacRimi 5f2e346bd8 Update select_linux_iso.sh 2025-06-29 14:07:07 +02:00
MacRimi 96c23bf138 Update select_nas_iso.sh 2025-06-29 13:57:23 +02:00
MacRimi 446a724077 update menu linux and nas 2025-06-29 13:53:10 +02:00
MacRimi 6e9afa7f9f Update disk-passthrough_ct.sh 2025-06-28 21:43:48 +02:00
ProxMenuxBot 5c13d124de Update helpers_cache.json 2025-06-28 01:03:36 +00:00
ProxMenuxBot 4f94e21ea8 Update helpers_cache.json 2025-06-27 18:18:47 +00:00
ProxMenuxBot 7498aab76a Update helpers_cache.json 2025-06-26 18:20:04 +00:00
MacRimi e65ac4ac8b Update uupdump_creator.sh 2025-06-25 20:23:45 +02:00
MacRimi df2ec69448 Create Iso.sh 2025-06-25 20:23:31 +02:00
ProxMenuxBot 6fe474e494 Update helpers_cache.json 2025-06-25 12:27:42 +00:00
ProxMenuxBot 6408477939 Update helpers_cache.json 2025-06-24 18:18:58 +00:00
ProxMenuxBot 5a20a4260b Update helpers_cache.json 2025-06-24 12:27:48 +00:00
ProxMenuxBot b604b81f37 Update helpers_cache.json 2025-06-24 01:06:00 +00:00
MacRimi 40e36b3203 Update customizable_post_install.sh 2025-06-21 19:09:10 +02:00
MacRimi e098b63beb Update customizable_post_install.sh 2025-06-21 09:18:58 +02:00
ProxMenuxBot 56e99bc6ba Update helpers_cache.json 2025-06-19 18:18:49 +00:00
MacRimi 0abc42bf2c Update uupdump_creator.sh 2025-06-19 19:46:33 +02:00
ProxMenuxBot f648eba8dd Update helpers_cache.json 2025-06-19 12:52:20 +00:00
ProxMenuxBot 20648b479f Update helpers_cache.json 2025-06-19 06:20:00 +00:00
ProxMenuxBot ef82bac8fc Update helpers_cache.json 2025-06-18 18:19:19 +00:00
MacRimi 28e330520b update menu backup 2025-06-18 18:56:11 +02:00
MacRimi b481bb08cc Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-17 19:52:39 +02:00
MacRimi 506d7fff22 Create backup_host.sh 2025-06-17 19:52:36 +02:00
ProxMenuxBot da3b42b6ac Update helpers_cache.json 2025-06-17 12:27:56 +00:00
ProxMenuxBot eea50c23b5 Update helpers_cache.json 2025-06-16 18:19:26 +00:00
ProxMenuxBot b1f5860335 Update helpers_cache.json 2025-06-16 06:21:42 +00:00
ProxMenuxBot 958c567b6b Update helpers_cache.json 2025-06-16 01:07:19 +00:00
MacRimi b443f278da Update mount_disk_host_bk.sh 2025-06-15 11:58:17 +02:00
MacRimi f5ae187012 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-15 11:48:40 +02:00
MacRimi 6e48279c8a Update mount_disk_host_bk.sh 2025-06-15 11:48:30 +02:00
MacRimi 61976e8c13 Update mount_disk_host_bk.sh 2025-06-15 11:47:28 +02:00
MacRimi 2009b0ff7e Update mount_disk_host_bk.sh 2025-06-15 11:35:57 +02:00
MacRimi 60dd0d45b9 Update mount to host 2025-06-14 20:17:51 +02:00
MacRimi a7422e4c1e Update mount_disk_host_bk.sh 2025-06-14 16:19:06 +02:00
MacRimi ffd79d2404 Update mount_disk_host_bk.sh 2025-06-14 16:17:43 +02:00
MacRimi 7363a461de Update mount_disk_host_bk.sh 2025-06-14 15:58:49 +02:00
MacRimi f7325f030c Rename Mount_disk_host_bk.sh to mount_disk_host_bk.sh 2025-06-14 11:59:25 +02:00
MacRimi 2668e5d8b2 Create Mount_disk_host_bk.sh 2025-06-14 11:52:50 +02:00
ProxMenuxBot f09e3ffcdb Update helpers_cache.json 2025-06-13 01:05:40 +00:00
ProxMenuxBot de4bba2e85 Update helpers_cache.json 2025-06-12 18:18:59 +00:00
ProxMenuxBot bb50ecc86c Update helpers_cache.json 2025-06-10 18:18:50 +00:00
MacRimi 2191fe4cdd Update customizable_post_install.sh 2025-06-10 19:58:34 +02:00
MacRimi 321e0b2331 Update customizable_post_install.sh 2025-06-10 19:54:31 +02:00
MacRimi f4611280a7 Update menu_Helper_Scripts.sh 2025-06-10 14:34:30 +02:00
MacRimi ed3f2415bb Update hw_grafics_menu.sh 2025-06-10 14:29:34 +02:00
MacRimi 495bc24b2f Update install_coral_pve.sh 2025-06-10 14:27:09 +02:00
MacRimi 397c84cacb Update install_coral_lxc.sh 2025-06-10 14:24:30 +02:00
MacRimi f04fb7e756 Update configure_igpu_lxc.sh 2025-06-10 14:18:05 +02:00
MacRimi dcbed8b173 Update configure_igpu_lxc.sh 2025-06-10 14:03:58 +02:00
MacRimi 3e5e79ba18 Update config_menu.sh 2025-06-10 13:57:07 +02:00
MacRimi ddaee77b59 Update config_menu.sh 2025-06-10 13:55:10 +02:00
MacRimi 2d5a08a921 Update create_vm_menu.sh 2025-06-10 13:52:30 +02:00
MacRimi 240a325ef1 Update create_vm_menu_.sh 2025-06-10 13:48:09 +02:00
MacRimi 663a0f15df Update cache.json 2025-06-10 13:30:03 +02:00
MacRimi cb7afac17b Update main_menu.sh 2025-06-10 13:28:06 +02:00
MacRimi b04710cf50 Update cache.json 2025-06-10 13:24:49 +02:00
MacRimi ce3fd894ae Update storage_menu.sh 2025-06-10 12:56:05 +02:00
MacRimi fd11f4e866 Update create_vm_menu.sh 2025-06-10 12:48:26 +02:00
MacRimi 5422af1e82 Update main_menu.sh 2025-06-10 12:45:17 +02:00
MacRimi 444002b006 Update storage_menu.sh 2025-06-10 12:44:49 +02:00
MacRimi f01c474536 Actualizar storage_menu.sh 2025-06-10 11:41:07 +02:00
ProxMenuxBot 696b42666f Update helpers_cache.json 2025-06-10 01:06:21 +00:00
ProxMenuxBot 84190e0806 Update helpers_cache.json 2025-06-09 01:08:35 +00:00
ProxMenuxBot 80253426b7 Update helpers_cache.json 2025-06-07 01:03:54 +00:00
MacRimi 26ccc63c96 Actualizar CHANGELOG.md 2025-06-06 19:19:11 +02:00
MacRimi 1124ac41f9 Update CHANGELOG.md 2025-06-06 18:29:10 +02:00
MacRimi d534d8b25c Update CHANGELOG.md 2025-06-06 18:28:26 +02:00
MacRimi 618afaacd4 add images 2025-06-06 17:54:44 +02:00
MacRimi 53b6ce56bf Update menu_Helper_Scripts.sh 2025-06-06 17:42:52 +02:00
MacRimi 8257c7d7e4 Update menu_Helper_Scripts.sh 2025-06-06 17:40:53 +02:00
MacRimi 769416f474 Update menu_Helper_Scripts.sh 2025-06-06 17:34:51 +02:00
MacRimi f978e5d261 Update main_menu.sh 2025-06-06 17:30:24 +02:00
MacRimi 9d3660a1e2 update menu 2025-06-06 17:29:06 +02:00
MacRimi f2637aad46 Update menu 2025-06-06 17:24:56 +02:00
MacRimi 371e8a9570 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-06 17:11:59 +02:00
MacRimi 56987fe7a0 Update customizable_post_install.sh 2025-06-06 17:11:57 +02:00
ProxMenuxBot dfe5138cad Update helpers_cache.json 2025-06-06 12:25:55 +00:00
ProxMenuxBot 90a2d83670 Update helpers_cache.json 2025-06-06 01:04:20 +00:00
MacRimi 8c8981ea9f Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-06 00:02:47 +02:00
MacRimi de49d67361 Update helpers-menu.sh 2025-06-06 00:02:43 +02:00
ProxMenuxBot 5826c383b7 Update helpers_cache.json 2025-06-05 12:27:18 +00:00
ProxMenuxBot 24962f44e1 Update helpers_cache.json 2025-06-04 21:26:29 +00:00
MacRimi 9f2fc40c76 Update generate_helpers_cache.py 2025-06-04 23:25:29 +02:00
MacRimi b8bdcf4c71 Update helpers-menu.sh 2025-06-04 20:50:01 +02:00
MacRimi c9c6dc7666 Update helpers-menu.sh 2025-06-04 20:47:11 +02:00
MacRimi 9f686b91a2 Update helpers-menu.sh 2025-06-04 20:45:05 +02:00
MacRimi 9e879d6582 Update helpers-menu.sh 2025-06-04 20:42:23 +02:00
MacRimi 2fc1df729b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-06-04 20:39:57 +02:00
MacRimi 6586b9746a Create helpers-menu.sh 2025-06-04 20:39:50 +02:00
ProxMenuxBot 37d3ba3bc1 Update helpers_cache.json 2025-06-04 18:19:08 +00:00
ProxMenuxBot 1126860834 Update helpers_cache.json 2025-06-03 18:42:23 +00:00
MacRimi c26141bc5d Update generate_helpers_cache.py 2025-06-03 20:41:20 +02:00
ProxMenuxBot 3d6cfb44bb Update helpers_cache.json 2025-06-03 17:41:05 +00:00
MacRimi 4b1bdc55f1 Update generate_helpers_cache.py 2025-06-03 19:40:01 +02:00
ProxMenuxBot c2bdd5f0bb Update helpers_cache.json 2025-06-03 16:44:28 +00:00
MacRimi c22544672c Update update-helpers-cache.yml 2025-06-03 18:42:14 +02:00
MacRimi 78064cc07c Update generate_helpers_cache.py 2025-06-03 18:41:49 +02:00
MacRimi 84f5897e38 Update generate_helpers_cache.py 2025-06-03 18:37:36 +02:00
ProxMenuxBot 9044f13d2b 🔄 Update helpers_cache.json 2025-06-03 09:13:31 +00:00
MacRimi cf9ee44970 Actualizar update-helpers-cache.yml 2025-06-03 11:11:24 +02:00
MacRimi 0cf5830671 Actualizar generate_helpers_cache.py 2025-06-03 11:09:29 +02:00
MacRimi f25d8aec3c Crear update-helpers-cache.yml 2025-06-03 11:00:47 +02:00
MacRimi 4ecb3f9943 Crear generate_helpers_cache.py 2025-06-03 10:58:58 +02:00
MacRimi 3dcb521422 Update import-disk-image.sh 2025-05-30 18:25:10 +02:00
MacRimi de4db1de9a Update import-disk-image.sh 2025-05-30 15:11:18 +02:00
MacRimi a6c2b958a2 Update import-disk-image.sh 2025-05-30 14:56:57 +02:00
MacRimi f721d9d774 Update README.md 2025-05-30 11:12:45 +02:00
MacRimi 3a8c1c3fd9 Update README.md 2025-05-30 11:08:59 +02:00
MacRimi 891c70dd4c Update CODE_OF_CONDUCT.md 2025-05-30 11:07:26 +02:00
MacRimi f2b99722e3 Create SECURITY.md 2025-05-30 11:03:54 +02:00
MacRimi 32204d3e17 Update vm_configurator.sh 2025-05-29 22:18:52 +02:00
MacRimi 112dfe08b3 Update vm_configurator.sh 2025-05-29 22:07:42 +02:00
MacRimi 4c3736fad7 Update storage_menu.sh 2025-05-29 21:50:48 +02:00
MacRimi 69a5b76e97 Update menu import image to vm 2025-05-29 21:49:21 +02:00
MacRimi f941207699 Update import-disk-image.sh 2025-05-29 21:43:39 +02:00
MacRimi 084a8956ca Update menu_post_install.sh 2025-05-28 23:33:59 +02:00
MacRimi 571b5270a2 Update menus dialog 2025-05-28 23:26:58 +02:00
MacRimi dcae6e1cd0 Update RSS page 2025-05-28 17:24:11 +02:00
MacRimi a7d84d27fd Update page.tsx 2025-05-28 17:16:06 +02:00
MacRimi 9fba81f51d Update CHANGELOG.md 2025-05-27 20:48:54 +02:00
MacRimi 35e399dbaf Update storage_menu.sh 2025-05-27 20:32:30 +02:00
MacRimi c3fc013002 Update storage_menu.sh 2025-05-27 20:30:17 +02:00
MacRimi a34fc6eaa4 Update menus 2025-05-27 19:58:03 +02:00
337 changed files with 186860 additions and 18946 deletions
@@ -0,0 +1,117 @@
title: "[Prompt] "
labels:
- custom-prompt
- community
body:
- type: markdown
attributes:
value: |
## Share Your Custom Prompt
Thank you for sharing your custom prompt with the community!
**Title format suggestion:** Include the provider in the title for easy filtering.
Example: `[Gemini] Clean Spanish - Structured, no emojis`
This helps others find prompts for their specific AI provider.
- type: dropdown
id: provider
attributes:
label: AI Provider
description: Which AI provider did you test this prompt with?
options:
- OpenAI
- Gemini
- Groq
- Ollama
- Anthropic
- OpenRouter
- DeepSeek
- Other
validations:
required: true
- type: input
id: model
attributes:
label: Model
description: The specific model you tested with
placeholder: "e.g., gpt-4o-mini, gemini-2.0-flash, llama3.2:3b"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Describe what your prompt does, main features, and output language
placeholder: |
This prompt generates concise notifications in Spanish.
Features:
- Brief format (2-3 lines)
- Includes severity indicators
- Uses emojis for visual clarity
validations:
required: true
- type: textarea
id: prompt-content
attributes:
label: Prompt Content
description: Paste your complete custom prompt here
render: text
placeholder: |
You are a notification formatter for ProxMenux Monitor.
Your task is to...
RULES:
1. ...
2. ...
OUTPUT FORMAT:
[TITLE]
...
[BODY]
...
validations:
required: true
- type: textarea
id: example-output
attributes:
label: Example Output
description: Show an example of how a notification looks with your prompt
placeholder: |
**Input notification:**
CPU usage high on node pve01
**Output with this prompt:**
pve01: High CPU Usage
CPU at 95% for 5 minutes. Check running processes.
validations:
required: false
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Any tips, variations, or known limitations
placeholder: |
- Works best with models that support system prompts
- May need adjustment for very long notifications
- Tested with Proxmox VE 8.x
validations:
required: false
- type: checkboxes
id: confirmation
attributes:
label: Confirmation
options:
- label: I have tested this prompt and it works correctly
required: true
- label: I am sharing this prompt for the community to use freely
required: true
+29
View File
@@ -0,0 +1,29 @@
---
name: Bug Report
about: Report a problem in the project
title: "[BUG] Describe the issue"
labels: bug
assignees: 'MacRimi'
---
## Description
Describe the bug clearly and concisely.
## Steps to Reproduce
1. ...
2. ...
3. ...
## Expected Behavior
What should happen?
## Screenshots (Required)
Add images to help illustrate the issue.
## Environment
- Operating system:
- Software version:
- Other relevant details:
## Additional Information
Add any other context about the problem here.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Soporte General
url: https://github.com/MacRimi/ProxMenux/discussions
about: If your request is neither a bug nor a feature, please use Discussions.
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[FEATURE] Describe your proposal"
labels: enhancement
assignees: 'MacRimi'
---
## Description
Explain the feature you are proposing.
## Motivation
Why is this improvement important? What problem does it solve?
## Alternatives Considered
Are there other solutions you have thought about?
## Additional Information
Add any extra details that help understand your proposal.
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
from typing import Any
import requests
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
POCKETBASE_BASE = "https://db.community-scripts.org/api/collections"
SCRIPT_COLLECTION_URL = f"{POCKETBASE_BASE}/script_scripts/records"
CATEGORY_COLLECTION_URL = f"{POCKETBASE_BASE}/script_categories/records"
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
TYPE_TO_PATH_PREFIX = {
"lxc": "ct",
"vm": "vm",
"addon": "tools/addon",
"pve": "tools/pve",
}
def to_mirror_url(raw_url: str) -> str:
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m:
return ""
org, repo, branch, path = m.groups()
if org.lower() != "community-scripts" or repo != "ProxmoxVE":
return ""
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def fetch_json(url: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
r = requests.get(url, params=params, timeout=60)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict):
raise RuntimeError(f"Unexpected response from {url}: expected object")
return data
def fetch_all_records(url: str, *, expand: str | None = None, per_page: int = 500) -> list[dict[str, Any]]:
page = 1
items: list[dict[str, Any]] = []
while True:
params: dict[str, Any] = {"page": page, "perPage": per_page}
if expand:
params["expand"] = expand
data = fetch_json(url, params=params)
page_items = data.get("items", [])
if not isinstance(page_items, list):
raise RuntimeError(f"Unexpected items list from {url}")
items.extend(page_items)
total_pages = data.get("totalPages", page)
if not isinstance(total_pages, int) or page >= total_pages:
break
page += 1
return items
def normalize_os_variants(install_methods_json: list[dict[str, Any]]) -> list[str]:
os_values: list[str] = []
for item in install_methods_json:
if not isinstance(item, dict):
continue
resources = item.get("resources", {})
if not isinstance(resources, dict):
continue
os_name = resources.get("os")
if isinstance(os_name, str) and os_name.strip():
normalized = os_name.strip().lower()
if normalized not in os_values:
os_values.append(normalized)
return os_values
def build_script_path(type_name: str, slug: str) -> str:
type_name = (type_name or "").strip().lower()
slug = (slug or "").strip()
if type_name == "turnkey":
return "turnkey/turnkey.sh"
prefix = TYPE_TO_PATH_PREFIX.get(type_name)
if not prefix or not slug:
return ""
return f"{prefix}/{slug}.sh"
def main() -> int:
try:
scripts = fetch_all_records(SCRIPT_COLLECTION_URL, expand="type,categories")
categories = fetch_all_records(CATEGORY_COLLECTION_URL)
except Exception as e:
print(f"ERROR: Unable to fetch PocketBase data: {e}", file=sys.stderr)
return 1
category_map: dict[str, dict[str, Any]] = {}
for category in categories:
category_id = category.get("id")
if isinstance(category_id, str) and category_id:
category_map[category_id] = category
cache: list[dict[str, Any]] = []
print(f"Fetched {len(scripts)} scripts and {len(category_map)} categories")
for idx, raw in enumerate(scripts, start=1):
if not isinstance(raw, dict):
continue
slug = raw.get("slug")
name = raw.get("name", "")
desc = raw.get("description", "")
if not isinstance(slug, str) or not slug.strip():
continue
expand = raw.get("expand", {}) if isinstance(raw.get("expand"), dict) else {}
type_expanded = expand.get("type", {}) if isinstance(expand.get("type"), dict) else {}
type_name = type_expanded.get("type", "") if isinstance(type_expanded.get("type"), str) else ""
script_path = build_script_path(type_name, slug)
if not script_path:
print(f"[{idx:03d}] WARNING: Unable to build script path for slug={slug} type={type_name!r}", file=sys.stderr)
continue
full_script_url = f"{SCRIPT_BASE}/{script_path}"
script_url_mirror = to_mirror_url(full_script_url)
install_methods_json = raw.get("install_methods_json", [])
if not isinstance(install_methods_json, list):
install_methods_json = []
notes_json = raw.get("notes_json", [])
if not isinstance(notes_json, list):
notes_json = []
notes = [
note.get("text", "")
for note in notes_json
if isinstance(note, dict) and isinstance(note.get("text"), str) and note.get("text", "").strip()
]
category_ids = raw.get("categories", [])
if not isinstance(category_ids, list):
category_ids = []
expanded_categories = expand.get("categories", []) if isinstance(expand.get("categories"), list) else []
category_names: list[str] = []
for cat in expanded_categories:
if isinstance(cat, dict):
cat_name = cat.get("name")
if isinstance(cat_name, str) and cat_name.strip():
category_names.append(cat_name.strip())
if not category_names:
for cat_id in category_ids:
cat = category_map.get(cat_id, {})
cat_name = cat.get("name")
if isinstance(cat_name, str) and cat_name.strip():
category_names.append(cat_name.strip())
# Shared fields across all install method entries
default_user = raw.get("default_user")
default_passwd = raw.get("default_passwd")
default_credentials: dict[str, str] | None = None
if (isinstance(default_user, str) and default_user.strip()) or (isinstance(default_passwd, str) and default_passwd.strip()):
default_credentials = {
"username": default_user if isinstance(default_user, str) else "",
"password": default_passwd if isinstance(default_passwd, str) else "",
}
base_entry: dict[str, Any] = {
"name": name,
"slug": slug,
"desc": desc,
"script": script_path,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror,
"type": type_name,
"type_id": raw.get("type", ""),
"categories": category_ids,
"category_names": category_names,
"notes": notes,
"port": raw.get("port", 0),
"website": raw.get("website", ""),
"documentation": raw.get("documentation", ""),
"logo": raw.get("logo", ""),
"updateable": bool(raw.get("updateable", False)),
"privileged": bool(raw.get("privileged", False)),
"has_arm": bool(raw.get("has_arm", False)),
"is_dev": bool(raw.get("is_dev", False)),
"execute_in": raw.get("execute_in", []),
"config_path": raw.get("config_path", ""),
}
if default_credentials:
base_entry["default_credentials"] = default_credentials
# Emit one entry per install method so the menu shell can offer an
# explicit OS choice. When there is only one method (or none), a
# single entry is emitted with os="" (script decides at runtime).
os_variants = normalize_os_variants(install_methods_json)
if len(os_variants) > 1:
for os_name in os_variants:
entry = {**base_entry, "os": os_name}
cache.append(entry)
print(f"[{len(cache):03d}] {slug:<24}{script_path:<28} type={type_name:<7} os={os_name}")
else:
os_name = os_variants[0] if os_variants else ""
entry = {**base_entry, "os": os_name}
cache.append(entry)
print(f"[{len(cache):03d}] {slug:<24}{script_path:<28} type={type_name:<7} os={os_name or 'n/a'}")
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Guardados: {len(cache)}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,76 @@
import requests, json
from pathlib import Path
# GitHub API URL to fetch all .json files describing scripts
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
# Base path to build the full URL for the installable scripts
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Output file where the consolidated helper scripts cache will be stored
OUTPUT_FILE = Path("json/helpers_cache.json")
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
res = requests.get(API_URL)
data = res.json()
cache = []
# Loop over each file in the JSON directory
for item in data:
url = item.get("download_url")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url).json()
if not isinstance(raw, dict):
continue
except:
continue
# Extract fields required to identify a valid helper script
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
script = raw.get("install_methods", [{}])[0].get("script", "")
if not slug or not script:
continue # Skip if it's not a valid script
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [note.get("text", "") for note in raw.get("notes", []) if isinstance(note, dict)]
full_script_url = f"{SCRIPT_BASE}/{script}"
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username")
cred_password = credentials.get("password")
add_credentials = (
(cred_username is not None and str(cred_username).strip() != "") or
(cred_password is not None and str(cred_password).strip() != "")
)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"categories": categories,
"notes": notes,
"type": type_
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password
}
cache.append(entry)
# Write the JSON cache to disk
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, indent=2)
print(f"✅ helpers_cache.json created at {OUTPUT_FILE} with {len(cache)} valid scripts.")
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
import requests
# ---------- Config ----------
# API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
API_URL = "https://api.github.com/repos/community-scripts/ProxmoxVE-Frontend-Archive/contents/public/json"
SCRIPT_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
# Escribimos siempre en <raiz_repo>/json/helpers_cache.json, independientemente del cwd
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_FILE = REPO_ROOT / "json" / "helpers_cache.json"
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# ----------------------------
def to_mirror_url(raw_url: str) -> str:
"""
Convierte una URL raw de GitHub al raw del mirror.
GH : https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh
MIR: https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/ct/docker.sh
"""
m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)$", raw_url or "")
if not m:
return ""
org, repo, branch, path = m.groups()
if org.lower() != "community-scripts" or repo != "ProxmoxVE":
return ""
return f"https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/{branch}/{path}"
def guess_os_from_script_path(script_path: str) -> str | None:
"""
Heurística suave cuando el JSON no publica resources.os:
- tools/pve/* -> proxmox
- ct/alpine-* -> alpine
- tools/addon/* -> generic (suele ejecutarse sobre LXC existente)
- ct/* -> debian (por defecto para CTs)
"""
if not script_path:
return None
if script_path.startswith("tools/pve/") or script_path == "tools/pve/host-backup.sh" or script_path.startswith("vm/"):
return "proxmox"
if "/alpine-" in script_path or script_path.startswith("ct/alpine-"):
return "alpine"
if script_path.startswith("tools/addon/"):
return "generic"
if script_path.startswith("ct/"):
return "debian"
return None
def fetch_directory_json(api_url: str) -> list[dict]:
r = requests.get(api_url, timeout=30)
r.raise_for_status()
data = r.json()
if not isinstance(data, list):
raise RuntimeError("GitHub API no devolvió una lista.")
return data
def main() -> int:
try:
directory = fetch_directory_json(API_URL)
except Exception as e:
print(f"ERROR: No se pudo leer el índice de JSONs: {e}", file=sys.stderr)
return 1
cache: list[dict] = []
seen: set[tuple[str, str]] = set() # (slug, script) para evitar duplicados
total_items = len(directory)
processed = 0
kept = 0
for item in directory:
url = item.get("download_url")
name_in_dir = item.get("name", "")
if not url or not url.endswith(".json"):
continue
try:
raw = requests.get(url, timeout=30).json()
if not isinstance(raw, dict):
continue
except Exception:
print(f"❌ Error al obtener/parsing {name_in_dir}", file=sys.stderr)
continue
processed += 1
name = raw.get("name", "")
slug = raw.get("slug")
type_ = raw.get("type", "")
desc = raw.get("description", "")
categories = raw.get("categories", [])
notes = [n.get("text", "") for n in raw.get("notes", []) if isinstance(n, dict)]
# Credenciales (si existen, se copian tal cual)
credentials = raw.get("default_credentials", {})
cred_username = credentials.get("username") if isinstance(credentials, dict) else None
cred_password = credentials.get("password") if isinstance(credentials, dict) else None
add_credentials = any([
cred_username not in (None, ""),
cred_password not in (None, "")
])
install_methods = raw.get("install_methods", [])
if not isinstance(install_methods, list) or not install_methods:
# Sin install_methods válidos -> continuamos
continue
for im in install_methods:
if not isinstance(im, dict):
continue
script = im.get("script", "")
if not script:
continue
# OS desde resources u heurística
resources = im.get("resources", {}) if isinstance(im, dict) else {}
os_name = resources.get("os") if isinstance(resources, dict) else None
if not os_name:
os_name = guess_os_from_script_path(script)
if isinstance(os_name, str):
os_name = os_name.strip().lower()
full_script_url = f"{SCRIPT_BASE}/{script}"
script_url_mirror = to_mirror_url(full_script_url)
key = (slug or "", script)
if key in seen:
continue
seen.add(key)
entry = {
"name": name,
"slug": slug,
"desc": desc,
"script": script,
"script_url": full_script_url,
"script_url_mirror": script_url_mirror, # nuevo
"os": os_name, # nuevo
"categories": categories,
"notes": notes,
"type": type_,
}
if add_credentials:
entry["default_credentials"] = {
"username": cred_username,
"password": cred_password,
}
cache.append(entry)
kept += 1
# Progreso ligero
print(f"[{kept:03d}] {slug or name:<24}{script:<28} os={os_name or 'n/a'} src={'GH+MR' if script_url_mirror else 'GH'}")
# Orden estable para commits reproducibles
cache.sort(key=lambda x: (x.get("slug") or "", x.get("script") or ""))
with OUTPUT_FILE.open("w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
print(f"\n✅ helpers_cache.json → {OUTPUT_FILE}")
print(f" Total JSON en índice: {total_items}")
print(f" Procesados: {processed} | Guardados: {kept} | Únicos (slug,script): {len(seen)}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,83 @@
name: Build AppImage Release
on:
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Install dependencies
working-directory: AppImage
run: npm install --legacy-peer-deps
- name: Build Next.js app
working-directory: AppImage
run: npm run build
- name: Install Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv
- name: Make build script executable
working-directory: AppImage
run: chmod +x scripts/build_appimage.sh
- name: Build AppImage
working-directory: AppImage
run: ./scripts/build_appimage.sh
- name: Get version from package.json
id: version
working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Generate SHA256 checksum
run: |
cd AppImage/dist
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage artifact
uses: actions/upload-artifact@v6
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: |
AppImage/dist/*.AppImage
AppImage/dist/*.sha256
retention-days: 30
- name: Commit AppImage to main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage release build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin main
+83
View File
@@ -0,0 +1,83 @@
name: Build AppImage Beta
on:
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout develop
uses: actions/checkout@v6
with:
ref: develop
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Install dependencies
working-directory: AppImage
run: npm install --legacy-peer-deps
- name: Build Next.js app
working-directory: AppImage
run: npm run build
- name: Install Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv
- name: Make build script executable
working-directory: AppImage
run: chmod +x scripts/build_appimage.sh
- name: Build AppImage
working-directory: AppImage
run: ./scripts/build_appimage.sh
- name: Get version from package.json
id: version
working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Generate SHA256 checksum
run: |
cd AppImage/dist
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage artifact
uses: actions/upload-artifact@v6
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-beta-AppImage
path: |
AppImage/dist/*.AppImage
AppImage/dist/*.sha256
retention-days: 30
- name: Commit AppImage to develop
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage beta build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin develop
@@ -0,0 +1,81 @@
name: Build ProxMenux Monitor AppImage
on:
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: AppImage
run: npm install --legacy-peer-deps
- name: Build Next.js app
working-directory: AppImage
run: npm run build
- name: Install Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv
- name: Make build script executable
working-directory: AppImage
run: chmod +x scripts/build_appimage.sh
- name: Build AppImage
working-directory: AppImage
run: ./scripts/build_appimage.sh
- name: Get version from package.json
id: version
working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
retention-days: 30
- name: Generate SHA256 checksum
run: |
cd AppImage/dist
sha256sum *.AppImage > ProxMenux-Monitor.AppImage.sha256
echo "Generated SHA256:"
cat ProxMenux-Monitor.AppImage.sha256
- name: Upload AppImage and checksum to /AppImage folder in main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout main
rm -f AppImage/*.AppImage AppImage/*.sha256 || true
# Copy new files
cp AppImage/dist/*.AppImage AppImage/
cp AppImage/dist/ProxMenux-Monitor.AppImage.sha256 AppImage/
git add AppImage/*.AppImage AppImage/*.sha256
git commit -m "Update AppImage build ($(date +'%Y-%m-%d %H:%M:%S'))" || echo "No changes to commit"
git push origin main
+59
View File
@@ -0,0 +1,59 @@
name: Build ProxMenux Monitor AppImage
on:
push:
branches: [ main ]
paths: [ 'AppImage/**' ]
pull_request:
branches: [ main ]
paths: [ 'AppImage/**' ]
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Install dependencies
working-directory: AppImage
run: npm install --legacy-peer-deps
- name: Build Next.js app
working-directory: AppImage
run: npm run build
- name: Install Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv
- name: Make build script executable
working-directory: AppImage
run: chmod +x scripts/build_appimage.sh
- name: Build AppImage
working-directory: AppImage
run: ./scripts/build_appimage.sh
- name: Get version from package.json
id: version
working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage artifact
uses: actions/upload-artifact@v6
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
retention-days: 30
@@ -0,0 +1,38 @@
name: Update Helper Scripts Cache
on:
# Manual trigger from GitHub Actions UI
workflow_dispatch:
# Automatic run every 6 hours
schedule:
- cron: "0 */6 * * *"
jobs:
update-cache:
runs-on: ubuntu-latest
permissions:
contents: write # Required to push changes to the repository
steps:
- name: ⬇️ Checkout the repository
uses: actions/checkout@v3
- name: 🐍 Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: 📦 Install Python dependencies
run: pip install requests
- name: ⚙️ Generate json/helpers_cache.json
run: python .github/scripts/generate_helpers_cache.py
- name: 📤 Commit and push if updated
run: |
git config user.name "ProxMenuxBot"
git config user.email "bot@proxmenux.local"
git add json/helpers_cache.json
git diff --cached --quiet || git commit -m "Update helpers_cache.json"
git push
+2
View File
@@ -51,3 +51,5 @@ Thumbs.db
!guides/
!web/
# GitHub authentication
.github/auth.sh
Binary file not shown.
@@ -0,0 +1 @@
1caca89b574241c9d754b9ac3bb11987c5eccc5f182d01a5c62e61623b62fda7
+753
View File
@@ -0,0 +1,753 @@
# ProxMenux Monitor
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
---
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Technology Stack](#technology-stack)
- [Installation](#installation)
- [Authentication & Security](#authentication--security)
- [Setup Authentication](#setup-authentication)
- [Two-Factor Authentication (2FA)](#two-factor-authentication-2fa)
- [Security Best Practices for API Tokens](#security-best-practices-for-api-tokens)
- [API Documentation](#api-documentation)
- [API Authentication](#api-authentication)
- [Generating API Tokens](#generating-api-tokens)
- [Available Endpoints](#available-endpoints)
- [Integration Examples](#integration-examples)
- [Homepage Integration](#homepage-integration)
- [Home Assistant Integration](#home-assistant-integration)
- [License](#license)
---
## Overview
**ProxMenux Monitor** is a comprehensive, real-time monitoring dashboard for Proxmox VE environments. Built with modern web technologies, it provides an intuitive interface to monitor system resources, virtual machines, containers, storage, network traffic, and system logs.
The application runs as a standalone AppImage on your Proxmox server and serves a web interface accessible from any device on your network.
## Screenshots
Get a quick overview of ProxMenux Monitor's main features:
<p align="center">
<img src="public/images/onboarding/imagen1.png" alt="Overview Dashboard" width="800"/>
<br/>
<em>System Overview - Monitor CPU, memory, temperature, and uptime in real-time</em>
</p>
---
## Features
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and system uptime
- **Storage Management**: Visual representation of storage distribution, disk health, and SMART data
- **Network Monitoring**: Network interface statistics, real-time traffic graphs, and bandwidth usage
- **Virtual Machines & LXC**: Comprehensive view of all VMs and containers with resource usage and controls
- **Hardware Information**: Detailed hardware specifications including CPU, GPU, PCIe devices, and disks
- **System Logs**: Real-time system log monitoring with filtering and search capabilities
- **Health Monitoring**: Proactive system health checks with persistent error tracking
- **Authentication & 2FA**: Optional password protection with TOTP-based two-factor authentication
- **RESTful API**: Complete API access for integrations with Homepage, Home Assistant, and custom dashboards
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
- **Release Notes**: Automatic notifications of new features and improvements
## Technology Stack
- **Frontend**: Next.js 15, React 19, TypeScript
- **Styling**: Tailwind CSS v4 with custom Proxmox-inspired theme
- **Charts**: Recharts for data visualization
- **UI Components**: Radix UI primitives with shadcn/ui
- **Backend**: Flask (Python) server for system data collection
- **Packaging**: AppImage for easy distribution and deployment
## Installation
**ProxMenux Monitor is integrated into [ProxMenux](https://proxmenux.com) and comes enabled by default.** No manual installation is required if you're using ProxMenux.
The monitor automatically starts when ProxMenux is installed and runs as a systemd service on your Proxmox server.
### Accessing the Dashboard
You can access ProxMenux Monitor in two ways:
1. **Direct Access**: `http://your-proxmox-ip:8008`
2. **Via Proxy** (Recommended): `https://your-domain.com/proxmenux-monitor/`
**Note**: All API endpoints work seamlessly with both direct access and proxy configurations. When using a reverse proxy, the application automatically detects and adapts to the proxied environment.
### Proxy Configuration
ProxMenux Monitor includes built-in support for reverse proxy configurations. If you're using Nginx, Caddy, or Traefik, the application will automatically:
- Detect the proxy headers (`X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`)
- Adjust API endpoints to work correctly through the proxy
- Maintain full functionality for all features including authentication and API access
## Authentication & Security
ProxMenux Monitor includes an optional authentication system to protect your dashboard with a password and two-factor authentication.
### Setup Authentication
On first launch, you'll be presented with three options:
1. **Set up authentication** - Create a username and password to protect your dashboard
2. **Enable 2FA** - Add TOTP-based two-factor authentication for enhanced security
3. **Skip** - Continue without authentication (not recommended for production environments)
![Authentication Setup](AppImage/public/images/docs/auth-setup.png)
### Two-Factor Authentication (2FA)
After setting up your password, you can enable 2FA using any TOTP authenticator app (Google Authenticator, Authy, 1Password, etc.):
1. Navigate to **Settings > Authentication**
2. Click **Enable 2FA**
3. Scan the QR code with your authenticator app
4. Enter the 6-digit code to verify
5. Save your backup codes in a secure location
![2FA Setup](AppImage/public/images/docs/2fa-setup.png)
### Security Best Practices for API Tokens
**IMPORTANT**: Never hardcode your API tokens directly in configuration files or scripts. Instead, use environment variables or secrets management.
**Option 1: Environment Variables**
Store your token in an environment variable:
```bash
# Linux/macOS - Add to ~/.bashrc or ~/.zshrc
export PROXMENUX_API_TOKEN="your_actual_token_here"
# Windows PowerShell - Add to profile
$env:PROXMENUX_API_TOKEN = "your_actual_token_here"
```
Then reference it in your scripts:
```bash
# Linux/macOS
curl -H "Authorization: Bearer $PROXMENUX_API_TOKEN" \
http://your-proxmox-ip:8008/api/system
# Windows PowerShell
curl -H "Authorization: Bearer $env:PROXMENUX_API_TOKEN" `
http://your-proxmox-ip:8008/api/system
```
**Option 2: Secrets File**
Create a dedicated secrets file (make sure to add it to `.gitignore`):
```bash
# Create secrets file
echo "PROXMENUX_API_TOKEN=your_actual_token_here" > ~/.proxmenux_secrets
# Secure the file (Linux/macOS only)
chmod 600 ~/.proxmenux_secrets
# Load in your script
source ~/.proxmenux_secrets
```
**Option 3: Homepage Secrets (Recommended)**
Homepage supports secrets management. Create a `secrets.yaml` file:
```yaml
# secrets.yaml (add to .gitignore!)
proxmenux_token: "your_actual_token_here"
```
Then reference it in your `services.yaml`:
```yaml
- ProxMenux Monitor:
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
```
**Option 4: Home Assistant Secrets**
Home Assistant has built-in secrets support. Edit `secrets.yaml`:
```yaml
# secrets.yaml
proxmenux_api_token: "your_actual_token_here"
```
Then reference it in `configuration.yaml`:
```yaml
sensor:
- platform: rest
name: ProxMenux CPU
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
```
**Token Security Checklist:**
- ✅ Store tokens in environment variables or secrets files
- ✅ Add secrets files to `.gitignore`
- ✅ Set proper file permissions (chmod 600 on Linux/macOS)
- ✅ Rotate tokens periodically (every 3-6 months)
- ✅ Use different tokens for different integrations
- ✅ Delete tokens you no longer use
- ❌ Never commit tokens to version control
- ❌ Never share tokens in screenshots or logs
- ❌ Never hardcode tokens in configuration files
---
## API Documentation
ProxMenux Monitor provides a comprehensive RESTful API for integrating with external services like Homepage, Home Assistant, or custom dashboards.
### API Authentication
When authentication is enabled on ProxMenux Monitor, all API endpoints (except `/api/health` and `/api/auth/*`) require a valid JWT token in the `Authorization` header.
### API Endpoint Base URL
**Direct Access:**
```
http://your-proxmox-ip:8008/api/
```
**Via Proxy:**
```
https://your-domain.com/proxmenux-monitor/api/
```
**Note**: All API examples in this documentation work with both direct and proxied URLs. Simply replace the base URL with your preferred access method.
### Generating API Tokens
To use the API with authentication enabled, you need to generate a long-lived API token.
#### Option 1: Generate via Web Panel (Recommended)
The easiest way to generate an API token is through the ProxMenux Monitor web interface:
1. Navigate to **Settings** tab in the dashboard
2. Scroll to the **API Access Tokens** section
3. Enter your password
4. If 2FA is enabled, enter your 6-digit code
5. Provide a name for the token (e.g., "Homepage Integration")
6. Click **Generate Token**
7. Copy the token immediately - it will not be shown again
![Generate API Token](AppImage/public/images/docs/generate-api-token.png)
The token will be valid for **365 days** (1 year) and can be used for integrations with Homepage, Home Assistant, or any custom application.
#### Option 2: Generate via API Call
For advanced users or automation, you can generate tokens programmatically:
```bash
curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
-H "Content-Type: application/json" \
-d '{
"username": "your-username",
"password": "your-password",
"totp_token": "123456",
"token_name": "Homepage Integration"
}'
```
**Response:**
```json
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_name": "Homepage Integration",
"expires_in": "365 days",
"message": "API token generated successfully. Store this token securely, it will not be shown again."
}
```
**Notes:**
- If 2FA is enabled, include the `totp_token` field with your 6-digit code
- If 2FA is not enabled, omit the `totp_token` field
- The token is valid for **365 days** (1 year)
- Store the token securely - it cannot be retrieved again
#### Option 3: Generate via cURL (without 2FA)
```bash
# Without 2FA
curl -X POST http://your-proxmox-ip:8008/api/auth/generate-api-token \
-H "Content-Type: application/json" \
-d '{"username":"pedro","password":"your-password","token_name":"Homepage"}'
```
### Using API Tokens
Once you have your API token, include it in the `Authorization` header of all API requests:
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN_HERE" \
http://your-proxmox-ip:8008/api/system
```
---
### Available Endpoints
Below is a complete list of all API endpoints with descriptions and example responses.
#### System & Metrics
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/system` | GET | Yes | Complete system information (CPU, memory, temperature, uptime) |
| `/api/system-info` | GET | No | Lightweight system info for header (hostname, uptime, health) |
| `/api/node/metrics` | GET | Yes | Historical metrics data (RRD) for CPU, memory, disk I/O |
| `/api/prometheus` | GET | Yes | Export metrics in Prometheus format |
**Example `/api/system` Response:**
```json
{
"hostname": "pve",
"cpu_usage": 15.2,
"memory_usage": 45.8,
"temperature": 42.5,
"uptime": 345600,
"kernel": "6.2.16-3-pve",
"pve_version": "8.0.3"
}
```
#### Storage
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/storage` | GET | Yes | Complete storage information with SMART data |
| `/api/storage/summary` | GET | Yes | Optimized storage summary (without SMART) |
| `/api/proxmox-storage` | GET | Yes | Proxmox storage pools information |
| `/api/backups` | GET | Yes | List of all backup files |
**Example `/api/storage/summary` Response:**
```json
{
"total_capacity": 1431894917120,
"used_space": 197414092800,
"free_space": 1234480824320,
"usage_percentage": 13.8,
"disks": [
{
"device": "/dev/sda",
"model": "Samsung SSD 970",
"size": "476.94 GB",
"type": "SSD"
}
]
}
```
#### Network
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/network` | GET | Yes | Complete network information for all interfaces |
| `/api/network/summary` | GET | Yes | Optimized network summary |
| `/api/network/<interface>/metrics` | GET | Yes | Historical metrics (RRD) for specific interface |
**Example `/api/network/summary` Response:**
```json
{
"interfaces": [
{
"name": "vmbr0",
"ip": "192.168.1.100",
"state": "up",
"rx_bytes": 1234567890,
"tx_bytes": 987654321
}
]
}
```
#### Virtual Machines & Containers
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/vms` | GET | Yes | List of all VMs and LXC containers |
| `/api/vms/<vmid>` | GET | Yes | Detailed configuration for specific VM/LXC |
| `/api/vms/<vmid>/metrics` | GET | Yes | Historical metrics (RRD) for specific VM/LXC |
| `/api/vms/<vmid>/logs` | GET | Yes | Download real logs for specific VM/LXC |
| `/api/vms/<vmid>/control` | POST | Yes | Control VM/LXC (start, stop, shutdown, reboot) |
| `/api/vms/<vmid>/config` | PUT | Yes | Update VM/LXC configuration (description/notes) |
**Example `/api/vms` Response:**
```json
{
"vms": [
{
"vmid": "100",
"name": "ubuntu-server",
"type": "qemu",
"status": "running",
"cpu": 2,
"maxcpu": 4,
"mem": 2147483648,
"maxmem": 4294967296,
"uptime": 86400
}
]
}
```
#### Hardware
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/hardware` | GET | Yes | Complete hardware information (CPU, GPU, PCIe, disks) |
| `/api/gpu/<slot>/realtime` | GET | Yes | Real-time monitoring for specific GPU |
**Example `/api/hardware` Response:**
```json
{
"cpu": {
"model": "AMD Ryzen 9 5950X",
"cores": 16,
"threads": 32,
"frequency": "3.4 GHz"
},
"gpus": [
{
"slot": "0000:01:00.0",
"vendor": "NVIDIA",
"model": "GeForce RTX 3080",
"driver": "nvidia"
}
]
}
```
#### Logs, Events & Notifications
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/logs` | GET | Yes | System logs (journalctl) with filters |
| `/api/logs/download` | GET | Yes | Download logs as text file |
| `/api/notifications` | GET | Yes | Proxmox notification history |
| `/api/notifications/download` | GET | Yes | Download full notification log |
| `/api/events` | GET | Yes | Recent Proxmox tasks and events |
| `/api/task-log/<upid>` | GET | Yes | Full log for specific task using UPID |
**Example `/api/logs` Query Parameters:**
```
/api/logs?severity=error&since=1h&search=failed
```
#### Health Monitoring
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/health` | GET | No | Basic health check (for external monitoring) |
| `/api/health/status` | GET | Yes | Summary of system health status |
| `/api/health/details` | GET | Yes | Detailed health check results |
| `/api/health/acknowledge` | POST | Yes | Dismiss/acknowledge health warnings |
| `/api/health/active-errors` | GET | Yes | Get active persistent errors |
#### ProxMenux Optimizations
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/proxmenux/installed-tools` | GET | Yes | List of installed ProxMenux optimizations |
#### Authentication
| Endpoint | Method | Auth Required | Description |
|----------|--------|---------------|-------------|
| `/api/auth/status` | GET | No | Current authentication status |
| `/api/auth/login` | POST | No | Authenticate and receive JWT token |
| `/api/auth/generate-api-token` | POST | No | Generate long-lived API token (365 days) |
| `/api/auth/setup` | POST | No | Initial setup of username/password |
| `/api/auth/enable` | POST | No | Enable authentication |
| `/api/auth/disable` | POST | Yes | Disable authentication |
| `/api/auth/change-password` | POST | No | Change password |
| `/api/auth/totp/setup` | POST | Yes | Initialize 2FA setup |
| `/api/auth/totp/enable` | POST | Yes | Enable 2FA after verification |
| `/api/auth/totp/disable` | POST | Yes | Disable 2FA |
---
## Integration Examples
### Homepage Integration
[Homepage](https://gethomepage.dev/) is a modern, fully static, fast, secure fully proxied, highly customizable application dashboard.
#### Basic Configuration (No Authentication)
```yaml
- ProxMenux Monitor:
href: http://proxmox.example.tld:8008/
icon: lucide:flask-round
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
refreshInterval: 10000
mappings:
- field: uptime
label: Uptime
icon: lucide:clock-4
format: text
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
```
#### With Authentication Enabled (Using Secrets)
First, generate an API token via the web interface (Settings > API Access Tokens) or via API.
Then, store your token securely in Homepage's `secrets.yaml`:
```yaml
# secrets.yaml (add to .gitignore!)
proxmenux_token: "your_actual_api_token_here"
```
Finally, reference the secret in your `services.yaml`:
```yaml
- ProxMenux Monitor:
href: http://proxmox.example.tld:8008/
icon: lucide:flask-round
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 10000
mappings:
- field: uptime
label: Uptime
icon: lucide:clock-4
format: text
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
```
#### Advanced Multi-Widget Configuration
```yaml
# Store token in secrets.yaml
# proxmenux_token: "your_actual_api_token_here"
- ProxMenux System:
href: http://proxmox.example.tld:8008/
icon: lucide:server
description: Proxmox VE Host
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 5000
mappings:
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
- ProxMenux Storage:
href: http://proxmox.example.tld:8008/#/storage
icon: lucide:hard-drive
description: Storage Overview
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/storage/summary
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 30000
mappings:
- field: usage_percentage
label: Used
icon: lucide:database
format: percent
- field: used_space
label: Space
icon: lucide:folder
format: bytes
- ProxMenux Network:
href: http://proxmox.example.tld:8008/#/network
icon: lucide:network
description: Network Stats
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/network/summary
headers:
Authorization: Bearer {{HOMEPAGE_VAR_PROXMENUX_TOKEN}}
refreshInterval: 5000
mappings:
- field: interfaces[0].rx_bytes
label: Received
icon: lucide:download
format: bytes
- field: interfaces[0].tx_bytes
label: Sent
icon: lucide:upload
format: bytes
```
![Homepage Integration Example](AppImage/public/images/docs/homepage-integration.png)
### Home Assistant Integration
[Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform.
#### Store Token Securely
First, add your API token to Home Assistant's `secrets.yaml`:
```yaml
# secrets.yaml
proxmenux_api_token: "Bearer your_actual_api_token_here"
```
**Note**: Include "Bearer " prefix in the secrets file for Home Assistant.
#### Configuration.yaml
```yaml
# ProxMenux Monitor Sensors
sensor:
- platform: rest
name: ProxMenux CPU
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: "{{ value_json.cpu_usage }}"
unit_of_measurement: "%"
scan_interval: 30
- platform: rest
name: ProxMenux Memory
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: "{{ value_json.memory_usage }}"
unit_of_measurement: "%"
scan_interval: 30
- platform: rest
name: ProxMenux Temperature
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: "{{ value_json.temperature }}"
unit_of_measurement: "°C"
device_class: temperature
scan_interval: 30
- platform: rest
name: ProxMenux Uptime
resource: http://proxmox.example.tld:8008/api/system
headers:
Authorization: !secret proxmenux_api_token
value_template: >
{% set uptime_seconds = value_json.uptime | int %}
{% set days = (uptime_seconds / 86400) | int %}
{% set hours = ((uptime_seconds % 86400) / 3600) | int %}
{% set minutes = ((uptime_seconds % 3600) / 60) | int %}
{{ days }}d {{ hours }}h {{ minutes }}m
scan_interval: 60
```
#### Lovelace Card Example
```yaml
type: entities
title: Proxmox Monitor
entities:
- entity: sensor.proxmenux_cpu
name: CPU Usage
icon: mdi:cpu-64-bit
- entity: sensor.proxmenux_memory
name: Memory Usage
icon: mdi:memory
- entity: sensor.proxmenux_temperature
name: Temperature
icon: mdi:thermometer
- entity: sensor.proxmenux_uptime
name: Uptime
icon: mdi:clock-outline
```
![Home Assistant Integration Example](AppImage/public/images/docs/homeassistant-integration.png)
---
## License
This project is licensed under the **Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0)**.
You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
- NonCommercial — You may not use the material for commercial purposes
For more details, see the [full license](https://creativecommons.org/licenses/by-nc/4.0/).
---
**ProxMenux Monitor** - Made with ❤️ for the Proxmox community
+9
View File
@@ -0,0 +1,9 @@
import { ProxmoxDashboard } from "../../components/proxmox-dashboard"
export default function DashboardPage() {
return (
<main className="min-h-screen bg-background">
<ProxmoxDashboard />
</main>
)
}
+177
View File
@@ -0,0 +1,177 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ===================== */
/* Light Mode (default) */
/* ===================== */
:root {
--background: oklch(1 0 0); /* blanco */
--foreground: oklch(0.145 0 0); /* casi negro */
--card: oklch(1 0 0);
--card-foreground: var(--foreground);
--popover: var(--card);
--popover-foreground: var(--foreground);
--primary: oklch(0.205 0 0); /* gris oscuro */
--primary-foreground: oklch(0.985 0 0); /* blanco */
--secondary: oklch(0.97 0 0);
--secondary-foreground: var(--primary);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); /* gris medio */
--accent: oklch(0.97 0 0);
--accent-foreground: var(--primary);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.145 0 0);
--border: oklch(0.922 0 0);
--input: var(--border);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: var(--primary);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
/* ===================== */
/* Dark Mode (gris) */
/* ===================== */
.dark {
--background: oklch(0.22 0 0); /* gris oscuro */
--foreground: oklch(0.97 0 0); /* blanco/gris claro */
--card: oklch(0.24 0 0);
--card-foreground: var(--foreground);
--popover: var(--card);
--popover-foreground: var(--foreground);
--primary: oklch(0.83 0 0); /* casi blanco */
--primary-foreground: var(--background);
--secondary: oklch(0.28 0 0);
--secondary-foreground: oklch(0.92 0 0);
--muted: oklch(0.26 0 0);
--muted-foreground: oklch(0.72 0 0);
--accent: oklch(0.28 0 0);
--accent-foreground: var(--primary);
--destructive: oklch(0.53 0.25 27);
--destructive-foreground: oklch(0.9 0 0);
--border: oklch(0.34 0 0);
--input: var(--border);
--ring: oklch(0.55 0 0);
--chart-1: oklch(0.60 0.20 255);
--chart-2: oklch(0.70 0.16 165);
--chart-3: oklch(0.76 0.19 70);
--chart-4: oklch(0.63 0.25 305);
--chart-5: oklch(0.66 0.24 20);
--sidebar: oklch(0.24 0 0);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--chart-1);
--sidebar-primary-foreground: var(--foreground);
--sidebar-accent: oklch(0.28 0 0);
--sidebar-accent-foreground: var(--foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
/* ===================== */
/* Base layer */
/* ===================== */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
/* Foco accesible */
:is(button,[role="button"],a,input,select,textarea,[tabindex]:not([tabindex="-1"])):focus {
@apply outline-none;
}
:is(button,[role="button"],a,input,select,textarea,[tabindex]:not([tabindex="-1"])):focus-visible {
@apply ring-2;
--tw-ring-color: var(--ring);
--tw-ring-opacity: 0.5; /* equivalente al /50 */
}
}
/* ===================== */
/* Ajustes para Charts */
/* ===================== */
@layer components {
/* Recharts axis */
.recharts-cartesian-axis-tick tspan {
fill: var(--muted-foreground);
}
.recharts-cartesian-axis-line,
.recharts-cartesian-grid line {
stroke: var(--border);
}
/* Chart.js axis */
.chartjs-render-monitor text {
fill: var(--muted-foreground);
}
.chartjs-render-monitor line {
stroke: var(--border);
}
}
/* ===================== */
/* Ajustes para xterm.js */
/* ===================== */
/* Quitar padding para que la terminal ocupe el 100% del ancho */
.xterm {
padding: 0 !important;
}
/* Por si acaso el viewport añade padding extra */
.xterm .xterm-viewport {
padding: 0 !important;
}
/* Opcional: asegurar que no haya margen raro */
.xterm-rows {
margin: 0 !important;
}
/* ===================== */
/* Progress Animations */
/* ===================== */
@keyframes indeterminate {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}
+5
View File
@@ -0,0 +1,5 @@
import Hardware from "@/components/hardware"
export default function HardwarePage() {
return <Hardware />
}
+52
View File
@@ -0,0 +1,52 @@
import type React from "react"
import type { Metadata, Viewport } from "next"
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import { ThemeProvider } from "../components/theme-provider"
import { Suspense } from "react"
import "./globals.css"
export const metadata: Metadata = {
title: "ProxMenux Monitor",
description: "Proxmox System Dashboard and Monitor",
generator: "v0.app",
manifest: "/manifest.json",
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/icon.svg", type: "image/svg+xml" },
{ url: "/icon.png", type: "image/png", sizes: "32x32" },
],
shortcut: "/favicon.ico",
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
},
}
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#2b2f36" },
],
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-background text-foreground`}>
<Suspense fallback={<div>Loading...</div>}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</Suspense>
</body>
</html>
)
}
+134
View File
@@ -0,0 +1,134 @@
"use client"
import { useState, useEffect } from "react"
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
import { Login } from "../components/login"
import { AuthSetup } from "../components/auth-setup"
import { getApiUrl } from "../lib/api-config"
export default function Home() {
const [authStatus, setAuthStatus] = useState<{
loading: boolean
authEnabled: boolean
authConfigured: boolean
authenticated: boolean
}>({
loading: true,
authEnabled: false,
authConfigured: false,
authenticated: false,
})
useEffect(() => {
checkAuthStatus()
}, [])
const checkAuthStatus = async () => {
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/status"), {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
// 401 here means the token is present but invalid — typically signed
// under a previous jwt_secret (rotated on AppImage upgrade or fresh
// install). If we let this fall into the catch below, the dashboard
// would render and every authenticated component would fire its own
// 401 in parallel, flooding the backend logs and looping reloads.
// Drop the dead token and force the Login screen instead.
if (response.status === 401) {
try {
localStorage.removeItem("proxmenux-auth-token")
} catch {
// private browsing — best-effort
}
setAuthStatus({
loading: false,
authEnabled: true,
authConfigured: true,
authenticated: false,
})
return
}
// Check if response is valid JSON before parsing
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const contentType = response.headers.get("content-type")
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Response is not JSON")
}
const data = await response.json()
const authenticated = data.auth_enabled ? data.authenticated : true
// Clear the 401 cascade-prevention flag when we successfully end
// up in the authenticated state. The flag is meant to dedupe a
// burst of 401s during a single page load; once we've confirmed
// the user is in, a future 401 (token rotation, restart, etc.)
// should be allowed to reload again. Without this, a stale flag
// can prevent the post-2FA dashboard from recovering from any
// transient 401 and leaves the UI blocked.
if (authenticated) {
try {
sessionStorage.removeItem("proxmenux-auth-401-handled")
} catch {
// private browsing — best-effort
}
}
setAuthStatus({
loading: false,
authEnabled: data.auth_enabled,
authConfigured: data.auth_configured,
authenticated,
})
} catch {
// API not available - assume no auth configured (silent fail, no console error)
setAuthStatus({
loading: false,
authEnabled: false,
authConfigured: false,
authenticated: true,
})
}
}
const handleAuthComplete = () => {
checkAuthStatus()
}
const handleLoginSuccess = () => {
checkAuthStatus()
}
if (authStatus.loading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="h-12 w-12 rounded-full border-2 border-muted"></div>
<div className="absolute inset-0 h-12 w-12 rounded-full border-2 border-transparent border-t-primary animate-spin"></div>
</div>
<div className="text-sm font-medium text-foreground">Loading...</div>
<p className="text-xs text-muted-foreground">Connecting to ProxMenux Monitor</p>
</div>
</div>
)
}
if (authStatus.authEnabled && !authStatus.authenticated) {
return <Login onLogin={handleLoginSuccess} />
}
// Show dashboard in all other cases
return (
<>
{!authStatus.authConfigured && <AuthSetup onComplete={handleAuthComplete} />}
<ProxmoxDashboard />
</>
)
}
+223
View File
@@ -0,0 +1,223 @@
"use client"
import Image from "next/image"
import {
Github,
Heart,
BookOpen,
MessageSquare,
Bug,
Sparkles,
Scale,
ExternalLink,
} from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { APP_VERSION } from "./release-notes-modal"
// Issue #191: a dedicated About tab. Centralises project metadata
// (version, license, author) and every external link the project
// already exposes — GitHub, docs, donation. Replaces the lone
// "Support and contribute to the project" footer link with a proper
// information surface that's easy to extend with new social channels
// without re-cluttering the dashboard footer.
interface LinkRow {
label: string
description: string
href: string
Icon: React.ComponentType<{ className?: string }>
accent?: keyof typeof ACCENT_CLASSES
}
// Tailwind only emits classes that appear as literal strings in the
// source. A dynamic `bg-${accent}/10` template does not survive the
// purge step, so each accent maps to a fully-spelled class pair below.
const ACCENT_CLASSES = {
gray: "bg-gray-500/10 text-gray-400",
blue: "bg-blue-500/10 text-blue-500",
purple: "bg-purple-500/10 text-purple-400",
red: "bg-red-500/10 text-red-500",
pink: "bg-pink-500/10 text-pink-500",
} as const
const PROJECT_LINKS: LinkRow[] = [
{
label: "GitHub repository",
description: "Source code, releases and issue tracker.",
href: "https://github.com/MacRimi/ProxMenux",
Icon: Github,
accent: "gray",
},
{
label: "Documentation",
description: "Full user guide for ProxMenux and the Monitor.",
href: "https://proxmenux.com",
Icon: BookOpen,
accent: "blue",
},
{
label: "Discussions",
description: "Ask questions, share custom AI prompts, swap ideas.",
href: "https://github.com/MacRimi/ProxMenux/discussions",
Icon: MessageSquare,
accent: "purple",
},
{
label: "Report a bug or request a feature",
description: "Open an issue on GitHub — bugs, ideas, regressions.",
href: "https://github.com/MacRimi/ProxMenux/issues",
Icon: Bug,
accent: "red",
},
]
const SUPPORT_LINKS: LinkRow[] = [
{
label: "Support the project on Ko-fi",
description: "ProxMenux is free and open source. Donations cover hosting and dev time.",
href: "https://ko-fi.com/macrimi",
Icon: Heart,
accent: "pink",
},
]
function LinkCard({ row }: { row: LinkRow }) {
const accentClass = ACCENT_CLASSES[row.accent ?? "blue"]
// Style mirrors the PCI Devices cards in the Hardware tab: subtle
// translucent background by default, slightly lighter on hover, no
// accent-coloured borders or text colour changes — keeps the look
// consistent with the rest of the project.
return (
<a
href={row.href}
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer flex items-start gap-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
<span
className={`inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md ${accentClass}`}
>
<row.Icon className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
{row.label}
<ExternalLink className="h-3 w-3 text-muted-foreground" />
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">{row.description}</p>
</div>
</a>
)
}
export function About() {
return (
<div className="space-y-4 md:space-y-6">
{/* Hero — logo, name, version, one-line description. */}
<Card>
<CardContent className="pt-6 pb-6">
<div className="flex flex-col md:flex-row items-center md:items-start gap-4 md:gap-6">
<div className="relative w-24 h-24 md:w-28 md:h-28 flex-shrink-0">
<Image
src="/images/proxmenux-logo.png"
alt="ProxMenux logo"
fill
priority
className="object-contain"
/>
</div>
<div className="text-center md:text-left flex-1 min-w-0">
<h2 className="text-2xl md:text-3xl font-semibold text-foreground">
ProxMenux Monitor
</h2>
<p className="text-sm text-muted-foreground mt-1">
A web dashboard and management layer for Proxmox VE health monitoring,
notifications, terminal, optimization tracker and more, packaged as a single
AppImage.
</p>
<div className="flex flex-wrap items-center justify-center md:justify-start gap-2 mt-3">
<span className="inline-flex items-center gap-1.5 rounded-md bg-blue-500/10 text-blue-500 border border-blue-500/30 px-2.5 py-1 text-xs font-mono">
<Sparkles className="h-3 w-3" />
v{APP_VERSION}
</span>
{/* Changelog goes to the web — the in-app modal version
duplicated content and lacked a close affordance on
some viewports, forcing a page refresh. The web
changelog is canonical and auto-syncs with releases. */}
<a
href="https://proxmenux.com/changelog"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-md bg-muted hover:bg-muted/70 transition-colors text-foreground border border-border px-2.5 py-1 text-xs"
>
Changelog
<ExternalLink className="h-3 w-3" />
</a>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Project links — GitHub, docs, discussions, bug tracker. */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Github className="h-4 w-4 text-muted-foreground" />
Project
</CardTitle>
<CardDescription>Repository, documentation and community channels.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{PROJECT_LINKS.map(row => (
<LinkCard key={row.href} row={row} />
))}
</div>
</CardContent>
</Card>
{/* Support + License combined — donation link and licensing
info in one card. The previous layout had a separate "Author"
block that has been removed by request. */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Heart className="h-4 w-4 text-pink-500" />
Support &amp; License
</CardTitle>
<CardDescription>
ProxMenux is free and open source under the GPL-3.0 license. If it&apos;s useful to
you, a one-off contribution helps keep it that way.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-2">
{SUPPORT_LINKS.map(row => (
<LinkCard key={row.href} row={row} />
))}
<a
href="https://github.com/MacRimi/ProxMenux/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer flex items-start gap-3 rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors"
>
<span className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md bg-gray-500/10 text-gray-400">
<Scale className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
GPL-3.0 license
<ExternalLink className="h-3 w-3 text-muted-foreground" />
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">
Free software see the LICENSE file for the full text.
</p>
</div>
</a>
</div>
</CardContent>
</Card>
</div>
)
}
+448
View File
@@ -0,0 +1,448 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Shield, Lock, User, AlertCircle, Eye, EyeOff, Upload, Trash2 } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
interface AuthSetupProps {
onComplete: () => void
}
export function AuthSetup({ onComplete }: AuthSetupProps) {
const [open, setOpen] = useState(false)
const [step, setStep] = useState<"choice" | "setup">("choice")
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
// Profile (Fase 2 — v1.2.2). Both optional decorations on top of the
// mandatory username + password. Persisted via PUT /api/auth/profile
// and POST /api/auth/profile/avatar after the user lands a successful
// /api/auth/setup so we don't change the setup endpoint's contract.
const [displayName, setDisplayName] = useState("")
const [avatarFile, setAvatarFile] = useState<File | null>(null)
const [avatarPreviewUrl, setAvatarPreviewUrl] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
const checkOnboardingStatus = async () => {
try {
const response = await fetch(getApiUrl("/api/auth/status"))
// Check if response is valid JSON before parsing
if (!response.ok) {
// API not available - don't show modal in preview
return
}
const contentType = response.headers.get("content-type")
if (!contentType || !contentType.includes("application/json")) {
return
}
const data = await response.json()
// Show modal if auth is not configured and not declined
if (!data.auth_configured) {
setTimeout(() => setOpen(true), 500)
}
} catch {
// API not available (preview environment) - don't show modal
}
}
checkOnboardingStatus()
}, [])
const handleSkipAuth = async () => {
setLoading(true)
setError("")
try {
const response = await fetch(getApiUrl("/api/auth/skip"), {
method: "POST",
headers: { "Content-Type": "application/json" },
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || "Failed to skip authentication")
}
if (data.auth_declined) {
}
localStorage.setItem("proxmenux-auth-declined", "true")
localStorage.removeItem("proxmenux-auth-token") // Remove any old token
setOpen(false)
onComplete()
} catch (err) {
console.error("[v0] Auth skip error:", err)
setError(err instanceof Error ? err.message : "Failed to save preference")
} finally {
setLoading(false)
}
}
const handleAvatarPick = () => fileInputRef.current?.click()
const handleAvatarChange = (file: File | null) => {
// Revoke the previous local preview so we don't leak blob URLs while
// the user picks another file before submitting.
if (avatarPreviewUrl) {
URL.revokeObjectURL(avatarPreviewUrl)
}
setAvatarFile(file)
setAvatarPreviewUrl(file ? URL.createObjectURL(file) : null)
}
const handleSetupAuth = async () => {
setError("")
if (!username || !password) {
setError("Please fill in all fields")
return
}
if (password !== confirmPassword) {
setError("Passwords do not match")
return
}
if (password.length < 6) {
setError("Password must be at least 6 characters")
return
}
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/setup"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username,
password,
}),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || "Failed to setup authentication")
}
if (data.token) {
localStorage.setItem("proxmenux-auth-token", data.token)
localStorage.removeItem("proxmenux-auth-declined")
}
// Profile decorations (Fase 2). Sent as a follow-up to the setup
// call so the /api/auth/setup endpoint stays minimal (username +
// password only) — these calls reuse the existing profile
// endpoints and the JWT we just received. Failures here are
// non-fatal: the user is already authenticated and can finish
// configuring the profile from the /profile page.
const token = data.token
if (token) {
const trimmedDisplayName = displayName.trim()
if (trimmedDisplayName) {
try {
await fetch(getApiUrl("/api/auth/profile"), {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ display_name: trimmedDisplayName }),
})
} catch (e) {
console.warn("[auth-setup] failed to save display_name:", e)
}
}
if (avatarFile) {
try {
await fetch(getApiUrl("/api/auth/profile/avatar"), {
method: "POST",
headers: {
"Content-Type": avatarFile.type,
Authorization: `Bearer ${token}`,
},
body: avatarFile,
})
} catch (e) {
console.warn("[auth-setup] failed to upload avatar:", e)
}
}
}
// Release the local preview blob now that the file has been
// uploaded (or skipped). The header avatar pulls a fresh copy
// from the backend.
if (avatarPreviewUrl) {
URL.revokeObjectURL(avatarPreviewUrl)
setAvatarPreviewUrl(null)
}
// Notify the header AvatarMenu (mounted on dashboard load with
// auth_enabled=false) to re-fetch its status + profile so the
// avatar appears immediately after first-time setup instead of
// requiring a page refresh.
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("proxmenux:profile-changed"))
}
setOpen(false)
onComplete()
} catch (err) {
console.error("[v0] Auth setup error:", err)
setError(err instanceof Error ? err.message : "Failed to setup authentication")
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogTitle className="sr-only">
{step === "choice" ? "Setup Dashboard Protection" : "Create Password"}
</DialogTitle>
{step === "choice" ? (
<div className="space-y-6 py-2">
<div className="text-center space-y-2">
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
<Shield className="h-8 w-8 text-blue-500" />
</div>
<h2 className="text-2xl font-bold">Protect Your Dashboard?</h2>
<p className="text-muted-foreground text-sm">
Add an extra layer of security to protect your Proxmox data when accessing from non-private networks.
</p>
</div>
<div className="space-y-3">
<Button onClick={() => setStep("setup")} className="w-full bg-blue-500 hover:bg-blue-600" size="lg">
<Lock className="h-4 w-4 mr-2" />
Yes, Setup Password
</Button>
<Button
onClick={handleSkipAuth}
variant="outline"
className="w-full bg-transparent"
size="lg"
disabled={loading}
>
No, Continue Without Protection
</Button>
</div>
<p className="text-xs text-center text-muted-foreground">You can always enable this later in Settings</p>
</div>
) : (
<div className="space-y-6 py-2">
<div className="text-center space-y-2">
<div className="mx-auto w-16 h-16 bg-blue-500/10 rounded-full flex items-center justify-center">
<Lock className="h-8 w-8 text-blue-500" />
</div>
<h2 className="text-2xl font-bold">Setup Authentication</h2>
<p className="text-muted-foreground text-sm">Create a username and password to protect your dashboard</p>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username" className="text-sm">
Username
</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="username"
type="text"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="username"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm">
Password
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Enter password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="new-password"
/>
<Button
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2"
disabled={loading}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password" className="text-sm">
Confirm Password
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="confirm-password"
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="new-password"
/>
<Button
variant="ghost"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2"
disabled={loading}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
{/* Optional profile decorations (Fase 2). Visually
separated from the mandatory credential fields by a
divider + a small heading so the operator understands
they can skip everything below and still complete the
setup. Both are saved with follow-up calls after the
setup endpoint returns the JWT. */}
<div className="pt-3 border-t border-border/60 space-y-4">
<p className="text-xs text-muted-foreground uppercase tracking-wider">
Profile · optional
</p>
<div className="space-y-2">
<Label htmlFor="display-name" className="text-sm">
Display name
</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="display-name"
type="text"
placeholder="Shown above the username in the menu"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
maxLength={64}
className="pl-10 text-base"
disabled={loading}
/>
</div>
<p className="text-[11px] text-muted-foreground">
Leave empty to render the username itself. Up to 64 characters.
</p>
</div>
<div className="space-y-2">
<Label className="text-sm">Avatar</Label>
<div className="flex items-center gap-3">
{avatarPreviewUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatarPreviewUrl}
alt=""
className="w-14 h-14 rounded-full object-cover border border-border bg-cyan-500/5 shrink-0"
/>
) : (
<span className="w-14 h-14 rounded-full bg-cyan-500/15 text-cyan-600 dark:text-cyan-300 flex items-center justify-center text-xl font-semibold border border-border shrink-0">
{(displayName || username || "U").trim().charAt(0).toUpperCase() || "U"}
</span>
)}
<div className="flex flex-col gap-1.5 min-w-0">
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp,image/gif"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0] || null
handleAvatarChange(file)
if (fileInputRef.current) fileInputRef.current.value = ""
}}
/>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAvatarPick}
disabled={loading}
className="h-7 text-xs"
>
<Upload className="h-3 w-3 mr-1.5" />
{avatarFile ? "Change" : "Choose image"}
</Button>
{avatarFile && (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleAvatarChange(null)}
disabled={loading}
className="h-7 text-xs text-red-500 hover:text-red-500 hover:bg-red-500/10"
>
<Trash2 className="h-3 w-3 mr-1.5" />
Clear
</Button>
)}
</div>
<p className="text-[11px] text-muted-foreground">
PNG, JPEG, WebP or GIF · up to 2 MB · pre-crop square for best results.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="space-y-2">
<Button onClick={handleSetupAuth} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Setting up..." : "Setup Authentication"}
</Button>
<Button onClick={() => setStep("choice")} variant="ghost" className="w-full" disabled={loading}>
Back
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
)
}
+281
View File
@@ -0,0 +1,281 @@
"use client"
import { useEffect, useState } from "react"
import { User, Shield, LogOut } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { fetchApi, getApiUrl, getAuthToken } from "../lib/api-config"
interface AuthStatus {
auth_enabled?: boolean
username?: string | null
}
interface ProfileData {
success: boolean
username?: string | null
display_name?: string | null
has_avatar?: boolean
avatar_mtime?: number | null
}
interface AvatarMenuProps {
/** Size of the avatar circle in the header trigger. */
size?: "md" | "lg"
/**
* Callback used by the Security menu item. The Monitor renders its
* Settings/Security panels inside the same dashboard route, not on
* a separate URL, so navigation is handled by the parent that knows
* how to switch tabs. Optional — when omitted the menu item is hidden.
*/
onOpenSecurity?: () => void
/**
* Callback for "View profile". Same rationale: the parent decides how
* to route there (modal, page, tab switch). Until Fase 2 lands the
* caller typically passes an alert/toast that the page is coming.
*/
onOpenProfile?: () => void
}
/**
* AvatarMenu — user/account dropdown for the header.
*
* Self-fetches the current auth status to derive the username and the
* initial that fills the avatar circle. Stays silent (renders nothing)
* when authentication is disabled on this install — no point showing
* an account menu for a "Sign out" that doesn't apply.
*
* Sign out clears the token from localStorage and reloads, mirroring
* the existing `handleLogout` in `security.tsx`. That keeps a single
* source of truth for the logout flow until Fase 2 introduces a
* proper /api/auth/logout that revokes the JWT server-side too.
*/
export function AvatarMenu({ size = "lg", onOpenSecurity, onOpenProfile }: AvatarMenuProps) {
// IMPORTANT — all hooks must run unconditionally on every render. The
// previous version short-circuited with `if (!auth_enabled) return null`
// BEFORE the avatar blob hooks, so the hook count changed between
// renders the moment auth status loaded → React error #310 ("rendered
// more hooks than during the previous render"). All `useState` and
// `useEffect` calls now live above any early return; the null branch
// is at the very end after the hooks.
const [status, setStatus] = useState<AuthStatus | null>(null)
const [profile, setProfile] = useState<ProfileData | null>(null)
const [open, setOpen] = useState(false)
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | null>(null)
// Load both auth_status (to decide whether to render at all) and the
// profile (to render display_name + avatar). Profile is fetched only
// when auth is enabled — saves one roundtrip on installs without
// auth where the menu won't show anyway.
useEffect(() => {
let cancelled = false
fetchApi<AuthStatus>("/api/auth/status")
.then(data => {
if (cancelled) return
setStatus(data)
if (data?.auth_enabled && data?.username) {
fetchApi<ProfileData>("/api/auth/profile")
.then(p => {
if (!cancelled) setProfile(p)
})
.catch(() => {
// Profile fetch is best-effort. Falls back to username + initials.
})
}
})
.catch(() => {
if (!cancelled) setStatus(null)
})
// Reload status + profile when the user updates the profile from
// the /profile page OR completes first-time auth setup. Refreshing
// status is what flips the menu visible after setup (when the
// initial mount saw auth_enabled=false); refreshing profile is
// what makes a new avatar/display name appear without a full
// browser refresh.
const handler = () => {
fetchApi<AuthStatus>("/api/auth/status")
.then(s => {
if (cancelled) return
setStatus(s)
if (s?.auth_enabled && s?.username) {
fetchApi<ProfileData>("/api/auth/profile")
.then(p => {
if (!cancelled) setProfile(p)
})
.catch(() => {})
}
})
.catch(() => {})
}
if (typeof window !== "undefined") {
window.addEventListener("proxmenux:profile-changed", handler)
}
return () => {
cancelled = true
if (typeof window !== "undefined") {
window.removeEventListener("proxmenux:profile-changed", handler)
}
}
}, [])
// Avatar fetch — the endpoint requires the Bearer header, which
// <img src=…> can't send, so we fetch as a blob and convert it to a
// local object URL for rendering. The blob URL is revoked on cleanup
// and on every refetch to avoid leaking memory.
useEffect(() => {
let cancelled = false
let currentBlobUrl: string | null = null
if (profile?.has_avatar) {
const token = getAuthToken()
const url = `${getApiUrl("/api/auth/profile/avatar")}?v=${profile.avatar_mtime || ""}`
fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
.then(r => (r.ok ? r.blob() : null))
.then(blob => {
if (cancelled || !blob) return
currentBlobUrl = URL.createObjectURL(blob)
setAvatarBlobUrl(currentBlobUrl)
})
.catch(() => {
if (!cancelled) setAvatarBlobUrl(null)
})
} else {
setAvatarBlobUrl(null)
}
return () => {
cancelled = true
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl)
}
}, [profile?.has_avatar, profile?.avatar_mtime])
// ── Hooks finished. Safe to early-return now. ──
// Hide the avatar entirely when auth isn't enabled on this install —
// there's no user identity to surface and no Sign out to offer.
if (!status?.auth_enabled || !status?.username) return null
const username = status.username
const displayName = profile?.display_name || username
const initial = displayName.trim().charAt(0).toUpperCase() || "U"
const handleSignOut = () => {
try {
localStorage.removeItem("proxmenux-auth-token")
localStorage.removeItem("proxmenux-auth-setup-complete")
} catch {
// localStorage may be unavailable (private mode); fall through.
}
window.location.reload()
}
// Avatar size in the header trigger. The trigger has no chevron now —
// removing it freed enough horizontal space to bump the avatar a
// notch up (40 → 44 / 32 → 36) without nudging the Refresh / Theme
// buttons sitting to its left.
const avatarSize = size === "lg" ? "w-11 h-11 text-lg" : "w-9 h-9 text-sm"
return (
<>
{/* Backdrop overlay — dim only (no blur). Mounted while the
dropdown is open. `bg-black/40` dims the page enough to focus
attention on the dropdown without distorting the content
behind, which testers found annoying when full backdrop blur
was used (especially on wider desktop viewports). `z-40`
places it above the dashboard content but below the dropdown
portal (`DropdownMenuContent` lands on z-[60]) and below the
header (which stays on z-50 so the avatar trigger remains
clickable). Clicking the backdrop closes the menu — the
explicit `onClick` mirrors Radix's outside-click handler. */}
{open && (
<div
aria-hidden="true"
onClick={() => setOpen(false)}
className="fixed inset-0 z-40 bg-black/40 animate-in fade-in-0 duration-150"
/>
)}
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<button
className="rounded-full hover:ring-2 hover:ring-cyan-500/30 transition-all relative z-50 focus:outline-none focus-visible:outline-none active:outline-none data-[state=open]:outline-none data-[state=open]:ring-0 select-none"
aria-label="Open user menu"
// WebKit ignores `outline` for the tap-highlight overlay
// shown on iOS / Android Chrome after a touch. That overlay
// was the white border that lingered on the avatar after
// dismissing the dropdown without picking anything. Setting
// `-webkit-tap-highlight-color` to transparent suppresses
// it without affecting keyboard focus visibility (handled
// separately by `focus-visible:outline-none` above).
style={{ WebkitTapHighlightColor: "transparent" }}
>
{avatarBlobUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatarBlobUrl}
alt=""
className={`${avatarSize} rounded-full object-cover bg-cyan-500/10`}
/>
) : (
<span
className={`${avatarSize} rounded-full flex items-center justify-center font-semibold bg-cyan-500/15 text-cyan-600 dark:text-cyan-300`}
>
{initial}
</span>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72 z-[60]">
<DropdownMenuLabel>
<div className="flex items-center gap-3 py-1">
{avatarBlobUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatarBlobUrl}
alt=""
className="w-20 h-20 rounded-full object-cover bg-cyan-500/10 shrink-0"
/>
) : (
<span className="w-20 h-20 rounded-full bg-cyan-500/15 text-cyan-600 dark:text-cyan-300 flex items-center justify-center text-3xl font-semibold shrink-0">
{initial}
</span>
)}
<div className="min-w-0">
<div className="text-base font-semibold truncate">{displayName}</div>
{profile?.display_name && (
<div className="text-xs text-muted-foreground truncate">{username}</div>
)}
{!profile?.display_name && (
<div className="text-xs text-muted-foreground truncate">Signed in</div>
)}
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{onOpenProfile && (
<DropdownMenuItem onClick={onOpenProfile}>
<User className="h-4 w-4 mr-2" />
View profile
</DropdownMenuItem>
)}
{onOpenSecurity && (
<DropdownMenuItem onClick={onOpenSecurity}>
<Shield className="h-4 w-4 mr-2" />
Security
</DropdownMenuItem>
)}
{(onOpenProfile || onOpenSecurity) && <DropdownMenuSeparator />}
<DropdownMenuItem
onClick={handleSignOut}
className="text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
>
<LogOut className="h-4 w-4 mr-2" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)
}
@@ -0,0 +1,161 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { Thermometer } from "lucide-react"
import { Badge } from "./ui/badge"
import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts"
import { fetchApi } from "@/lib/api-config"
import { useDiskTempThresholds } from "@/lib/health-thresholds"
interface TempPoint {
timestamp: number
value: number
}
interface DiskTemperatureCardProps {
diskName: string
liveTemperature: number
/** Disk class — "HDD" | "SSD" | "NVMe" | "SAS". Drives the threshold colors. */
diskType: string
/** Click handler — opens the full timeframe-selector modal as drill-down. */
onOpenDetail?: () => void
}
// Disk-temperature thresholds come from the user-configurable backend
// (lib/health-thresholds.ts). The classifier here takes the resolved
// pair so the consumer can read it from the hook once per render.
function statusFor(temp: number, t: { warn: number; hot: number }) {
if (temp <= 0) return { label: "N/A", className: "bg-gray-500/10 text-gray-500 border-gray-500/20", color: "#6b7280" }
if (temp >= t.hot) return { label: "Hot", className: "bg-red-500/10 text-red-500 border-red-500/20", color: "#ef4444" }
if (temp >= t.warn) return { label: "Warm", className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", color: "#f59e0b" }
return { label: "Normal", className: "bg-green-500/10 text-green-500 border-green-500/20", color: "#22c55e" }
}
const MiniTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const ts = payload[0].payload?.timestamp
const date = ts ? new Date(ts * 1000) : null
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-md px-2 py-1 shadow-xl">
{date && (
<p className="text-[10px] text-gray-300">
{date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</p>
)}
<p className="text-xs font-semibold text-white">{payload[0].value}°C</p>
</div>
)
}
return null
}
export function DiskTemperatureCard({
diskName,
liveTemperature,
diskType,
onOpenDetail,
}: DiskTemperatureCardProps) {
const [data, setData] = useState<TempPoint[]>([])
const [loading, setLoading] = useState(true)
const cancelled = useRef(false)
useEffect(() => {
cancelled.current = false
const fetchHistory = async () => {
setLoading(true)
try {
const result = await fetchApi<{ data: TempPoint[] }>(
`/api/disk/${encodeURIComponent(diskName)}/temperature/history?timeframe=hour`,
)
if (cancelled.current) return
setData(result?.data || [])
} catch {
if (!cancelled.current) setData([])
} finally {
if (!cancelled.current) setLoading(false)
}
}
fetchHistory()
// Refresh once a minute so the inline chart tracks the collector
// without needing the user to reopen the modal.
const id = setInterval(fetchHistory, 60_000)
return () => {
cancelled.current = true
clearInterval(id)
}
}, [diskName])
const allThresholds = useDiskTempThresholds()
const dt = (() => {
const t = (diskType || "").toUpperCase()
if (t === "HDD") return allThresholds.HDD
if (t === "NVME") return allThresholds.NVMe
if (t === "SAS") return allThresholds.SAS
return allThresholds.SSD
})()
const status = statusFor(liveTemperature, dt)
const lineColor = status.color
const tempDisplay = liveTemperature > 0 ? `${liveTemperature}°C` : "N/A"
const samples = data.length
const interactive = !!onOpenDetail
const Wrapper: any = interactive ? "button" : "div"
return (
<Wrapper
type={interactive ? "button" : undefined}
onClick={interactive ? onOpenDetail : undefined}
className={[
"w-full text-left border border-white/10 rounded-lg p-3 bg-white/[0.02]",
interactive ? "cursor-pointer hover:bg-white/[0.04] transition-colors focus:outline-none focus:ring-1 focus:ring-white/20" : "",
].join(" ")}
title={interactive ? "Open temperature history" : undefined}
>
<div className="flex items-start justify-between gap-3 mb-1.5">
<div className="min-w-0">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Temperature</p>
<p className="text-xl font-bold leading-tight mt-0.5" style={{ color: lineColor }}>
{tempDisplay}
</p>
</div>
<div className="flex flex-col items-end gap-1 flex-shrink-0">
<Thermometer className="h-3.5 w-3.5" style={{ color: lineColor }} />
<Badge variant="outline" className={`${status.className} text-[10px] px-2 py-0`}>
{status.label}
</Badge>
</div>
</div>
<div className="h-[40px] -mx-1">
{loading ? (
<div className="h-full w-full animate-pulse bg-white/[0.03] rounded" />
) : samples < 2 ? (
<div className="h-full flex items-center justify-center text-[10px] text-muted-foreground">
Collecting samples chart populates after ~2 minutes
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 2, right: 4, left: 4, bottom: 0 }}>
<defs>
<linearGradient id={`diskTempCardGrad-${diskName}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={lineColor} stopOpacity={0.35} />
<stop offset="100%" stopColor={lineColor} stopOpacity={0.02} />
</linearGradient>
</defs>
<Tooltip content={<MiniTooltip />} cursor={{ stroke: lineColor, strokeOpacity: 0.3, strokeWidth: 1 }} />
<Area
type="monotone"
dataKey="value"
stroke={lineColor}
strokeWidth={1.6}
fill={`url(#diskTempCardGrad-${diskName})`}
dot={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</Wrapper>
)
}
@@ -0,0 +1,267 @@
"use client"
import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Thermometer, TrendingDown, TrendingUp, Minus } from "lucide-react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
import { useIsMobile } from "../hooks/use-mobile"
import { fetchApi } from "@/lib/api-config"
import { useDiskTempThresholds, type DiskTempThreshold } from "@/lib/health-thresholds"
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
{ value: "day", label: "24 Hours" },
{ value: "week", label: "7 Days" },
{ value: "month", label: "30 Days" },
]
interface TempHistoryPoint {
timestamp: number
value: number
min?: number
max?: number
}
interface TempStats {
min: number
max: number
avg: number
current: number
}
interface DiskTemperatureDetailModalProps {
open: boolean
onOpenChange: (open: boolean) => void
diskName: string
diskModel?: string
liveTemperature?: number
diskType?: "HDD" | "SSD" | "NVMe" | "SAS" | string
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value}°C</span>
</div>
))}
</div>
</div>
)
}
return null
}
// Per-disk-class thresholds come from the user-configurable backend
// (lib/health-thresholds.ts), so the chart line color stays in sync
// with whatever the user sets in Settings → Health Monitor Thresholds.
function colorFor(temp: number, t: DiskTempThreshold): string {
if (temp >= t.hot) return "#ef4444"
if (temp >= t.warn) return "#f59e0b"
return "#22c55e"
}
function statusInfoFor(temp: number, t: DiskTempThreshold) {
if (temp <= 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
if (temp >= t.hot) return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
if (temp >= t.warn) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
}
export function DiskTemperatureDetailModal({
open,
onOpenChange,
diskName,
diskModel,
liveTemperature,
diskType,
}: DiskTemperatureDetailModalProps) {
const [timeframe, setTimeframe] = useState("day")
const [data, setData] = useState<TempHistoryPoint[]>([])
const [stats, setStats] = useState<TempStats>({ min: 0, max: 0, avg: 0, current: 0 })
const [loading, setLoading] = useState(true)
const isMobile = useIsMobile()
useEffect(() => {
if (open && diskName) {
fetchHistory()
}
}, [open, timeframe, diskName])
const fetchHistory = async () => {
setLoading(true)
try {
const result = await fetchApi<{ data: TempHistoryPoint[]; stats: TempStats }>(
`/api/disk/${encodeURIComponent(diskName)}/temperature/history?timeframe=${timeframe}`,
)
if (result && result.data) {
setData(result.data)
setStats(result.stats)
} else {
setData([])
setStats({ min: 0, max: 0, avg: 0, current: 0 })
}
} catch (err) {
console.error("[ProxMenux] Failed to fetch disk temperature history:", err)
setData([])
} finally {
setLoading(false)
}
}
const formatTime = (timestamp: number) => {
const date = new Date(timestamp * 1000)
if (timeframe === "hour" || timeframe === "day") {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
}
return date.toLocaleDateString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
}
const chartData = data.map((d) => ({ ...d, time: formatTime(d.timestamp) }))
const currentTemp = liveTemperature && liveTemperature > 0 ? Math.round(liveTemperature * 10) / 10 : stats.current
const allThresholds = useDiskTempThresholds()
const dt: DiskTempThreshold = (() => {
const t = (diskType || "").toUpperCase()
if (t === "HDD") return allThresholds.HDD
if (t === "NVME") return allThresholds.NVMe
if (t === "SAS") return allThresholds.SAS
return allThresholds.SSD
})()
const chartColor = colorFor(currentTemp, dt)
const currentStatus = statusInfoFor(currentTemp, dt)
const values = data.map((d) => d.value)
const yMin = values.length > 0 ? Math.max(0, Math.floor(Math.min(...values) - 3)) : 0
const yMax = values.length > 0 ? Math.ceil(Math.max(...values) + 3) : 100
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl bg-card border-border px-3 sm:px-6">
<DialogHeader>
{/*
Header layout mirrors temperature-detail-modal exactly so the
mobile breakpoints behave the same. Earlier we tried to inline
the model name in the DialogTitle, but the long WD/Samsung
strings broke `truncate` and pushed the dialog past the
viewport — clipping the timeframe selector and the right two
stat cards. Keeping the title short and parking the model in
a second line (DialogDescription) lets the standard mobile
grid render correctly.
*/}
<div className="flex items-center justify-between pr-6">
<DialogTitle className="text-foreground flex items-center gap-2">
<Thermometer className="h-5 w-5" />
/dev/{diskName}
</DialogTitle>
<Select value={timeframe} onValueChange={setTimeframe}>
<SelectTrigger className="w-[130px] bg-card border-border">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{diskModel && (
<p className="text-xs text-muted-foreground truncate pr-6 mt-0.5">{diskModel}</p>
)}
</DialogHeader>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3">
<div className={`rounded-lg p-3 text-center border ${currentStatus.color}`}>
<div className="text-xs opacity-80 mb-1">Current</div>
<div className="text-lg font-bold">{currentTemp > 0 ? `${currentTemp}°C` : "N/A"}</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground mb-1 flex items-center justify-center gap-1">
<TrendingDown className="h-3 w-3" /> Min
</div>
<div className="text-lg font-bold text-green-500">{stats.min}°C</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground mb-1 flex items-center justify-center gap-1">
<Minus className="h-3 w-3" /> Avg
</div>
<div className="text-lg font-bold text-foreground">{stats.avg}°C</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground mb-1 flex items-center justify-center gap-1">
<TrendingUp className="h-3 w-3" /> Max
</div>
<div className="text-lg font-bold text-red-500">{stats.max}°C</div>
</div>
</div>
<div className="h-[300px] lg:h-[350px]">
{loading ? (
<div className="h-full flex items-center justify-center">
<div className="space-y-3 w-full animate-pulse">
<div className="h-4 bg-muted rounded w-1/4 mx-auto" />
<div className="h-[250px] bg-muted/50 rounded" />
</div>
</div>
) : chartData.length === 0 ? (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Thermometer className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No temperature data yet for this disk</p>
<p className="text-sm mt-1">Samples are collected every 60 seconds</p>
</div>
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id={`diskTempGradient-${diskName}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={chartColor} stopOpacity={0.3} />
<stop offset="100%" stopColor={chartColor} stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: isMobile ? 10 : 12 }}
interval="preserveStartEnd"
minTickGap={isMobile ? 40 : 60}
/>
<YAxis
domain={[yMin, yMax]}
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: isMobile ? 10 : 12 }}
tickFormatter={(v) => `${v}°`}
width={isMobile ? 40 : 45}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="value"
name="Temperature"
stroke={chartColor}
strokeWidth={2}
fill={`url(#diskTempGradient-${diskName})`}
dot={false}
activeDot={{ r: 4, fill: chartColor, stroke: "#fff", strokeWidth: 2 }}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,399 @@
"use client"
import { cn } from "@/lib/utils"
interface SriovInfo {
role: "vf" | "pf-active" | "pf-idle"
physfn?: string // VF only: parent PF BDF
vfCount?: number // PF only: active VF count
totalvfs?: number // PF only: maximum VFs
}
interface GpuSwitchModeIndicatorProps {
mode: "lxc" | "vm" | "sriov" | "unknown"
isEditing?: boolean
pendingMode?: "lxc" | "vm" | null
onToggle?: (e: React.MouseEvent) => void
className?: string
sriovInfo?: SriovInfo
}
export function GpuSwitchModeIndicator({
mode,
isEditing = false,
pendingMode = null,
onToggle,
className,
sriovInfo,
}: GpuSwitchModeIndicatorProps) {
// SR-IOV is a non-editable hardware state. Pending toggles don't apply here.
const displayMode = mode === "sriov" ? "sriov" : (pendingMode ?? mode)
const isLxcActive = displayMode === "lxc"
const isVmActive = displayMode === "vm"
const isSriovActive = displayMode === "sriov"
const hasChanged =
mode !== "sriov" && pendingMode !== null && pendingMode !== mode
// Colors
const sriovColor = "#14b8a6" // teal-500
const activeColor = isSriovActive
? sriovColor
: isLxcActive
? "#3b82f6"
: isVmActive
? "#a855f7"
: "#6b7280"
const inactiveColor = "#374151" // gray-700 for dark theme
const dimmedColor = "#4b5563" // gray-600 for dashed SR-IOV branches
const lxcColor = isLxcActive ? "#3b82f6" : inactiveColor
const vmColor = isVmActive ? "#a855f7" : inactiveColor
const handleClick = (e: React.MouseEvent) => {
// SR-IOV state can't be toggled — swallow the click so it doesn't reach
// the card (which would open the detail modal unexpectedly from this
// area). For lxc/vm, preserve the original behavior.
if (isSriovActive) {
e.stopPropagation()
return
}
if (isEditing) {
e.stopPropagation()
if (onToggle) {
onToggle(e)
}
}
// When not editing, let the click propagate to the card to open the modal
}
// Build the VF count label shown in the SR-IOV badge. For PFs we know
// exactly how many VFs are active; for a VF we show its parent PF.
const sriovBadgeText = (() => {
if (!isSriovActive) return ""
if (sriovInfo?.role === "vf") return "SR-IOV VF"
if (sriovInfo?.vfCount && sriovInfo.vfCount > 0) return `SR-IOV ×${sriovInfo.vfCount}`
return "SR-IOV"
})()
return (
<div
className={cn(
// On very narrow containers (mobile, narrow modal), stack the SVG
// above the status text so the 224px-wide SVG doesn't squeeze the
// text into a 2-character-wide column. At sm+ we go back to the
// original side-by-side layout.
"flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:gap-6",
isEditing && !isSriovActive && "cursor-pointer",
className
)}
onClick={handleClick}
>
{/* Large SVG Diagram */}
<svg
viewBox="0 0 220 100"
className="h-24 w-56 flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
>
{/* GPU Chip - Large with "GPU" text */}
<g transform="translate(0, 22)">
{/* Main chip body */}
<rect
x="4"
y="8"
width="44"
height="36"
rx="6"
fill={`${activeColor}20`}
stroke={activeColor}
strokeWidth="2.5"
className="transition-all duration-300"
/>
{/* Chip pins - top */}
<line x1="14" y1="2" x2="14" y2="8" stroke={activeColor} strokeWidth="2.5" strokeLinecap="round" className="transition-all duration-300" />
<line x1="26" y1="2" x2="26" y2="8" stroke={activeColor} strokeWidth="2.5" strokeLinecap="round" className="transition-all duration-300" />
<line x1="38" y1="2" x2="38" y2="8" stroke={activeColor} strokeWidth="2.5" strokeLinecap="round" className="transition-all duration-300" />
{/* Chip pins - bottom */}
<line x1="14" y1="44" x2="14" y2="50" stroke={activeColor} strokeWidth="2.5" strokeLinecap="round" className="transition-all duration-300" />
<line x1="26" y1="44" x2="26" y2="50" stroke={activeColor} strokeWidth="2.5" strokeLinecap="round" className="transition-all duration-300" />
<line x1="38" y1="44" x2="38" y2="50" stroke={activeColor} strokeWidth="2.5" strokeLinecap="round" className="transition-all duration-300" />
{/* GPU text */}
<text
x="26"
y="32"
textAnchor="middle"
fill={activeColor}
className="text-[14px] font-bold transition-all duration-300"
style={{ fontFamily: 'system-ui, sans-serif' }}
>
GPU
</text>
</g>
{/* Connection line from GPU to switch */}
<line
x1="52"
y1="50"
x2="78"
y2="50"
stroke={activeColor}
strokeWidth="3"
strokeLinecap="round"
className="transition-all duration-300"
/>
{/* Central Switch Node - Large circle with inner dot */}
<circle
cx="95"
cy="50"
r="14"
fill={isEditing && !isSriovActive ? "#f59e0b20" : `${activeColor}20`}
stroke={isEditing && !isSriovActive ? "#f59e0b" : activeColor}
strokeWidth="3"
className="transition-all duration-300"
/>
<circle
cx="95"
cy="50"
r="6"
fill={isEditing && !isSriovActive ? "#f59e0b" : activeColor}
className="transition-all duration-300"
/>
{/* LXC Branch Line - going up-right.
In SR-IOV mode the branch is dashed + dimmed to show that the
target is theoretically reachable via a VF but not controlled
by ProxMenux. */}
<path
d="M 109 42 L 135 20"
fill="none"
stroke={isSriovActive ? dimmedColor : lxcColor}
strokeWidth={isLxcActive ? "3.5" : "2"}
strokeLinecap="round"
strokeDasharray={isSriovActive ? "3 3" : undefined}
className="transition-all duration-300"
/>
{/* VM Branch Line - going down-right (dashed/dimmed in SR-IOV). */}
<path
d="M 109 58 L 135 80"
fill="none"
stroke={isSriovActive ? dimmedColor : vmColor}
strokeWidth={isVmActive ? "3.5" : "2"}
strokeLinecap="round"
strokeDasharray={isSriovActive ? "3 3" : undefined}
className="transition-all duration-300"
/>
{/* SR-IOV in-line connector + badge (only when mode === 'sriov').
A horizontal line from the switch node leads to a pill-shaped
badge carrying the "SR-IOV ×N" label. Placed on the GPU's
baseline to visually read as an in-line extension, not as a
third branch. */}
{isSriovActive && (
<>
<line
x1="109"
y1="50"
x2="130"
y2="50"
stroke={sriovColor}
strokeWidth="3"
strokeLinecap="round"
className="transition-all duration-300"
/>
<rect
x="132"
y="40"
width="60"
height="20"
rx="10"
fill={`${sriovColor}25`}
stroke={sriovColor}
strokeWidth="2"
className="transition-all duration-300"
/>
<text
x="162"
y="54"
textAnchor="middle"
fill={sriovColor}
className="text-[11px] font-bold transition-all duration-300"
style={{ fontFamily: 'system-ui, sans-serif' }}
>
{sriovBadgeText}
</text>
</>
)}
{/* LXC Container Icon - dimmed/smaller in SR-IOV mode. */}
{!isSriovActive && (
<g transform="translate(138, 2)">
<rect
x="0"
y="0"
width="32"
height="28"
rx="4"
fill={isLxcActive ? `${lxcColor}25` : "transparent"}
stroke={lxcColor}
strokeWidth={isLxcActive ? "2.5" : "1.5"}
className="transition-all duration-300"
/>
<line x1="0" y1="10" x2="32" y2="10" stroke={lxcColor} strokeWidth={isLxcActive ? "1.5" : "1"} className="transition-all duration-300" />
<line x1="0" y1="19" x2="32" y2="19" stroke={lxcColor} strokeWidth={isLxcActive ? "1.5" : "1"} className="transition-all duration-300" />
<circle cx="7" cy="5" r="2" fill={lxcColor} className="transition-all duration-300" />
<circle cx="7" cy="14.5" r="2" fill={lxcColor} className="transition-all duration-300" />
<circle cx="7" cy="23.5" r="2" fill={lxcColor} className="transition-all duration-300" />
</g>
)}
{/* SR-IOV: compact dimmed LXC glyph so the geometry stays recognizable
but it's clearly not the active target. */}
{isSriovActive && (
<g transform="translate(138, 6)" opacity="0.35">
<rect x="0" y="0" width="20" height="18" rx="3" fill="transparent" stroke={dimmedColor} strokeWidth="1.5" />
<line x1="0" y1="6" x2="20" y2="6" stroke={dimmedColor} strokeWidth="1" />
<line x1="0" y1="12" x2="20" y2="12" stroke={dimmedColor} strokeWidth="1" />
</g>
)}
{/* LXC Label */}
{!isSriovActive && (
<text
x="188"
y="22"
textAnchor="start"
fill={lxcColor}
className={cn(
"transition-all duration-300",
isLxcActive ? "text-[14px] font-bold" : "text-[12px] font-medium"
)}
style={{ fontFamily: 'system-ui, sans-serif' }}
>
LXC
</text>
)}
{isSriovActive && (
<text
x="162"
y="16"
fill={dimmedColor}
className="text-[9px] font-medium"
style={{ fontFamily: 'system-ui, sans-serif' }}
>
LXC
</text>
)}
{/* VM Monitor Icon - active view */}
{!isSriovActive && (
<g transform="translate(138, 65)">
<rect
x="2"
y="0"
width="28"
height="18"
rx="3"
fill={isVmActive ? `${vmColor}25` : "transparent"}
stroke={vmColor}
strokeWidth={isVmActive ? "2.5" : "1.5"}
className="transition-all duration-300"
/>
<rect
x="5"
y="3"
width="22"
height="12"
rx="1"
fill={isVmActive ? `${vmColor}30` : `${vmColor}10`}
className="transition-all duration-300"
/>
<line x1="16" y1="18" x2="16" y2="24" stroke={vmColor} strokeWidth={isVmActive ? "2.5" : "1.5"} strokeLinecap="round" className="transition-all duration-300" />
<line x1="8" y1="24" x2="24" y2="24" stroke={vmColor} strokeWidth={isVmActive ? "2.5" : "1.5"} strokeLinecap="round" className="transition-all duration-300" />
</g>
)}
{/* SR-IOV: compact dimmed VM monitor glyph, mirror of the LXC glyph. */}
{isSriovActive && (
<g transform="translate(138, 72)" opacity="0.35">
<rect x="0" y="0" width="20" height="13" rx="2" fill="transparent" stroke={dimmedColor} strokeWidth="1.5" />
<line x1="10" y1="13" x2="10" y2="17" stroke={dimmedColor} strokeWidth="1.5" strokeLinecap="round" />
<line x1="5" y1="17" x2="15" y2="17" stroke={dimmedColor} strokeWidth="1.5" strokeLinecap="round" />
</g>
)}
{/* VM Label */}
{!isSriovActive && (
<text
x="188"
y="84"
textAnchor="start"
fill={vmColor}
className={cn(
"transition-all duration-300",
isVmActive ? "text-[14px] font-bold" : "text-[12px] font-medium"
)}
style={{ fontFamily: 'system-ui, sans-serif' }}
>
VM
</text>
)}
{isSriovActive && (
<text
x="162"
y="82"
fill={dimmedColor}
className="text-[9px] font-medium"
style={{ fontFamily: 'system-ui, sans-serif' }}
>
VM
</text>
)}
</svg>
{/* Status Text - Large like GPU name */}
<div className="flex flex-col gap-1 min-w-0 flex-1">
<span
className={cn(
"text-base font-semibold transition-all duration-300",
isSriovActive
? "text-teal-500"
: isLxcActive
? "text-blue-500"
: isVmActive
? "text-purple-500"
: "text-muted-foreground"
)}
>
{isSriovActive
? "SR-IOV active"
: isLxcActive
? "Ready for LXC containers"
: isVmActive
? "Ready for VM passthrough"
: "Mode unknown"}
</span>
<span className="text-sm text-muted-foreground">
{isSriovActive
? "Virtual Functions managed externally"
: isLxcActive
? "Native driver active"
: isVmActive
? "VFIO-PCI driver active"
: "No driver detected"}
</span>
{isSriovActive && sriovInfo && (
<span className="text-xs font-mono text-teal-600/80 dark:text-teal-400/80">
{sriovInfo.role === "vf"
? `Virtual Function${sriovInfo.physfn ? ` · parent PF ${sriovInfo.physfn}` : ""}`
: sriovInfo.vfCount !== undefined
? `1 PF + ${sriovInfo.vfCount} VF${sriovInfo.vfCount === 1 ? "" : "s"}${sriovInfo.totalvfs ? ` / ${sriovInfo.totalvfs} max` : ""}`
: null}
</span>
)}
{hasChanged && (
<span className="text-sm text-amber-500 font-medium animate-pulse">
Change pending...
</span>
)}
</div>
</div>
)
}
+114
View File
@@ -0,0 +1,114 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
import { Cpu } from "@/components/icons/cpu" // Added import for Cpu
import type { PCIDevice } from "../types/hardware" // Fixed import to use relative path instead of alias
import { Progress } from "@/components/ui/progress"
function GPUCard({ device }: { device: PCIDevice }) {
const hasMonitoring = device.gpu_temperature !== undefined || device.gpu_utilization !== undefined
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Cpu className="h-5 w-5" />
{device.device}
</CardTitle>
<CardDescription>{device.vendor}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="text-muted-foreground">Slot</div>
<div className="font-medium">{device.slot}</div>
</div>
{device.driver && (
<div>
<div className="text-muted-foreground">Driver</div>
<div className="font-medium">{device.driver}</div>
</div>
)}
{device.gpu_driver_version && (
<div>
<div className="text-muted-foreground">Driver Version</div>
<div className="font-medium">{device.gpu_driver_version}</div>
</div>
)}
{device.gpu_memory && (
<div>
<div className="text-muted-foreground">Memory</div>
<div className="font-medium">{device.gpu_memory}</div>
</div>
)}
{device.gpu_compute_capability && (
<div>
<div className="text-muted-foreground">Compute Capability</div>
<div className="font-medium">{device.gpu_compute_capability}</div>
</div>
)}
</div>
{hasMonitoring && (
<div className="space-y-3 pt-4 border-t">
<h4 className="text-sm font-semibold">Real-time Monitoring</h4>
{device.gpu_temperature !== undefined && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Temperature</span>
<span className="font-medium">{device.gpu_temperature}°C</span>
</div>
<Progress value={(device.gpu_temperature / 100) * 100} className="h-2" />
</div>
)}
{device.gpu_utilization !== undefined && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">GPU Utilization</span>
<span className="font-medium">{device.gpu_utilization}%</span>
</div>
<Progress value={device.gpu_utilization} className="h-2" />
</div>
)}
{device.gpu_memory_used && device.gpu_memory_total && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Memory Usage</span>
<span className="font-medium">
{device.gpu_memory_used} / {device.gpu_memory_total}
</span>
</div>
<Progress
value={(Number.parseInt(device.gpu_memory_used) / Number.parseInt(device.gpu_memory_total)) * 100}
className="h-2"
/>
</div>
)}
{device.gpu_power_draw && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Power Draw</span>
<span className="font-medium">{device.gpu_power_draw}</span>
</div>
)}
{device.gpu_clock_speed && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">GPU Clock</span>
<span className="font-medium">{device.gpu_clock_speed}</span>
</div>
)}
{device.gpu_memory_clock && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Memory Clock</span>
<span className="font-medium">{device.gpu_memory_clock}</span>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}
File diff suppressed because it is too large Load Diff
+842
View File
@@ -0,0 +1,842 @@
"use client"
import type React from "react"
import { useState, useEffect, useCallback } from "react"
import { getAuthToken } from "@/lib/api-config"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Loader2,
CheckCircle2,
AlertTriangle,
XCircle,
Info,
Activity,
Cpu,
MemoryStick,
HardDrive,
Disc,
Network,
Box,
Settings,
FileText,
RefreshCw,
Shield,
X,
Clock,
BellOff,
ChevronRight,
Settings2,
HelpCircle,
} from "lucide-react"
interface CategoryCheck {
status: string
reason?: string
details?: any
checks?: Record<string, { status: string; detail: string; [key: string]: any }>
dismissable?: boolean
[key: string]: any
}
interface DismissedError {
error_key: string
category: string
severity: string
reason: string
dismissed: boolean
permanent?: boolean
suppression_remaining_hours: number
suppression_hours?: number
resolved_at: string
}
interface CustomSuppression {
key: string
label: string
category: string
icon: string
hours: number
}
interface HealthDetails {
overall: string
summary: string
details: {
cpu: CategoryCheck
memory: CategoryCheck
storage: CategoryCheck
disks: CategoryCheck
network: CategoryCheck
vms: CategoryCheck
services: CategoryCheck
logs: CategoryCheck
updates: CategoryCheck
security: CategoryCheck
}
timestamp: string
}
interface FullHealthData {
health: HealthDetails
active_errors: any[]
dismissed: DismissedError[]
custom_suppressions: CustomSuppression[]
timestamp: string
}
interface HealthStatusModalProps {
open: boolean
onOpenChange: (open: boolean) => void
getApiUrl: (path: string) => string
}
const CATEGORIES = [
{ key: "cpu", category: "temperature", label: "CPU Usage & Temperature", Icon: Cpu },
{ key: "memory", category: "memory", label: "Memory & Swap", Icon: MemoryStick },
{ key: "storage", category: "storage", label: "Storage Mounts & Space", Icon: HardDrive },
{ key: "disks", category: "disks", label: "Disk I/O & Errors", Icon: Disc },
{ key: "network", category: "network", label: "Network Interfaces", Icon: Network },
{ key: "vms", category: "vms", label: "VMs & Containers", Icon: Box },
{ key: "services", category: "pve_services", label: "PVE Services", Icon: Settings },
{ key: "logs", category: "logs", label: "System Logs", Icon: FileText },
{ key: "updates", category: "updates", label: "System Updates", Icon: RefreshCw },
{ key: "security", category: "security", label: "Security & Certificates", Icon: Shield },
]
export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) {
const [loading, setLoading] = useState(true)
const [healthData, setHealthData] = useState<HealthDetails | null>(null)
const [dismissedItems, setDismissedItems] = useState<DismissedError[]>([])
const [customSuppressions, setCustomSuppressions] = useState<CustomSuppression[]>([])
const [error, setError] = useState<string | null>(null)
const [dismissingKey, setDismissingKey] = useState<string | null>(null)
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
const fetchHealthDetails = useCallback(async () => {
setLoading(true)
setError(null)
try {
let newOverallStatus = "OK"
// Use the new combined endpoint for fewer round-trips
const token = getAuthToken()
const authHeaders: Record<string, string> = {}
if (token) {
authHeaders["Authorization"] = `Bearer ${token}`
}
const response = await fetch(getApiUrl("/api/health/full"), { headers: authHeaders })
let infoCount = 0
if (!response.ok) {
// Fallback to legacy endpoint
const legacyResponse = await fetch(getApiUrl("/api/health/details"), { headers: authHeaders })
if (!legacyResponse.ok) throw new Error("Failed to fetch health details")
const data = await legacyResponse.json()
setHealthData(data)
setDismissedItems([])
setCustomSuppressions([])
newOverallStatus = data?.overall || "OK"
// Count INFO categories from legacy data
if (data?.details) {
CATEGORIES.forEach(({ key }) => {
const cat = data.details[key as keyof typeof data.details]
if (cat && cat.status?.toUpperCase() === "INFO") {
infoCount++
}
})
}
} else {
const fullData: FullHealthData = await response.json()
setHealthData(fullData.health)
setDismissedItems(fullData.dismissed || [])
setCustomSuppressions(fullData.custom_suppressions || [])
newOverallStatus = fullData.health?.overall || "OK"
// Get categories that have dismissed items (these become INFO)
const customCats = new Set((fullData.custom_suppressions || []).map((cs: { category: string }) => cs.category))
const filteredDismissed = (fullData.dismissed || []).filter((item: { category: string }) => !customCats.has(item.category))
const categoriesWithDismissed = new Set<string>()
filteredDismissed.forEach((item: { category: string }) => {
const catMeta = CATEGORIES.find(c => c.category === item.category || c.key === item.category)
if (catMeta) {
categoriesWithDismissed.add(catMeta.key)
}
})
// Count effective INFO categories (original INFO + OK categories with dismissed)
if (fullData.health?.details) {
CATEGORIES.forEach(({ key }) => {
const cat = fullData.health.details[key as keyof typeof fullData.health.details]
if (cat) {
const originalStatus = cat.status?.toUpperCase()
// Count as INFO if: originally INFO OR (originally OK and has dismissed items)
if (originalStatus === "INFO" || (originalStatus === "OK" && categoriesWithDismissed.has(key))) {
infoCount++
}
}
})
}
}
const totalInfoCount = infoCount
// Emit event with the FRESH data from the response, not the stale state
const event = new CustomEvent("healthStatusUpdated", {
detail: { status: newOverallStatus, infoCount: totalInfoCount },
})
window.dispatchEvent(event)
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error")
} finally {
setLoading(false)
}
}, [getApiUrl])
// Tick counter to force re-render every 30s so "X minutes ago" stays current
const [, setTick] = useState(0)
useEffect(() => {
if (!open) return
const tickInterval = setInterval(() => setTick(t => t + 1), 30000)
return () => clearInterval(tickInterval)
}, [open])
useEffect(() => {
if (open) {
fetchHealthDetails()
// Auto-refresh every 5 minutes while modal is open
const refreshInterval = setInterval(fetchHealthDetails, 300000)
return () => clearInterval(refreshInterval)
}
}, [open, fetchHealthDetails])
// Auto-expand non-OK categories when data loads
useEffect(() => {
if (healthData?.details) {
const nonOkCategories = new Set<string>()
CATEGORIES.forEach(({ key }) => {
const cat = healthData.details[key as keyof typeof healthData.details]
if (cat && cat.status?.toUpperCase() !== "OK") {
// Updates section: only auto-expand on WARNING+, not INFO
if (key === "updates" && cat.status?.toUpperCase() === "INFO") {
return
}
nonOkCategories.add(key)
}
})
setExpandedCategories(nonOkCategories)
}
}, [healthData])
const toggleCategory = (key: string) => {
setExpandedCategories(prev => {
const next = new Set(prev)
if (next.has(key)) {
next.delete(key)
} else {
next.add(key)
}
return next
})
}
const getStatusIcon = (status: string, size: "sm" | "md" = "md") => {
const statusUpper = status?.toUpperCase()
const cls = size === "sm" ? "h-4 w-4" : "h-5 w-5"
switch (statusUpper) {
case "OK":
return <CheckCircle2 className={`${cls} text-green-500`} />
case "INFO":
return <Info className={`${cls} text-blue-500`} />
case "WARNING":
return <AlertTriangle className={`${cls} text-yellow-500`} />
case "CRITICAL":
return <XCircle className={`${cls} text-red-500`} />
case "UNKNOWN":
return <HelpCircle className={`${cls} text-amber-400`} />
default:
return <Activity className={`${cls} text-muted-foreground`} />
}
}
const getStatusBadge = (status: string) => {
const statusUpper = status?.toUpperCase()
switch (statusUpper) {
case "OK":
return <Badge className="bg-green-500 text-white hover:bg-green-500">OK</Badge>
case "INFO":
return <Badge className="bg-blue-500 text-white hover:bg-blue-500">Info</Badge>
case "WARNING":
return <Badge className="bg-yellow-500 text-white hover:bg-yellow-500">Warning</Badge>
case "CRITICAL":
return <Badge className="bg-red-500 text-white hover:bg-red-500">Critical</Badge>
case "UNKNOWN":
return <Badge className="bg-amber-500 text-white hover:bg-amber-500">UNKNOWN</Badge>
default:
return <Badge>Unknown</Badge>
}
}
// Get categories that have dismissed items (to show as INFO)
const getCategoriesWithDismissed = () => {
const customCats = new Set(customSuppressions.map(cs => cs.category))
const filteredDismissed = dismissedItems.filter(item => !customCats.has(item.category))
const categoriesWithDismissed = new Set<string>()
filteredDismissed.forEach(item => {
// Map dismissed category to our CATEGORIES keys
const catMeta = CATEGORIES.find(c => c.category === item.category || c.key === item.category)
if (catMeta) {
categoriesWithDismissed.add(catMeta.key)
}
})
return categoriesWithDismissed
}
const categoriesWithDismissed = getCategoriesWithDismissed()
// Get effective status for a category (considers dismissed items)
const getEffectiveStatus = (key: string, originalStatus: string) => {
// If category has dismissed items and original status is OK, show as INFO
if (categoriesWithDismissed.has(key) && originalStatus?.toUpperCase() === "OK") {
return "INFO"
}
return originalStatus?.toUpperCase() || "UNKNOWN"
}
const getHealthStats = () => {
if (!healthData?.details) return { total: 0, healthy: 0, info: 0, warnings: 0, critical: 0, unknown: 0 }
let healthy = 0
let info = 0
let warnings = 0
let critical = 0
let unknown = 0
CATEGORIES.forEach(({ key }) => {
const categoryData = healthData.details[key as keyof typeof healthData.details]
if (categoryData) {
const effectiveStatus = getEffectiveStatus(key, categoryData.status)
if (effectiveStatus === "OK") healthy++
else if (effectiveStatus === "INFO") info++
else if (effectiveStatus === "WARNING") warnings++
else if (effectiveStatus === "CRITICAL") critical++
else if (effectiveStatus === "UNKNOWN") unknown++
}
})
return { total: CATEGORIES.length, healthy, info, warnings, critical, unknown }
}
const stats = getHealthStats()
const handleCategoryClick = (categoryKey: string, status: string) => {
if (status === "OK" || status === "INFO") return
onOpenChange(false)
const categoryToTab: Record<string, string> = {
storage: "storage",
disks: "storage",
network: "network",
vms: "vms",
logs: "logs",
hardware: "hardware",
services: "hardware",
}
const targetTab = categoryToTab[categoryKey]
if (targetTab) {
const event = new CustomEvent("changeTab", { detail: { tab: targetTab } })
window.dispatchEvent(event)
}
}
const handleAcknowledge = async (errorKey: string, e: React.MouseEvent) => {
e.stopPropagation()
setDismissingKey(errorKey)
try {
const url = getApiUrl("/api/health/acknowledge")
const token = getAuthToken()
const headers: Record<string, string> = { "Content-Type": "application/json" }
if (token) {
headers["Authorization"] = `Bearer ${token}`
}
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({ error_key: errorKey }),
})
const responseData = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(responseData.error || `Failed to dismiss error (${response.status})`)
}
// Optimistically update local state to avoid slow re-fetch
// Add the dismissed item to the local list immediately
if (responseData.result || responseData.success) {
const dismissedItem = {
error_key: errorKey,
category: responseData.result?.category || responseData.category || '',
severity: responseData.result?.original_severity || 'WARNING',
reason: 'Dismissed by user',
dismissed: true,
acknowledged_at: new Date().toISOString()
}
setDismissedItems(prev => [...prev, dismissedItem])
}
// Fetch fresh data in background (non-blocking)
fetchHealthDetails().catch(() => {})
} catch (err) {
console.error("Error dismissing:", err)
} finally {
setDismissingKey(null)
}
}
const getTimeSinceCheck = () => {
if (!healthData?.timestamp) return null
const checkTime = new Date(healthData.timestamp)
const now = new Date()
const diffMs = now.getTime() - checkTime.getTime()
const diffMin = Math.floor(diffMs / 60000)
if (diffMin < 1) return "just now"
if (diffMin === 1) return "1 minute ago"
if (diffMin < 60) return `${diffMin} minutes ago`
const diffHours = Math.floor(diffMin / 60)
return `${diffHours}h ${diffMin % 60}m ago`
}
const getCategoryRowStyle = (status: string) => {
const s = status?.toUpperCase()
if (s === "CRITICAL") return "bg-red-500/5 border-red-500/20 hover:bg-red-500/10 cursor-pointer"
if (s === "WARNING") return "bg-yellow-500/5 border-yellow-500/20 hover:bg-yellow-500/10 cursor-pointer"
if (s === "UNKNOWN") return "bg-amber-500/5 border-amber-500/20 hover:bg-amber-500/10 cursor-pointer"
if (s === "INFO") return "bg-blue-500/5 border-blue-500/20 hover:bg-blue-500/10"
return "bg-card border-border hover:bg-muted/30"
}
const getOutlineBadgeStyle = (status: string) => {
const s = status?.toUpperCase()
if (s === "OK") return "border-green-500 text-green-500 bg-transparent"
if (s === "INFO") return "border-blue-500 text-blue-500 bg-blue-500/5"
if (s === "WARNING") return "border-yellow-500 text-yellow-500 bg-yellow-500/5"
if (s === "CRITICAL") return "border-red-500 text-red-500 bg-red-500/5"
if (s === "UNKNOWN") return "border-amber-400 text-amber-400 bg-amber-500/5"
return ""
}
const formatCheckLabel = (key: string): string => {
const labels: Record<string, string> = {
// CPU
cpu_usage: "CPU Usage",
cpu_temperature: "Temperature",
// Memory
ram_usage: "RAM Usage",
swap_usage: "Swap Usage",
// Disk I/O
root_filesystem: "Root Filesystem",
smart_health: "SMART Health",
io_errors: "I/O Errors",
zfs_pools: "ZFS Pools",
lvm_volumes: "LVM Volumes",
lvm_check: "LVM Status",
// Network
connectivity: "Connectivity",
// VMs & CTs
qmp_communication: "QMP Communication",
container_startup: "Container Startup",
vm_startup: "VM Startup",
oom_killer: "OOM Killer",
// Services
cluster_mode: "Cluster Mode",
// Logs (prefixed with log_)
log_error_cascade: "Error Cascade",
log_error_spike: "Error Spike",
log_persistent_errors: "Persistent Errors",
log_critical_errors: "Critical Errors",
// Updates
pve_version: "Proxmox VE Version",
security_updates: "Security Updates",
system_age: "System Age",
pending_updates: "Pending Updates",
kernel_pve: "Kernel / PVE",
// Security
uptime: "Uptime",
certificates: "Certificates",
login_attempts: "Login Attempts",
fail2ban: "Fail2Ban",
// Storage (Proxmox)
proxmox_storages: "Proxmox Storages",
}
if (labels[key]) return labels[key]
// Convert snake_case or camelCase to Title Case
return key
.replace(/_/g, " ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/\b\w/g, (c) => c.toUpperCase())
}
const renderChecks = (
checks: Record<string, { status: string; detail: string; dismissable?: boolean; [key: string]: any }>,
categoryKey: string
) => {
if (!checks || Object.keys(checks).length === 0) return null
return (
<div className="mt-2 space-y-0.5">
{Object.entries(checks)
.filter(([, checkData]) => checkData.installed !== false)
.map(([checkKey, checkData]) => {
const isDismissable = checkData.dismissable === true
const checkStatus = checkData.status?.toUpperCase() || "OK"
return (
<div
key={checkKey}
className="flex items-center justify-between gap-1.5 sm:gap-2 text-[10px] sm:text-xs py-1.5 px-2 sm:px-3 rounded-md hover:bg-muted/40 transition-colors"
>
<div className="flex items-start gap-1.5 sm:gap-2 min-w-0 flex-1">
<span className="mt-0.5 shrink-0">{getStatusIcon(checkData.dismissed ? "INFO" : checkData.status, "sm")}</span>
<span className="font-medium shrink-0">{formatCheckLabel(checkKey)}</span>
<span className="text-muted-foreground break-words whitespace-pre-wrap min-w-0">{checkData.detail}</span>
{checkData.dismissed && (
<Badge variant="outline" className="text-[9px] px-1 py-0 h-4 shrink-0 text-blue-400 border-blue-400/30">
Dismissed
</Badge>
)}
</div>
<div className="flex items-center gap-1 sm:gap-1.5 shrink-0">
{(checkStatus === "WARNING" || checkStatus === "CRITICAL" || checkStatus === "UNKNOWN") && isDismissable && !checkData.dismissed && (
<Button
size="sm"
variant="outline"
className="h-5 px-1 sm:px-1.5 shrink-0 hover:bg-red-500/10 hover:border-red-500/50 bg-transparent text-[10px]"
disabled={dismissingKey === (checkData.error_key || checkKey)}
onClick={(e) => {
e.stopPropagation()
handleAcknowledge(checkData.error_key || checkKey, e)
}}
>
{dismissingKey === (checkData.error_key || checkKey) ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<>
<X className="h-3 w-3 sm:mr-0.5" />
<span className="hidden sm:inline">Dismiss</span>
</>
)}
</Button>
)}
</div>
</div>
)
})}
</div>
)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl w-[calc(100vw-2rem)] sm:w-[95vw] max-h-[85vh] overflow-y-auto overflow-x-hidden p-4 sm:p-6">
<DialogHeader>
<div className="flex items-center justify-between gap-3">
<DialogTitle className="flex items-center gap-2 flex-1 min-w-0">
<Activity className="h-5 w-5 sm:h-6 sm:w-6 shrink-0" />
<span className="truncate text-base sm:text-lg">System Health Status</span>
{healthData && <div className="shrink-0">{getStatusBadge(healthData.overall)}</div>}
</DialogTitle>
</div>
<DialogDescription className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs sm:text-sm">
<span>Detailed health checks for all system components</span>
{getTimeSinceCheck() && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{getTimeSinceCheck()}
</span>
)}
</DialogDescription>
</DialogHeader>
{loading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-800 dark:bg-red-950 dark:border-red-800 dark:text-red-200">
<p className="font-medium">Error loading health status</p>
<p className="text-sm">{error}</p>
</div>
)}
{healthData && !loading && (
<div className="space-y-4">
{/* Overall Stats Summary */}
<div className={`grid gap-2 sm:gap-3 p-3 sm:p-4 rounded-lg bg-muted/30 border ${stats.info > 0 ? "grid-cols-5" : "grid-cols-4"}`}>
<div className="text-center">
<div className="text-lg sm:text-2xl font-bold">{stats.total}</div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Total</div>
</div>
<div className="text-center">
<div className="text-lg sm:text-2xl font-bold text-green-500">{stats.healthy}</div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Healthy</div>
</div>
{stats.info > 0 && (
<div className="text-center">
<div className="text-lg sm:text-2xl font-bold text-blue-500">{stats.info}</div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Info</div>
</div>
)}
<div className="text-center">
<div className="text-lg sm:text-2xl font-bold text-yellow-500">{stats.warnings}</div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Warn</div>
</div>
<div className="text-center">
<div className="text-lg sm:text-2xl font-bold text-red-500">{stats.critical}</div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Critical</div>
</div>
{stats.unknown > 0 && (
<div className="text-center">
<div className="text-lg sm:text-2xl font-bold text-amber-400">{stats.unknown}</div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Unknown</div>
</div>
)}
</div>
{healthData.summary && healthData.summary !== "All systems operational" && (
<div className="text-xs sm:text-sm p-3 rounded-lg bg-muted/20 border overflow-hidden max-w-full">
<p className="font-medium text-foreground break-words whitespace-pre-wrap">{healthData.summary}</p>
</div>
)}
{/* Category List */}
<div className="space-y-2">
{CATEGORIES.map(({ key, label, Icon }) => {
const categoryData = healthData.details[key as keyof typeof healthData.details]
const originalStatus = categoryData?.status || "UNKNOWN"
const status = getEffectiveStatus(key, originalStatus)
const reason = categoryData?.reason
const checks = categoryData?.checks
const isExpanded = expandedCategories.has(key)
const hasChecks = checks && Object.keys(checks).length > 0
return (
<div
key={key}
className={`rounded-lg border transition-colors overflow-hidden ${getCategoryRowStyle(status)}`}
>
{/* Clickable header row */}
<div
className="flex items-center gap-2 sm:gap-3 p-2 sm:p-3 cursor-pointer select-none overflow-hidden"
onClick={() => toggleCategory(key)}
>
<div className="shrink-0 flex items-center gap-1.5 sm:gap-2">
<Icon className="h-4 w-4 text-blue-500 hidden sm:block" />
{getStatusIcon(status)}
</div>
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-1.5 sm:gap-2">
<p className="font-medium text-xs sm:text-sm truncate">{label}</p>
{hasChecks && (
<span className="text-[10px] text-muted-foreground shrink-0">
({Object.values(checks).filter(c => c.installed !== false).length})
</span>
)}
</div>
{reason && !isExpanded && (
<p className="text-[10px] sm:text-xs text-muted-foreground mt-0.5 line-clamp-2 break-words">{reason}</p>
)}
</div>
<div className="flex items-center gap-1 sm:gap-2 shrink-0">
<Badge variant="outline" className={`text-[10px] sm:text-xs px-1.5 sm:px-2.5 ${getOutlineBadgeStyle(status)}`}>
{status}
</Badge>
<ChevronRight
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground transition-transform duration-200 ${
isExpanded ? "rotate-90" : ""
}`}
/>
</div>
</div>
{/* Expandable checks section */}
{isExpanded && (
<div className="border-t border-border/50 bg-muted/5 px-1.5 sm:px-2 py-1.5 overflow-hidden">
{reason && (
<div className="flex items-center justify-between gap-2 px-3 py-1.5 mb-1">
<p className="text-xs text-muted-foreground break-words whitespace-pre-wrap flex-1">{reason}</p>
{/* Show dismiss button for UNKNOWN status at category level when dismissable */}
{status === "UNKNOWN" && categoryData?.dismissable && !hasChecks && (
<Button
size="sm"
variant="outline"
className="h-5 px-1.5 shrink-0 hover:bg-red-500/10 hover:border-red-500/50 bg-transparent text-[10px]"
disabled={dismissingKey === `category_${key}`}
onClick={(e) => {
e.stopPropagation()
handleAcknowledge(`category_${key}_unknown`, e)
}}
>
{dismissingKey === `category_${key}` ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<>
<X className="h-3 w-3 sm:mr-0.5" />
<span className="hidden sm:inline">Dismiss</span>
</>
)}
</Button>
)}
</div>
)}
{hasChecks ? (
renderChecks(checks, key)
) : (
<div className="flex items-center gap-2 text-xs text-muted-foreground px-3 py-2">
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
No issues detected
</div>
)}
</div>
)}
</div>
)
})}
</div>
{/* Dismissed Items Section -- hide items whose category has custom suppression */}
{(() => {
const customCats = new Set(customSuppressions.map(cs => cs.category))
const filteredDismissed = dismissedItems.filter(item => !customCats.has(item.category))
if (filteredDismissed.length === 0) return null
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground pt-2">
<BellOff className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
Dismissed Items ({filteredDismissed.length})
</div>
{filteredDismissed.map((item) => {
const catMeta = CATEGORIES.find(c => c.category === item.category || c.key === item.category)
const CatIcon = catMeta?.Icon || BellOff
const catLabel = catMeta?.label || item.category
const isPermanent = item.permanent || item.suppression_remaining_hours === -1
return (
<div
key={item.error_key}
className="flex items-start gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border bg-muted/10 border-muted opacity-75"
>
<div className="mt-0.5 shrink-0 flex items-center gap-1.5 sm:gap-2">
<CatIcon className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<div className="min-w-0 flex-1 overflow-hidden">
<p className="font-medium text-xs sm:text-sm text-muted-foreground truncate">{catLabel}</p>
<p className="text-[10px] sm:text-xs text-muted-foreground/70 break-words line-clamp-2">{item.reason}</p>
</div>
<div className="flex items-center gap-1.5 shrink-0">
{isPermanent ? (
<Badge variant="outline" className="text-[9px] sm:text-xs border-amber-500/50 text-amber-500/70 bg-transparent whitespace-nowrap">
Permanent
</Badge>
) : (
<Badge variant="outline" className="text-[9px] sm:text-xs border-blue-500/50 text-blue-500/70 bg-transparent whitespace-nowrap">
Dismissed
</Badge>
)}
<Badge variant="outline" className={`text-[9px] sm:text-xs whitespace-nowrap ${getOutlineBadgeStyle(item.severity)}`}>
was {item.severity}
</Badge>
</div>
</div>
<p className="text-[10px] sm:text-xs text-muted-foreground flex items-center gap-1">
<Clock className="h-3 w-3" />
{isPermanent
? "Permanently suppressed"
: `Suppressed for ${
item.suppression_remaining_hours < 24
? `${Math.round(item.suppression_remaining_hours)}h`
: item.suppression_remaining_hours < 720
? `${Math.round(item.suppression_remaining_hours / 24)} days`
: `${Math.round(item.suppression_remaining_hours / 720)} month(s)`
} more`
}
</p>
</div>
</div>
)
})}
</div>
)
})()}
{/* Custom Suppression Settings Summary */}
{customSuppressions.length > 0 && (
<div className="space-y-2 pt-2">
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
<Settings2 className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
Custom Suppression Settings
</div>
<div className="rounded-lg border border-blue-500/20 bg-blue-500/5 p-2.5 sm:p-3">
<div className="space-y-1.5">
{customSuppressions.map((cs) => {
const catMeta = CATEGORIES.find(c => c.category === cs.category || c.key === cs.category || c.label === cs.label)
const CatIcon = catMeta?.Icon || Settings2
const durationLabel = cs.hours === -1
? "Permanent"
: cs.hours >= 8760
? `${Math.floor(cs.hours / 8760)} year(s)`
: cs.hours >= 720
? `${Math.floor(cs.hours / 720)} month(s)`
: cs.hours >= 168
? `${Math.floor(cs.hours / 168)} week(s)`
: cs.hours >= 72
? `${Math.floor(cs.hours / 24)} days`
: `${cs.hours}h`
return (
<div key={cs.key} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<CatIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5 text-blue-400/70 shrink-0" />
<span className="text-[11px] sm:text-xs text-blue-400/80 truncate">{cs.label}</span>
</div>
<Badge variant="outline" className="text-[9px] sm:text-[10px] border-blue-500/30 text-blue-400/80 bg-transparent shrink-0">
{durationLabel}
</Badge>
</div>
)
})}
</div>
<p className="text-[10px] text-muted-foreground/60 mt-2 pt-1.5 border-t border-blue-500/10">
Alerts in these categories are auto-suppressed when detected.
</p>
</div>
</div>
)}
{healthData.timestamp && (
<div className="text-xs text-muted-foreground text-center pt-2">
Last updated: {new Date(healthData.timestamp).toLocaleString()}
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
)
}
+596
View File
@@ -0,0 +1,596 @@
"use client"
import { useEffect, useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Input } from "./ui/input"
import {
SlidersHorizontal,
Cpu,
MemoryStick,
HardDrive,
Server,
Thermometer,
Settings2,
Check,
Loader2,
RotateCcw,
AlertCircle,
FolderOpen,
Database,
Waves,
} from "lucide-react"
import { getApiUrl, getAuthToken } from "../lib/api-config"
// Local fetch wrapper that *preserves* the JSON body on non-2xx
// responses so we can surface backend validation messages
// (e.g. "critical must be >= warning") to the user. The shared
// `fetchApi` throws a generic "API request failed: 400" on any
// non-OK response, eating the body.
async function fetchJson<T>(endpoint: string, init?: RequestInit): Promise<T> {
const token = getAuthToken()
const headers: Record<string, string> = {
"Content-Type": "application/json",
...((init?.headers as Record<string, string>) || {}),
}
if (token) headers["Authorization"] = `Bearer ${token}`
const res = await fetch(getApiUrl(endpoint), {
...init,
headers,
cache: "no-store",
})
let data: any = null
try {
data = await res.json()
} catch {
// empty body — fall through with raw status
}
if (!res.ok) {
if (res.status === 401 && typeof window !== "undefined") {
try {
localStorage.removeItem("proxmenux-auth-token")
} catch {}
const path = window.location.pathname
if (!path.startsWith("/auth") && !path.startsWith("/login")) {
window.location.assign("/")
}
}
const msg =
(data && (data.message || data.error)) ||
`${res.status} ${res.statusText}`
throw new Error(msg)
}
return data as T
}
// ─── Types ───────────────────────────────────────────────────────────────────
//
// The backend returns a tree of leaves. Each leaf carries the metadata
// the UI needs to render an input + the recommended/customised flags.
// We mirror the shape rather than hand-coding it to keep the contract
// in one place — the backend is the source of truth.
interface ThresholdLeaf {
value: number
recommended: number
customised: boolean
unit: string
min: number
max: number
step: number
}
interface ThresholdsTree {
cpu: { warning: ThresholdLeaf; critical: ThresholdLeaf }
memory: { warning: ThresholdLeaf; critical: ThresholdLeaf; swap_critical: ThresholdLeaf }
host_storage: { warning: ThresholdLeaf; critical: ThresholdLeaf }
lxc_rootfs: { warning: ThresholdLeaf; critical: ThresholdLeaf }
cpu_temperature: { warning: ThresholdLeaf; critical: ThresholdLeaf }
disk_temperature: {
hdd: { warning: ThresholdLeaf; critical: ThresholdLeaf }
ssd: { warning: ThresholdLeaf; critical: ThresholdLeaf }
nvme: { warning: ThresholdLeaf; critical: ThresholdLeaf }
sas: { warning: ThresholdLeaf; critical: ThresholdLeaf }
}
// Phase 3 additions
lxc_mount: { warning: ThresholdLeaf; critical: ThresholdLeaf }
pve_storage: { warning: ThresholdLeaf; critical: ThresholdLeaf }
zfs_pool: { warning: ThresholdLeaf; critical: ThresholdLeaf }
}
// Pending edits: { "section/key" : "76" } — kept as raw strings while
// the user types so partial input ("8" mid-type) doesn't fail the
// numeric coercion. Coerced + validated on Save.
type PendingEdits = Record<string, string>
// ─── Section descriptors ─────────────────────────────────────────────────────
//
// Drives both the render order and the labels. Keeping it data-only
// means adding a new section later (Phase 4) is one entry, not a JSX
// surgery.
interface SectionField {
// Path in the thresholds tree, e.g. ["cpu", "warning"] or
// ["disk_temperature", "nvme", "critical"].
path: string[]
label: string
}
interface SectionDef {
id: string // Backend section key — used by the reset endpoint
title: string
icon: React.ComponentType<{ className?: string }>
description?: string
fields: SectionField[]
// For tabular sections (disk temperature) we group by sub-key. When
// present, fields are rendered in a 2-column grid (warning, critical)
// labelled by sub-key (HDD / SSD / NVMe / SAS).
rowGroups?: Array<{ subKey: string; label: string }>
}
// Order: compute → heat → storage capacity. Reading top-to-bottom
// flows naturally with no domain jumps:
// • Compute (CPU usage, RAM/Swap)
// • Heat (CPU temp, then disk temp — both °C)
// • Storage capacity (host → LXC rootfs → LXC mounts → PVE → ZFS,
// i.e. concrete to abstract)
const SECTIONS: SectionDef[] = [
// ── Compute ─────────────────────────────────────────────────────
{
id: "cpu",
title: "CPU usage",
icon: Cpu,
fields: [
{ path: ["cpu", "warning"], label: "Warning" },
{ path: ["cpu", "critical"], label: "Critical" },
],
},
{
id: "memory",
title: "Memory & Swap",
icon: MemoryStick,
fields: [
{ path: ["memory", "warning"], label: "Memory warning" },
{ path: ["memory", "critical"], label: "Memory critical" },
{ path: ["memory", "swap_critical"], label: "Swap critical" },
],
},
// ── Heat ────────────────────────────────────────────────────────
{
id: "cpu_temperature",
title: "CPU temperature",
icon: Thermometer,
fields: [
{ path: ["cpu_temperature", "warning"], label: "Warning" },
{ path: ["cpu_temperature", "critical"], label: "Critical" },
],
},
{
id: "disk_temperature",
title: "Disk temperature",
icon: Thermometer,
description:
"Per-class thresholds. Same units (°C) — different defaults because each class tolerates a different envelope.",
rowGroups: [
{ subKey: "hdd", label: "HDD" },
{ subKey: "ssd", label: "SSD" },
{ subKey: "nvme", label: "NVMe" },
{ subKey: "sas", label: "SAS" },
],
// For row-group sections, `fields` is unused — we generate per-row
// path lookups from the rowGroups + a hardcoded ["warning","critical"].
fields: [],
},
// ── Storage capacity ────────────────────────────────────────────
{
id: "host_storage",
title: "Disk space — host",
icon: HardDrive,
description: "Applies to / and every mountpoint under /var/lib/vz, /mnt/* etc.",
fields: [
{ path: ["host_storage", "warning"], label: "Warning" },
{ path: ["host_storage", "critical"], label: "Critical" },
],
},
{
id: "lxc_rootfs",
title: "Disk space — LXC rootfs",
icon: Server,
description: "Per-container root disk, evaluated against the rootfs size from PVE.",
fields: [
{ path: ["lxc_rootfs", "warning"], label: "Warning" },
{ path: ["lxc_rootfs", "critical"], label: "Critical" },
],
},
{
id: "lxc_mount",
title: "LXC mount points",
icon: FolderOpen,
description:
"Capacity of mountpoints inside running CTs (mp0, mp1, NFS, bind mounts). Excludes the rootfs — that's covered above.",
fields: [
{ path: ["lxc_mount", "warning"], label: "Warning" },
{ path: ["lxc_mount", "critical"], label: "Critical" },
],
},
{
id: "pve_storage",
title: "PVE storage capacity",
icon: Database,
description:
"Block-style PVE storages: LVM, LVM-thin, ZFS-pool, RBD/Ceph, PBS. Filesystem-style (dir/nfs/cifs) is already covered by host disk thresholds.",
fields: [
{ path: ["pve_storage", "warning"], label: "Warning" },
{ path: ["pve_storage", "critical"], label: "Critical" },
],
},
{
id: "zfs_pool",
title: "ZFS pool capacity",
icon: Waves,
description:
"ZFS pools at the host level — independent of PVE registration so rpool and dedicated backup pools are also monitored.",
fields: [
{ path: ["zfs_pool", "warning"], label: "Warning" },
{ path: ["zfs_pool", "critical"], label: "Critical" },
],
},
]
// ─── Helpers ─────────────────────────────────────────────────────────────────
function getLeaf(tree: ThresholdsTree | null, path: string[]): ThresholdLeaf | null {
if (!tree) return null
let node: any = tree
for (const p of path) {
if (node == null || typeof node !== "object") return null
node = node[p]
}
return node as ThresholdLeaf | null
}
function pathKey(path: string[]): string {
return path.join("/")
}
// ─── Component ───────────────────────────────────────────────────────────────
export function HealthThresholds() {
const [tree, setTree] = useState<ThresholdsTree | null>(null)
const [loading, setLoading] = useState(true)
const [editMode, setEditMode] = useState(false)
const [saving, setSaving] = useState(false)
const [savedFlash, setSavedFlash] = useState(false)
const [error, setError] = useState<string | null>(null)
const [pending, setPending] = useState<PendingEdits>({})
// Load on mount + auto-refresh after each save
const fetchTree = async () => {
try {
setLoading(true)
const res = await fetchJson<{ success: boolean; thresholds: ThresholdsTree }>(
"/api/health/thresholds",
)
if (res?.success && res.thresholds) setTree(res.thresholds)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load thresholds")
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchTree()
}, [])
const hasPendingChanges = Object.keys(pending).length > 0
// Build the partial payload from pending. Any blank or unparseable
// entry is skipped — the backend will reject anything malformed
// anyway, but we want to fail fast on the UI side too.
const buildPayload = (): Record<string, any> | null => {
const payload: Record<string, any> = {}
for (const [key, raw] of Object.entries(pending)) {
const parts = key.split("/")
const trimmed = raw.trim()
if (trimmed === "") continue
const num = Number(trimmed)
if (!isFinite(num)) {
setError(`Invalid value for ${key}: must be a number`)
return null
}
// Walk into payload mirroring the path
let cur: any = payload
for (let i = 0; i < parts.length - 1; i++) {
cur[parts[i]] = cur[parts[i]] || {}
cur = cur[parts[i]]
}
cur[parts[parts.length - 1]] = num
}
return payload
}
const handleEdit = () => {
setEditMode(true)
setError(null)
}
const handleCancel = () => {
setEditMode(false)
setPending({})
setError(null)
}
const handleSave = async () => {
const payload = buildPayload()
if (payload === null) return
if (Object.keys(payload).length === 0) {
setEditMode(false)
return
}
try {
setSaving(true)
setError(null)
const data = await fetchJson<{ success: boolean; thresholds: ThresholdsTree; message?: string }>(
"/api/health/thresholds",
{ method: "PUT", body: JSON.stringify(payload) },
)
if (!data.success || !data.thresholds) {
setError(data.message || "Save failed")
return
}
setTree(data.thresholds)
setPending({})
setEditMode(false)
setSavedFlash(true)
setTimeout(() => setSavedFlash(false), 2000)
} catch (err) {
setError(err instanceof Error ? err.message : "Network error while saving")
} finally {
setSaving(false)
}
}
const handleResetSection = async (sectionId: string) => {
if (!confirm(`Reset all "${SECTIONS.find((s) => s.id === sectionId)?.title}" thresholds to recommended values?`))
return
try {
const data = await fetchJson<{ success: boolean; thresholds: ThresholdsTree; message?: string }>(
`/api/health/thresholds/reset?section=${encodeURIComponent(sectionId)}`,
{ method: "POST" },
)
if (!data.success || !data.thresholds) {
setError(data.message || "Reset failed")
return
}
setTree(data.thresholds)
// Drop any pending edits within this section so the UI stays
// consistent — the values were just reset on the server.
setPending((p) => {
const next: PendingEdits = {}
for (const [k, v] of Object.entries(p)) {
if (!k.startsWith(sectionId + "/")) next[k] = v
}
return next
})
} catch (err) {
setError(err instanceof Error ? err.message : "Network error while resetting")
}
}
const handleResetAll = async () => {
if (!confirm("Reset ALL thresholds to recommended values? This affects every section.")) return
try {
const data = await fetchJson<{ success: boolean; thresholds: ThresholdsTree; message?: string }>(
"/api/health/thresholds/reset",
{ method: "POST" },
)
if (!data.success || !data.thresholds) {
setError(data.message || "Reset failed")
return
}
setTree(data.thresholds)
setPending({})
} catch (err) {
setError(err instanceof Error ? err.message : "Network error while resetting")
}
}
const renderField = (path: string[], label: string) => {
const leaf = getLeaf(tree, path)
if (!leaf) return null
const key = pathKey(path)
const editingValue = pending[key] ?? String(leaf.value)
// Visual rules (rebuilt — the original used /40 opacity borders +
// a blue ring stacked on top of the colour border, both of which
// were nearly invisible in read-only mode and stacked weirdly when
// a value was customised):
//
// • Read-only mode (editMode=false): keep severity colour on the
// border at a higher opacity (/70 instead of /40) and on the
// background (/10) so the field is clearly readable, and
// restore foreground colour (no `opacity-70` washout). This is
// the default state the user sees most of the time — it must
// match the visual weight of the rest of the Settings page.
// • Edit mode + value matches the recommended default: severity
// border + soft severity bg, same as read-only.
// • Edit mode + value customised: ONE border in blue, replacing
// (not stacking on top of) the severity border. This is the
// single signal that "this value differs from recommended".
//
// `swap_critical` and any other `*_critical` leaf falls into the
// red bucket via the substring check.
const last = path[path.length - 1] || ""
const isCritical = last.toLowerCase().includes("critical")
const isWarning = last.toLowerCase().includes("warning")
const severityClass = isCritical
? "border-red-500/70 bg-red-500/10 focus-visible:border-red-500"
: isWarning
? "border-amber-500/70 bg-amber-500/10 focus-visible:border-amber-500"
: "border-input"
const isCustomised = leaf.customised && !(key in pending)
const customisedClass = "border-blue-500 bg-blue-500/10 focus-visible:border-blue-500"
const fieldClass = isCustomised ? customisedClass : severityClass
const recommendedTooltip = `Recommended: ${leaf.recommended}${leaf.unit}`
return (
<div key={key} className="flex items-center justify-between gap-2 py-1.5 px-1">
<span className="text-xs sm:text-sm text-foreground/90 min-w-0">
{label}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
<Input
type="number"
min={leaf.min}
max={leaf.max}
step={leaf.step}
disabled={!editMode}
value={editingValue}
title={recommendedTooltip}
onChange={(e) =>
setPending((p) => ({ ...p, [key]: e.target.value }))
}
className={`w-20 h-7 text-xs text-right tabular-nums border ${fieldClass} ${
!editMode ? "disabled:opacity-100 disabled:cursor-default" : ""
}`}
/>
<span className="text-[11px] text-muted-foreground w-6">{leaf.unit}</span>
</div>
</div>
)
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2 min-w-0">
<SlidersHorizontal className="h-5 w-5 text-amber-500" />
<CardTitle>Health Monitor Thresholds</CardTitle>
</div>
{!loading && (
<div className="flex items-center gap-2">
{savedFlash && (
<span className="flex items-center gap-1 text-xs text-green-500">
<Check className="h-3.5 w-3.5" />
Saved
</span>
)}
{editMode ? (
<>
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
onClick={handleCancel}
disabled={saving}
>
Cancel
</button>
<button
className="h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1.5"
onClick={handleSave}
disabled={saving || !hasPendingChanges}
>
{saving ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
Save
</button>
</>
) : (
<>
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground flex items-center gap-1.5"
onClick={handleResetAll}
title="Reset every threshold to its recommended value"
>
<RotateCcw className="h-3 w-3" />
Reset all
</button>
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
onClick={handleEdit}
>
<Settings2 className="h-3 w-3" />
Edit
</button>
</>
)}
</div>
)}
</div>
<CardDescription>
The Health Monitor and notifications fire when these thresholds are crossed.
Amber inputs are warning levels, red inputs are critical levels. A blue ring
marks a value you've customised away from the recommended default hover the
field to see the recommendation, or use Reset to restore it.
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : !tree ? (
<div className="text-sm text-muted-foreground">Failed to load thresholds.</div>
) : (
<div>
{error && (
<div className="mb-4 flex items-start gap-2 p-2.5 rounded-md bg-red-500/10 border border-red-500/30 text-red-500 text-xs">
<AlertCircle className="h-4 w-4 flex-shrink-0 mt-0.5" />
<div className="flex-1">{error}</div>
</div>
)}
{/*
Masonry-style flow via CSS columns: cards keep their natural
height (CPU = 2 rows, Disk temperature = 8 rows) and the
browser packs them top-to-bottom into 1/2/3 columns based on
viewport. `break-inside-avoid` keeps each card whole.
Mobile (<md) stays single-column as today.
*/}
<div className="columns-1 md:columns-2 2xl:columns-3 gap-4 space-y-4 [&>*]:break-inside-avoid">
{SECTIONS.map((section) => {
const Icon = section.icon
return (
<div key={section.id} className="rounded-md border border-border/50 px-3 py-2">
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-2 min-w-0">
<Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<h4 className="text-sm font-medium">{section.title}</h4>
</div>
{!editMode && (
<button
className="h-6 w-6 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors flex items-center justify-center"
onClick={() => handleResetSection(section.id)}
title="Reset this section to recommended"
>
<RotateCcw className="h-3 w-3" />
</button>
)}
</div>
{section.description && (
<p className="text-[11px] text-muted-foreground mb-1.5 leading-snug">
{section.description}
</p>
)}
<div className="divide-y divide-border/40">
{section.rowGroups
? section.rowGroups.map((group) => (
<div key={group.subKey} className="py-1.5">
<div className="text-[11px] uppercase tracking-wider text-muted-foreground mb-0.5 px-1">
{group.label}
</div>
{renderField([section.id, group.subKey, "warning"], "Warning")}
{renderField([section.id, group.subKey, "critical"], "Critical")}
</div>
))
: section.fields.map((f) => renderField(f.path, f.label))}
</div>
</div>
)
})}
</div>
</div>
)}
</CardContent>
</Card>
)
}
File diff suppressed because it is too large Load Diff
+278
View File
@@ -0,0 +1,278 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Checkbox } from "./ui/checkbox"
import { Lock, User, AlertCircle, Server, Shield, Eye, EyeOff } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
import Image from "next/image"
interface LoginProps {
onLogin: () => void
}
export function Login({ onLogin }: LoginProps) {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [totpCode, setTotpCode] = useState("")
const [requiresTotp, setRequiresTotp] = useState(false)
const [rememberMe, setRememberMe] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
useEffect(() => {
// The Login screen is, by construction, the recovery path from any
// 401 cascade (the api-config wrapper redirects here when an
// expired/invalid JWT is detected). Clear the cascade-prevention
// flag on mount so a successful login can subsequently fire a fresh
// reload if a NEW 401 ever occurs. Without this clear, any 401 set
// earlier in the session sticks around forever and the next 401
// (e.g. mid-2FA, or right after a successful login if the token was
// briefly stale) is silently swallowed by the de-dup — the user
// sees a blank/stuck dashboard.
try {
sessionStorage.removeItem("proxmenux-auth-401-handled")
} catch {
// private browsing — best-effort
}
const savedUsername = localStorage.getItem("proxmenux-saved-username")
const savedPassword = localStorage.getItem("proxmenux-saved-password")
if (savedUsername && savedPassword) {
setUsername(savedUsername)
setPassword(savedPassword)
setRememberMe(true)
}
}, [])
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setError("")
if (!username || !password) {
setError("Please enter username and password")
return
}
if (requiresTotp && !totpCode) {
setError("Please enter your 2FA code")
return
}
setLoading(true)
try {
const response = await fetch(getApiUrl("/api/auth/login"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username,
password,
totp_token: totpCode || undefined, // Include 2FA code if provided
}),
})
const data = await response.json()
if (data.requires_totp) {
setRequiresTotp(true)
setLoading(false)
return
}
if (!response.ok) {
throw new Error(data.message || "Login failed")
}
localStorage.setItem("proxmenux-auth-token", data.token)
try {
sessionStorage.removeItem("proxmenux-auth-401-handled")
} catch {
// ignore
}
if (rememberMe) {
localStorage.setItem("proxmenux-saved-username", username)
localStorage.setItem("proxmenux-saved-password", password)
} else {
localStorage.removeItem("proxmenux-saved-username")
localStorage.removeItem("proxmenux-saved-password")
}
onLogin()
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed")
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="w-20 h-20 relative flex items-center justify-center bg-primary/10 rounded-lg">
<Image
src="/images/proxmenux-logo.png"
alt="ProxMenux Logo"
width={80}
height={80}
className="object-contain"
priority
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.parentElement?.querySelector(".fallback-icon")
if (fallback) {
fallback.classList.remove("hidden")
}
}}
/>
<Server className="h-12 w-12 text-primary absolute fallback-icon hidden" />
</div>
</div>
<div>
<h1 className="text-3xl font-bold">ProxMenux Monitor</h1>
<p className="text-muted-foreground mt-2">Sign in to access your dashboard</p>
</div>
</div>
<div className="bg-card border border-border rounded-lg p-6 shadow-lg">
<form onSubmit={handleLogin} className="space-y-4">
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{!requiresTotp ? (
<>
<div className="space-y-2">
<Label htmlFor="login-username" className="text-sm">
Username
</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="login-username"
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10 text-base"
disabled={loading}
autoComplete="username"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="login-password" className="text-sm">
Password
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="login-password"
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10 pr-10 text-base"
disabled={loading}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
disabled={loading}
tabIndex={-1}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="remember-me"
checked={rememberMe}
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
disabled={loading}
/>
<Label htmlFor="remember-me" className="text-sm font-normal cursor-pointer select-none">
Remember me
</Label>
</div>
</>
) : (
<div className="space-y-4">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Shield className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-blue-500">Two-Factor Authentication</p>
<p className="text-xs text-blue-500 mt-1">Enter the 6-digit code from your authentication app</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="totp-code" className="text-sm">
Authentication Code
</Label>
<Input
id="totp-code"
type="text"
placeholder="000000"
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
className="text-center text-lg tracking-widest font-mono text-base"
maxLength={6}
disabled={loading}
autoComplete="one-time-code"
autoFocus
/>
<p className="text-xs text-muted-foreground text-center">
You can also use a backup code (format: XXXX-XXXX)
</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setRequiresTotp(false)
setTotpCode("")
setError("")
}}
className="w-full"
>
Back to login
</Button>
</div>
)}
<Button type="submit" className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Signing in..." : requiresTotp ? "Verify Code" : "Sign In"}
</Button>
</form>
</div>
<p className="text-center text-sm text-muted-foreground">ProxMenux Monitor v1.2.1.3-beta</p>
</div>
</div>
)
}
+905
View File
@@ -0,0 +1,905 @@
"use client"
import type React from "react"
import { useState, useEffect, useRef, useCallback } from "react"
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import {
Activity,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
CornerDownLeft,
GripHorizontal,
ChevronDown,
Search,
Send,
Lightbulb,
Terminal,
Trash2,
X,
Copy,
Clipboard,
} from "lucide-react"
import { copyTerminalSelection, pasteFromClipboard } from "@/lib/terminal-clipboard"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu"
import { DialogHeader, DialogDescription } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Dialog as SearchDialog, DialogContent as SearchDialogContent, DialogTitle as SearchDialogTitle } from "@/components/ui/dialog"
import "xterm/css/xterm.css"
import { API_PORT, fetchApi } from "@/lib/api-config"
import { getTicketedWsUrl } from "@/lib/terminal-ws"
interface LxcTerminalModalProps {
open: boolean
onClose: () => void
vmid: number
vmName: string
}
interface CheatSheetResult {
command: string
description: string
examples: string[]
}
const proxmoxCommands = [
{ cmd: "ls -la", desc: "List all files with details" },
{ cmd: "cd /path/to/dir", desc: "Change directory" },
{ cmd: "cat filename", desc: "Display file contents" },
{ cmd: "grep 'pattern' file", desc: "Search for pattern in file" },
{ cmd: "find . -name 'file'", desc: "Find files by name" },
{ cmd: "df -h", desc: "Show disk usage" },
{ cmd: "du -sh *", desc: "Show directory sizes" },
{ cmd: "free -h", desc: "Show memory usage" },
{ cmd: "top", desc: "Show running processes" },
{ cmd: "ps aux | grep process", desc: "Find running process" },
{ cmd: "systemctl status service", desc: "Check service status" },
{ cmd: "systemctl restart service", desc: "Restart a service" },
{ cmd: "apt update && apt upgrade", desc: "Update packages" },
{ cmd: "apt install package", desc: "Install package" },
{ cmd: "tail -f /var/log/syslog", desc: "Follow log file" },
{ cmd: "chmod 755 file", desc: "Change file permissions" },
{ cmd: "chown user:group file", desc: "Change file owner" },
{ cmd: "tar -xzf file.tar.gz", desc: "Extract tar.gz archive" },
{ cmd: "docker ps", desc: "List running containers" },
{ cmd: "docker images", desc: "List Docker images" },
{ cmd: "ip addr show", desc: "Show IP addresses" },
{ cmd: "ping host", desc: "Test network connectivity" },
{ cmd: "curl -I url", desc: "Get HTTP headers" },
{ cmd: "history", desc: "Show command history" },
{ cmd: "clear", desc: "Clear terminal screen" },
]
function getWebSocketUrl(): string {
if (typeof window === "undefined") {
return "ws://localhost:8008/ws/terminal"
}
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
if (isStandardPort) {
return `${wsProtocol}//${hostname}/ws/terminal`
} else {
return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal`
}
}
export function LxcTerminalModal({
open: isOpen,
onClose,
vmid,
vmName,
}: LxcTerminalModalProps) {
const termRef = useRef<any>(null)
const wsRef = useRef<WebSocket | null>(null)
const fitAddonRef = useRef<any>(null)
const terminalContainerRef = useRef<HTMLDivElement>(null)
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "online" | "offline">("connecting")
const [isMobile, setIsMobile] = useState(false)
const [isTablet, setIsTablet] = useState(false)
const isInsideLxcRef = useRef(false)
const outputBufferRef = useRef<string>("")
const [modalHeight, setModalHeight] = useState(500)
const [isResizing, setIsResizing] = useState(false)
const resizeBarRef = useRef<HTMLDivElement>(null)
const modalHeightRef = useRef(500)
// Search state
const [searchModalOpen, setSearchModalOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [filteredCommands, setFilteredCommands] = useState<Array<{ cmd: string; desc: string }>>(proxmoxCommands)
const [isSearching, setIsSearching] = useState(false)
const [searchResults, setSearchResults] = useState<CheatSheetResult[]>([])
const [useOnline, setUseOnline] = useState(true)
// Detect mobile/tablet
useEffect(() => {
const checkDevice = () => {
const width = window.innerWidth
setIsMobile(width < 640)
setIsTablet(width >= 640 && width < 1024)
}
checkDevice()
window.addEventListener("resize", checkDevice)
return () => window.removeEventListener("resize", checkDevice)
}, [])
// Cleanup on close
useEffect(() => {
if (!isOpen) {
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
pingIntervalRef.current = null
}
if (wsRef.current) {
wsRef.current.close()
wsRef.current = null
}
if (termRef.current) {
termRef.current.dispose()
termRef.current = null
}
setConnectionStatus("connecting")
isInsideLxcRef.current = false
outputBufferRef.current = ""
}
}, [isOpen])
// Initialize terminal
useEffect(() => {
if (!isOpen) return
// `cancelled` short-circuits the async init if the modal closes
// before the dynamic xterm import resolves. Without this, we'd
// construct a Terminal instance, attach it to a now-stale ref, and
// open a WebSocket that nobody listens to. Audit Tier 6 — useEffect
// con `import("xterm")` sin cancelación.
let cancelled = false
// Small delay to ensure Dialog content is rendered
const initTimeout = setTimeout(() => {
if (cancelled || !terminalContainerRef.current) return
initTerminal()
}, 100)
const initTerminal = async () => {
const [TerminalClass, FitAddonClass] = await Promise.all([
import("xterm").then((mod) => mod.Terminal),
import("xterm-addon-fit").then((mod) => mod.FitAddon),
])
if (cancelled) return
const fontSize = window.innerWidth < 768 ? 12 : 16
const term = new TerminalClass({
rendererType: "dom",
fontFamily: '"MesloLGS NF", "FiraCode Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "Symbols Nerd Font", "Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace',
fontSize: fontSize,
lineHeight: 1,
cursorBlink: true,
scrollback: 2000,
disableStdin: false,
customGlyphs: true,
fontWeight: "500",
fontWeightBold: "700",
theme: {
background: "#000000",
foreground: "#ffffff",
cursor: "#ffffff",
cursorAccent: "#000000",
black: "#2e3436",
red: "#cc0000",
green: "#4e9a06",
yellow: "#c4a000",
blue: "#3465a4",
magenta: "#75507b",
cyan: "#06989a",
white: "#d3d7cf",
brightBlack: "#555753",
brightRed: "#ef2929",
brightGreen: "#8ae234",
brightYellow: "#fce94f",
brightBlue: "#729fcf",
brightMagenta: "#ad7fa8",
brightCyan: "#34e2e2",
brightWhite: "#eeeeec",
},
})
const fitAddon = new FitAddonClass()
term.loadAddon(fitAddon)
if (terminalContainerRef.current) {
term.open(terminalContainerRef.current)
fitAddon.fit()
}
termRef.current = term
fitAddonRef.current = fitAddon
// Connect WebSocket to host terminal. We append a single-use ticket
// (`?ticket=...`) which the backend consumes on handshake — see
// lib/terminal-ws.ts and AppImage/scripts/flask_terminal_routes.py.
const wsUrl = getWebSocketUrl()
const ws = new WebSocket(await getTicketedWsUrl(wsUrl))
wsRef.current = ws
// Reset state for new connection
isInsideLxcRef.current = false
outputBufferRef.current = ""
ws.onopen = () => {
setConnectionStatus("online")
// Start heartbeat ping
pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
} else {
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
}
}
}, 25000)
// Sync terminal size
fitAddon.fit()
ws.send(JSON.stringify({
type: "resize",
cols: term.cols,
rows: term.rows,
}))
// Auto-execute pct enter after connection is ready.
// The string is sent verbatim to the bash PTY, so a non-numeric
// `vmid` would land as shell input (e.g. `pct enter ; rm -rf /`).
// The prop is typed `number` but JSON / URL query injections can
// sneak strings in; validate as a defensive redundancy. Audit
// residual #lxc-terminal-vmid-injection.
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) return
// Coerce + verify: must be a positive integer that round-trips
// through Number without losing fidelity.
const id = Number(vmid)
if (!Number.isInteger(id) || id <= 0 || id >= 1_000_000) {
term.writeln('\r\n\x1b[31m[ERROR] Invalid VMID — refusing to execute pct enter\x1b[0m')
return
}
ws.send(`pct enter ${id}\r`)
}, 300)
}
ws.onerror = () => {
setConnectionStatus("offline")
term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m")
}
ws.onclose = () => {
setConnectionStatus("offline")
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
}
term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m")
}
term.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data)
}
})
ws.onmessage = (event) => {
// Filter out pong responses
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
return
}
// Helper to strip ANSI escape codes for pattern matching
const stripAnsi = (str: string) => str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
// Buffer output until we detect we're inside the LXC
// pct enter always enters directly without login prompt when run as root
if (!isInsideLxcRef.current) {
outputBufferRef.current += event.data
const buffer = outputBufferRef.current
const cleanBuffer = stripAnsi(buffer)
// Look for pct enter command followed by a new prompt
const pctEnterMatch = cleanBuffer.match(/pct enter (\d+)\r?\n/)
if (pctEnterMatch) {
const afterPctEnter = cleanBuffer.substring(cleanBuffer.indexOf(pctEnterMatch[0]) + pctEnterMatch[0].length)
// Extract the host name from the prompt BEFORE pct enter (e.g., "root@amd").
// Charset widened to accept dotted FQDNs (`proxmox.lan`) and unicode
// letters/numbers (host names like `próxmox` or non-Latin scripts).
// The previous `[a-zA-Z0-9_-]` truncated the hostname and the
// "are we inside the LXC?" comparison then misfired.
const hostPromptMatch = cleanBuffer.match(/@([\p{L}\p{N}._-]+).*pct enter/u)
const hostName = hostPromptMatch ? hostPromptMatch[1] : null
// Look for a new prompt after pct enter that ends with # or $
// This works for both bash (user@host:~#) and ash/Alpine ([user@host /]#)
const promptMatch = afterPctEnter.match(/[@\[]([\p{L}\p{N}._-]+)[^\r\n]*[#$]\s*$/u)
if (promptMatch) {
const lxcHostname = promptMatch[1]
// If we found a prompt with a DIFFERENT hostname than the Proxmox host,
// we're inside the LXC container
if (!hostName || lxcHostname !== hostName) {
isInsideLxcRef.current = true
// Find the original prompt with ANSI codes to display it properly
const afterPctEnterWithAnsi = buffer.substring(buffer.indexOf('pct enter') + pctEnterMatch[0].length)
// Write the LXC prompt (last line with # or $)
const lastPromptMatch = afterPctEnterWithAnsi.match(/[^\r\n]*[#$]\s*$/)
if (lastPromptMatch) {
term.write(lastPromptMatch[0])
}
// Detect if this is Alpine/ash shell by checking prompt format
// Alpine uses: [root@hostname ~]# or [root@hostname /]#
// Other distros use: root@hostname:/# or root@hostname:~#
const isAlpine = afterPctEnter.match(/\[[^\]]+@[^\]]+\s+[^\]]*\][#$]/)
if (isAlpine) {
// Send an extra Enter ONLY for Alpine containers (ash shell)
// This forces the prompt to refresh properly
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('\r')
}
}, 100)
}
return
}
}
}
} else {
// Already inside LXC, write directly
term.write(event.data)
}
}
}
return () => {
cancelled = true
clearTimeout(initTimeout)
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
}
if (wsRef.current) {
wsRef.current.close()
}
if (termRef.current) {
termRef.current.dispose()
}
}
}, [isOpen, vmid])
// Resize handling
useEffect(() => {
if (termRef.current && fitAddonRef.current && isOpen) {
setTimeout(() => {
fitAddonRef.current?.fit()
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({
type: "resize",
cols: termRef.current.cols,
rows: termRef.current.rows,
}))
}
}, 100)
}
}, [modalHeight, isOpen])
// Resize bar handlers
const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault()
setIsResizing(true)
modalHeightRef.current = modalHeight
}, [modalHeight])
useEffect(() => {
if (!isResizing) return
const handleMove = (e: MouseEvent | TouchEvent) => {
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
const windowHeight = window.innerHeight
const newHeight = windowHeight - clientY - 20
const clampedHeight = Math.max(300, Math.min(windowHeight - 100, newHeight))
modalHeightRef.current = clampedHeight
setModalHeight(clampedHeight)
}
const handleEnd = () => {
setIsResizing(false)
}
document.addEventListener("mousemove", handleMove)
document.addEventListener("mouseup", handleEnd)
document.addEventListener("touchmove", handleMove)
document.addEventListener("touchend", handleEnd)
return () => {
document.removeEventListener("mousemove", handleMove)
document.removeEventListener("mouseup", handleEnd)
document.removeEventListener("touchmove", handleMove)
document.removeEventListener("touchend", handleEnd)
}
}, [isResizing])
// Send key helpers for mobile/tablet
const sendKey = useCallback((key: string) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(key)
}
}, [])
const sendEsc = useCallback(() => sendKey("\x1b"), [sendKey])
const sendTab = useCallback(() => sendKey("\t"), [sendKey])
const sendArrowUp = useCallback(() => sendKey("\x1b[A"), [sendKey])
const sendArrowDown = useCallback(() => sendKey("\x1b[B"), [sendKey])
const sendArrowLeft = useCallback(() => sendKey("\x1b[D"), [sendKey])
const sendArrowRight = useCallback(() => sendKey("\x1b[C"), [sendKey])
const sendEnter = useCallback(() => sendKey("\r"), [sendKey])
const sendCtrlC = useCallback(() => sendKey("\x03"), [sendKey]) // Ctrl+C
// Mobile clipboard helpers — see lib/terminal-clipboard.ts for the rationale.
const handleCopy = useCallback(async () => {
await copyTerminalSelection(termRef.current)
}, [])
const handlePaste = useCallback(async () => {
await pasteFromClipboard(sendKey)
}, [sendKey])
// Search effect - debounced search with cheat.sh
useEffect(() => {
const searchCheatSh = async (query: string) => {
if (!query.trim()) {
setSearchResults([])
setFilteredCommands(proxmoxCommands)
return
}
try {
setIsSearching(true)
const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}`
const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, {
method: "GET",
signal: AbortSignal.timeout(10000),
})
if (!data.success || !data.examples || data.examples.length === 0) {
throw new Error("No examples found")
}
const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({
command: example.command,
description: example.description || "",
examples: [example.command],
}))
setUseOnline(true)
setSearchResults(formattedResults)
} catch (error) {
const filtered = proxmoxCommands.filter(
(item) =>
item.cmd.toLowerCase().includes(query.toLowerCase()) ||
item.desc.toLowerCase().includes(query.toLowerCase()),
)
setFilteredCommands(filtered)
setSearchResults([])
setUseOnline(false)
} finally {
setIsSearching(false)
}
}
const debounce = setTimeout(() => {
if (searchQuery && searchQuery.length >= 2) {
searchCheatSh(searchQuery)
} else {
setSearchResults([])
setFilteredCommands(proxmoxCommands)
}
}, 800)
return () => clearTimeout(debounce)
}, [searchQuery])
const handleClear = useCallback(() => {
if (termRef.current) {
termRef.current.clear()
}
}, [])
const sendToTerminal = useCallback((command: string) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(command)
setTimeout(() => {
setSearchModalOpen(false)
}, 100)
}
}, [])
const showMobileControls = isMobile || isTablet
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
className="max-w-4xl w-[95vw] p-0 gap-0 bg-black border-border overflow-hidden flex flex-col"
style={{ height: `${modalHeight}px` }}
hideClose
>
{/* Resize bar */}
<div
ref={resizeBarRef}
className="h-3 w-full cursor-ns-resize flex items-center justify-center bg-zinc-900 hover:bg-zinc-800 transition-colors touch-none"
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
>
<GripHorizontal className="h-4 w-4 text-zinc-500" />
</div>
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800">
<DialogTitle className="text-sm font-medium text-white">
Terminal: {vmName} (ID: {vmid})
</DialogTitle>
<div className="flex gap-2">
<Button
onClick={() => setSearchModalOpen(true)}
variant="outline"
size="sm"
disabled={connectionStatus !== "online"}
className="h-8 gap-2 bg-blue-600/20 hover:bg-blue-600/30 border-blue-600/50 text-blue-400 disabled:opacity-50"
>
<Search className="h-4 w-4" />
<span className="hidden sm:inline">Search</span>
</Button>
<Button
onClick={handleClear}
variant="outline"
size="sm"
disabled={connectionStatus !== "online"}
className="h-8 gap-2 bg-yellow-600/20 hover:bg-yellow-600/30 border-yellow-600/50 text-yellow-400 disabled:opacity-50"
>
<Trash2 className="h-4 w-4" />
<span className="hidden sm:inline">Clear</span>
</Button>
</div>
</div>
{/* Terminal container */}
<div className="flex-1 overflow-hidden bg-black p-1">
<div
ref={terminalContainerRef}
className="w-full h-full"
style={{ minHeight: "200px" }}
/>
</div>
{/* Mobile/Tablet control buttons */}
{showMobileControls && (
<div className="px-2 py-2 bg-zinc-900 border-t border-zinc-800">
<div className="flex items-center justify-center gap-1.5">
<Button
variant="outline"
size="sm"
onClick={sendEsc}
className="h-8 px-2 text-xs bg-zinc-800 border-zinc-700 text-zinc-300"
>
ESC
</Button>
<Button
variant="outline"
size="sm"
onClick={sendTab}
className="h-8 px-2 text-xs bg-zinc-800 border-zinc-700 text-zinc-300"
>
TAB
</Button>
<Button
variant="outline"
size="sm"
onClick={sendArrowUp}
className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700"
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={sendArrowDown}
className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700"
>
<ArrowDown className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={sendArrowLeft}
className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={sendArrowRight}
className="h-8 w-8 p-0 bg-zinc-800 border-zinc-700"
>
<ArrowRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={sendEnter}
className="h-8 px-2 text-xs bg-blue-600/20 border-blue-600/50 text-blue-400 hover:bg-blue-600/30"
>
<CornerDownLeft className="h-4 w-4 mr-1" />
Enter
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 border-zinc-700 text-zinc-300 gap-1"
>
Ctrl
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="text-xs text-muted-foreground">Control Sequences</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => sendKey("\x03")}>
<span className="font-mono text-xs mr-2">Ctrl+C</span>
<span className="text-muted-foreground text-xs">Cancel/Interrupt</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => sendKey("\x18")}>
<span className="font-mono text-xs mr-2">Ctrl+X</span>
<span className="text-muted-foreground text-xs">Exit (nano)</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => sendKey("\x12")}>
<span className="font-mono text-xs mr-2">Ctrl+R</span>
<span className="text-muted-foreground text-xs">Search history</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">Clipboard</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => { void handleCopy() }}>
<Copy className="h-3.5 w-3.5 mr-2" />
<span className="text-xs">Copy selection</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => { void handlePaste() }}>
<Clipboard className="h-3.5 w-3.5 mr-2" />
<span className="text-xs">Paste</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)}
{/* Status bar at bottom */}
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-t border-zinc-800">
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-blue-500" />
<div
className={`w-2 h-2 rounded-full ${
connectionStatus === "online"
? "bg-green-500"
: connectionStatus === "connecting"
? "bg-yellow-500 animate-pulse"
: "bg-red-500"
}`}
/>
<span className="text-xs text-zinc-400 capitalize">{connectionStatus}</span>
</div>
<Button
onClick={onClose}
variant="outline"
size="sm"
className="h-8 gap-2 bg-red-600/20 hover:bg-red-600/30 border-red-600/50 text-red-400"
>
<X className="h-4 w-4" />
<span className="hidden sm:inline">Close</span>
</Button>
</div>
</DialogContent>
{/* Search Commands Modal */}
<SearchDialog open={searchModalOpen} onOpenChange={setSearchModalOpen}>
<SearchDialogContent className="max-w-3xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4 border-b border-zinc-800">
<SearchDialogTitle className="text-xl font-semibold">Search Commands</SearchDialogTitle>
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${useOnline ? "bg-green-500" : "bg-red-500"}`}
title={useOnline ? "Online - Using cheat.sh API" : "Offline - Using local commands"}
/>
</div>
</DialogHeader>
<DialogDescription className="sr-only">Search for Linux commands</DialogDescription>
<div className="space-y-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
<Input
placeholder="Search commands... (e.g., tar, docker, systemctl)"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
spellCheck={false}
/>
</div>
{isSearching && (
<div className="text-center py-4 text-zinc-400">
<div className="animate-spin inline-block w-6 h-6 border-2 border-current border-t-transparent rounded-full mb-2" />
<p className="text-sm">Searching cheat.sh...</p>
</div>
)}
<div className="flex-1 overflow-y-auto space-y-2 pr-2 max-h-[50vh]">
{searchResults.length > 0 ? (
<>
{searchResults.map((result, index) => (
<div
key={index}
className="p-4 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:border-zinc-600 transition-colors"
>
{result.description && (
<p className="text-xs text-zinc-400 mb-2 leading-relaxed"># {result.description}</p>
)}
<div
onClick={() => sendToTerminal(result.command)}
className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2"
>
<code className="text-sm text-blue-400 font-mono break-all flex-1">{result.command}</code>
<Send className="h-4 w-4 text-zinc-600 group-hover:text-blue-400 flex-shrink-0 mt-0.5 transition-colors" />
</div>
</div>
))}
<div className="text-center py-2">
<p className="text-xs text-zinc-500">
<Lightbulb className="inline-block w-3 h-3 mr-1" />
Powered by cheat.sh
</p>
</div>
</>
) : filteredCommands.length > 0 && !useOnline ? (
filteredCommands.map((item, index) => (
<div
key={index}
onClick={() => sendToTerminal(item.cmd)}
className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors"
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<code className="text-sm text-blue-400 font-mono break-all">{item.cmd}</code>
<p className="text-xs text-zinc-400 mt-1">{item.desc}</p>
</div>
<Button
onClick={(e) => {
e.stopPropagation()
sendToTerminal(item.cmd)
}}
size="sm"
variant="ghost"
className="shrink-0 h-7 px-2 text-xs"
>
<Send className="h-3 w-3 mr-1" />
Send
</Button>
</div>
</div>
))
) : !isSearching && !searchQuery && !useOnline ? (
proxmoxCommands.map((item, index) => (
<div
key={index}
onClick={() => sendToTerminal(item.cmd)}
className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors"
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<code className="text-sm text-blue-400 font-mono break-all">{item.cmd}</code>
<p className="text-xs text-zinc-400 mt-1">{item.desc}</p>
</div>
<Button
onClick={(e) => {
e.stopPropagation()
sendToTerminal(item.cmd)
}}
size="sm"
variant="ghost"
className="shrink-0 h-7 px-2 text-xs"
>
<Send className="h-3 w-3 mr-1" />
Send
</Button>
</div>
</div>
))
) : !isSearching ? (
<div className="text-center py-12 space-y-4">
{searchQuery ? (
<>
<Search className="w-12 h-12 text-zinc-600 mx-auto" />
<div>
<p className="text-zinc-400 font-medium">{"No results found for \""}{searchQuery}{"\""}</p>
<p className="text-xs text-zinc-500 mt-1">Try a different command or check your spelling</p>
</div>
</>
) : (
<>
<Terminal className="w-12 h-12 text-zinc-600 mx-auto" />
<div>
<p className="text-zinc-400 font-medium mb-2">Search for any command</p>
<div className="text-sm text-zinc-500 space-y-1">
<p>Try searching for:</p>
<div className="flex flex-wrap justify-center gap-2 mt-2">
{["tar", "grep", "docker", "systemctl", "curl"].map((cmd) => (
<code
key={cmd}
onClick={() => setSearchQuery(cmd)}
className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700"
>
{cmd}
</code>
))}
</div>
</div>
</div>
{useOnline && (
<div className="flex items-center justify-center gap-2 text-xs text-zinc-600 mt-4">
<Lightbulb className="w-3 h-3" />
<span>Powered by cheat.sh</span>
</div>
)}
</>
)}
</div>
) : null}
</div>
<div className="pt-2 border-t border-zinc-800 flex items-center justify-between text-xs text-zinc-500">
<div className="flex items-center gap-2">
<Lightbulb className="w-3 h-3" />
<span>Tip: Search for any Linux command</span>
</div>
{useOnline && searchResults.length > 0 && <span className="text-zinc-600">Powered by cheat.sh</span>}
</div>
</div>
</SearchDialogContent>
</SearchDialog>
</Dialog>
)
}
@@ -0,0 +1,227 @@
"use client"
import { useEffect, useState } from "react"
import { Boxes, Info, Loader2, Settings2, CheckCircle2 } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { fetchApi } from "../lib/api-config"
interface DetectionResponse {
success: boolean
enabled?: boolean
message?: string
purged?: number
}
export function LxcUpdateDetection() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [enabled, setEnabled] = useState<boolean>(true)
const [pending, setPending] = useState<boolean>(true)
const [editMode, setEditMode] = useState(false)
const [error, setError] = useState<string | null>(null)
const [saved, setSaved] = useState(false)
const [lastPurged, setLastPurged] = useState<number | null>(null)
useEffect(() => {
let cancelled = false
fetchApi<DetectionResponse>("/api/lxc-updates/detection")
.then(data => {
if (cancelled) return
if (data.success && typeof data.enabled === "boolean") {
setEnabled(data.enabled)
setPending(data.enabled)
} else {
setError(data.message || "Failed to load setting")
}
})
.catch(e => {
if (!cancelled) setError(String(e))
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [])
const hasChanges = pending !== enabled
function handleEdit() {
setEditMode(true)
setError(null)
setSaved(false)
setLastPurged(null)
}
function handleCancel() {
setPending(enabled)
setEditMode(false)
setError(null)
setLastPurged(null)
}
async function handleSave() {
if (!hasChanges) {
setEditMode(false)
return
}
setSaving(true)
setError(null)
setSaved(false)
setLastPurged(null)
try {
const data = await fetchApi<DetectionResponse>("/api/lxc-updates/detection", {
method: "POST",
body: JSON.stringify({ enabled: pending }),
})
if (!data.success) {
setError(data.message || "Failed to save setting")
return
}
setEnabled(pending)
setEditMode(false)
setSaved(true)
setTimeout(() => setSaved(false), 3000)
if (!pending && typeof data.purged === "number" && data.purged > 0) {
setLastPurged(data.purged)
}
// Notify the Notifications section so it hides/shows the
// lxc_updates_available toggle in real time.
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("proxmenux:lxc-detection-changed", { detail: { enabled: pending } }),
)
}
} catch (e) {
setError(String(e))
} finally {
setSaving(false)
}
}
return (
<Card>
<CardHeader>
<div className="flex items-start justify-between gap-3">
{/* Title row flex-wrap so on narrow screens the badge can drop
under the title without dragging the icon along with it. The
icon stays on the same baseline as the title text on every
breakpoint thanks to `items-center` + leading-tight title. */}
<div className="flex items-center gap-2 flex-wrap min-w-0">
<Boxes className="h-5 w-5 text-purple-500 shrink-0" />
<CardTitle className="leading-tight">LXC Update Detection</CardTitle>
{enabled ? (
<Badge variant="outline" className="text-[10px] border-green-500/30 text-green-500">
Active
</Badge>
) : (
<Badge variant="outline" className="text-[10px] border-muted-foreground/30 text-muted-foreground">
Disabled
</Badge>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{saved && (
<span className="flex items-center gap-1 text-xs text-green-500">
<CheckCircle2 className="h-3.5 w-3.5" />
Saved
</span>
)}
{error && !editMode && (
<span
className="flex items-center gap-1 text-xs text-red-500 max-w-[40ch] truncate"
title={error}
>
Save failed: {error}
</span>
)}
{editMode ? (
<>
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
onClick={handleCancel}
disabled={saving}
>
Cancel
</button>
<button
className="h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1.5"
onClick={handleSave}
disabled={saving || !hasChanges}
>
{saving ? <Loader2 className="h-3 w-3 animate-spin" /> : <CheckCircle2 className="h-3 w-3" />}
Save
</button>
</>
) : (
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
onClick={handleEdit}
disabled={loading}
>
<Settings2 className="h-3 w-3" />
Edit
</button>
)}
</div>
</div>
<CardDescription>
Periodically check running Debian/Ubuntu/Alpine LXC containers for pending package updates
(<code>apt list --upgradable</code> / <code>apk list -u</code>) and surface them on the dashboard. The
corresponding notification toggle in <strong>Notifications Services</strong> appears only while detection
is enabled.
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
{/* Enable/Disable single-line label + toggle. The description
paragraph was removed because the CardDescription above already
covers the behaviour; on mobile that second paragraph forced
the icon to top-align and made the toggle wrap awkwardly. */}
<div className="flex items-center justify-between gap-3 py-2 px-1">
<div className="flex items-center gap-2 min-w-0">
<Boxes
className={`h-4 w-4 shrink-0 ${pending ? "text-purple-500" : "text-muted-foreground"}`}
/>
<span className="text-sm font-medium truncate">Enable LXC update detection</span>
</div>
<button
className={`relative w-10 h-5 rounded-full transition-colors shrink-0 ${
pending ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
} ${!editMode ? "opacity-60 cursor-not-allowed" : "cursor-pointer"}`}
onClick={() => editMode && setPending(p => !p)}
disabled={!editMode || saving}
role="switch"
aria-checked={pending}
aria-label="Enable LXC update detection"
>
<span
className={`absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow transition-transform ${
pending ? "translate-x-5" : "translate-x-0"
}`}
/>
</button>
</div>
{lastPurged !== null && lastPurged > 0 && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-muted/50 border border-border">
<Info className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" />
<p className="text-[11px] text-muted-foreground leading-relaxed">
{lastPurged} LXC entries removed from the registry. Re-enabling detection will repopulate them on the
next scan cycle.
</p>
</div>
)}
{error && editMode && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30">
<Info className="h-3.5 w-3.5 text-amber-400 shrink-0 mt-0.5" />
<p className="text-[11px] text-amber-500 leading-relaxed break-all">{error}</p>
</div>
)}
</CardContent>
</Card>
)
}
+489
View File
@@ -0,0 +1,489 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ArrowLeft, Loader2 } from "lucide-react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { fetchApi } from "@/lib/api-config"
interface MetricsViewProps {
vmid: number
vmName: string
vmType: "qemu" | "lxc"
onBack: () => void
}
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
{ value: "day", label: "24 Hours" },
{ value: "week", label: "7 Days" },
{ value: "month", label: "30 Days" },
{ value: "year", label: "1 Year" },
]
const CustomCPUTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value}</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomMemoryTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} GB</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomDiskTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} MB</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomNetworkTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} MB</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function MetricsView({ vmid, vmName, vmType, onBack }: MetricsViewProps) {
const [timeframe, setTimeframe] = useState("week")
const [data, setData] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hiddenDiskLines, setHiddenDiskLines] = useState<string[]>([])
const [hiddenNetworkLines, setHiddenNetworkLines] = useState<string[]>([])
useEffect(() => {
fetchMetrics()
}, [vmid, timeframe])
const fetchMetrics = async () => {
setLoading(true)
setError(null)
try {
const result = await fetchApi<any>(`/api/vms/${vmid}/metrics?timeframe=${timeframe}`)
const transformedData = result.data.map((item: any) => {
const date = new Date(item.time * 1000)
let timeLabel = ""
if (timeframe === "hour") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "day") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "week") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "month") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
})
} else {
timeLabel = date.toLocaleString("en-US", {
month: "short",
year: "numeric",
})
}
return {
time: timeLabel,
timestamp: item.time,
cpu: item.cpu ? Number((item.cpu * 100).toFixed(2)) : 0,
memory: item.mem ? Number(((item.mem / item.maxmem) * 100).toFixed(2)) : 0,
memoryGB: item.mem ? Number((item.mem / 1024 / 1024 / 1024).toFixed(2)) : 0,
maxMemoryGB: item.maxmem ? Number((item.maxmem / 1024 / 1024 / 1024).toFixed(2)) : 0,
netin: item.netin ? Number((item.netin / 1024 / 1024).toFixed(2)) : 0,
netout: item.netout ? Number((item.netout / 1024 / 1024).toFixed(2)) : 0,
diskread: item.diskread ? Number((item.diskread / 1024 / 1024).toFixed(2)) : 0,
diskwrite: item.diskwrite ? Number((item.diskwrite / 1024 / 1024).toFixed(2)) : 0,
}
})
setData(transformedData)
} catch (err: any) {
setError(err.message || "Error loading metrics")
} finally {
setLoading(false)
}
}
const formatXAxisTick = (tick: any) => {
return tick
}
const renderAllCharts = () => {
if (loading) {
return (
<div className="flex items-center justify-center h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center h-[400px]">
<p className="text-red-500">{error}</p>
</div>
)
}
if (data.length === 0) {
return (
<div className="flex items-center justify-center h-[400px]">
<p className="text-muted-foreground">No data available</p>
</div>
)
}
const tickInterval = Math.ceil(data.length / 8)
return (
<div className="space-y-8">
{/* CPU Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">CPU Usage</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "%", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomCPUTooltip />} />
<Area
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.3}
name="CPU %"
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Memory Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">Memory Usage</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomMemoryTooltip />} />
<Area
type="monotone"
dataKey="memoryGB"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.3}
strokeWidth={2}
name="Memory GB"
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Disk I/O Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">Disk I/O</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "MB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomDiskTooltip />} />
<Legend content={renderDiskLegend} verticalAlign="top" />
<Area
type="monotone"
dataKey="diskread"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.3}
strokeWidth={2}
name="Read"
hide={hiddenDiskLines.includes("diskread")}
/>
<Area
type="monotone"
dataKey="diskwrite"
stroke="#3b82f6"
fill="#3b82f6"
fillOpacity={0.3}
strokeWidth={2}
name="Write"
hide={hiddenDiskLines.includes("diskwrite")}
/>
</AreaChart>
</ResponsiveContainer>
</div>
{/* Network I/O Chart */}
<div>
<h3 className="text-lg font-semibold mb-4">Network I/O</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
tickFormatter={formatXAxisTick}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor" }}
label={{ value: "MB", angle: -90, position: "insideLeft", fill: "currentColor" }}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomNetworkTooltip />} />
<Legend content={renderNetworkLegend} verticalAlign="top" />
<Area
type="monotone"
dataKey="netin"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.3}
strokeWidth={2}
name="Download"
hide={hiddenNetworkLines.includes("netin")}
/>
<Area
type="monotone"
dataKey="netout"
stroke="#3b82f6"
fill="#3b82f6"
fillOpacity={0.3}
strokeWidth={2}
name="Upload"
hide={hiddenNetworkLines.includes("netout")}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)
}
const handleDiskLegendClick = (dataKey: string) => {
setHiddenDiskLines((prev) => {
if (prev.includes(dataKey)) {
return prev.filter((key) => key !== dataKey)
} else {
return [...prev, dataKey]
}
})
}
const handleNetworkLegendClick = (dataKey: string) => {
setHiddenNetworkLines((prev) => {
if (prev.includes(dataKey)) {
return prev.filter((key) => key !== dataKey)
} else {
return [...prev, dataKey]
}
})
}
const renderDiskLegend = (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-6 pb-2">
{payload.map((entry: any) => (
<button
key={entry.dataKey}
onClick={() => handleDiskLegendClick(entry.dataKey)}
className={`flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-100 ${
hiddenDiskLines.includes(entry.dataKey) ? "opacity-40" : "opacity-100"
}`}
>
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-sm">{entry.value}</span>
</button>
))}
</div>
)
}
const renderNetworkLegend = (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-6 pb-2">
{payload.map((entry: any) => (
<button
key={entry.dataKey}
onClick={() => handleNetworkLegendClick(entry.dataKey)}
className={`flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-100 ${
hiddenNetworkLines.includes(entry.dataKey) ? "opacity-40" : "opacity-100"
}`}
>
<span className="w-3 h-3 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-sm">{entry.value}</span>
</button>
))}
</div>
)
}
return (
<div className="flex flex-col h-full max-h-[90vh]">
{/* Fixed Header */}
<div className="p-6 pb-4 border-b shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h2 className="text-xl font-semibold">Metrics - {vmName}</h2>
<p className="text-sm text-muted-foreground mt-1">
VMID: {vmid} Type: {vmType.toUpperCase()}
</p>
</div>
</div>
<Select value={timeframe} onValueChange={setTimeframe}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Scrollable Content with all charts */}
<div className="flex-1 overflow-y-auto p-6 min-h-0">{renderAllCharts()}</div>
</div>
)
}
+234
View File
@@ -0,0 +1,234 @@
"use client"
import { Card, CardContent } from "./ui/card"
import { Badge } from "./ui/badge"
import { Wifi, Zap } from 'lucide-react'
import { useState, useEffect } from "react"
import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
interface NetworkCardProps {
interface_: {
name: string
type: string
status: string
speed: number
duplex?: string
mtu?: number
mac_address: string | null
addresses: Array<{
ip: string
netmask: string
}>
bytes_sent?: number
bytes_recv?: number
bridge_physical_interface?: string
bridge_bond_slaves?: string[]
vmid?: number
vm_name?: string
vm_type?: string
}
timeframe: "hour" | "day" | "week" | "month" | "year"
onClick?: () => void
}
const getInterfaceTypeBadge = (type: string) => {
switch (type) {
case "physical":
return { color: "bg-blue-500/10 text-blue-500 border-blue-500/20", label: "Physical" }
case "bridge":
return { color: "bg-green-500/10 text-green-500 border-green-500/20", label: "Bridge" }
case "bond":
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "Bond" }
case "vlan":
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "VLAN" }
case "vm_lxc":
return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" }
case "virtual":
return { color: "bg-orange-500/10 text-orange-500 border-orange-500/20", label: "Virtual" }
default:
return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" }
}
}
const getVMTypeBadge = (vmType: string | undefined) => {
if (vmType === "lxc") {
return { color: "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", label: "LXC" }
} else if (vmType === "vm") {
return { color: "bg-purple-500/10 text-purple-500 border-purple-500/20", label: "VM" }
}
return { color: "bg-gray-500/10 text-gray-500 border-gray-500/20", label: "Unknown" }
}
const formatSpeed = (speed: number): string => {
if (speed === 0) return "N/A"
if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbps`
return `${speed} Mbps`
}
export function NetworkCard({ interface_, timeframe, onClick }: NetworkCardProps) {
const typeBadge = getInterfaceTypeBadge(interface_.type)
const vmTypeBadge = interface_.vm_type ? getVMTypeBadge(interface_.vm_type) : null
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(getNetworkUnit())
const [trafficData, setTrafficData] = useState<{ received: number; sent: number }>({
received: 0,
sent: 0,
})
useEffect(() => {
const handleUnitChange = () => {
setNetworkUnit(getNetworkUnit())
}
window.addEventListener("networkUnitChanged", handleUnitChange)
window.addEventListener("storage", handleUnitChange)
return () => {
window.removeEventListener("networkUnitChanged", handleUnitChange)
window.removeEventListener("storage", handleUnitChange)
}
}, [])
useEffect(() => {
const fetchTrafficData = async () => {
try {
const data = await fetchApi(`/api/network/${interface_.name}/metrics?timeframe=${timeframe}`)
if (data.data && data.data.length > 0) {
const lastPoint = data.data[data.data.length - 1]
const firstPoint = data.data[0]
const receivedGB = Math.max(0, (lastPoint.netin || 0) - (firstPoint.netin || 0))
const sentGB = Math.max(0, (lastPoint.netout || 0) - (firstPoint.netout || 0))
setTrafficData({
received: receivedGB,
sent: sentGB,
})
}
} catch (error) {
console.error("[v0] Failed to fetch traffic data for card:", error)
setTrafficData({ received: 0, sent: 0 })
}
}
if (interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm") {
fetchTrafficData()
const interval = setInterval(fetchTrafficData, 60000)
return () => clearInterval(interval)
}
}, [interface_.name, interface_.status, interface_.vm_type, timeframe])
const getTimeframeLabel = () => {
switch (timeframe) {
case "hour":
return "Last Hour"
case "day":
return "Last 24 Hours"
case "week":
return "Last 7 Days"
case "month":
return "Last 30 Days"
case "year":
return "Last Year"
default:
return "Last 24 Hours"
}
}
return (
<Card className="bg-card border-border hover:bg-white/5 transition-colors cursor-pointer" onClick={onClick}>
<CardContent className="p-4">
<div className="flex flex-col gap-3">
{/* First row: Icon, Name, Type Badge, Status */}
<div className="flex items-center gap-3 flex-wrap">
<Wifi className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<div className="flex items-center gap-2 min-w-0 flex-1 flex-wrap">
<div className="font-medium text-foreground">{interface_.name}</div>
{vmTypeBadge ? (
<Badge variant="outline" className={vmTypeBadge.color}>
{vmTypeBadge.label}
</Badge>
) : (
<Badge variant="outline" className={typeBadge.color}>
{typeBadge.label}
</Badge>
)}
{interface_.vm_name && (
<div className="text-sm text-muted-foreground truncate"> {interface_.vm_name}</div>
)}
{interface_.type === "bridge" && interface_.bridge_physical_interface && (
<div className="text-sm text-blue-500 font-medium flex items-center gap-1 flex-wrap break-all">
{interface_.bridge_physical_interface}
</div>
)}
</div>
<Badge
variant="outline"
className={
interface_.status === "up"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{interface_.status.toUpperCase()}
</Badge>
</div>
{/* Second row: Details - Responsive layout */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<div className="text-muted-foreground text-xs">
{interface_.type === "vm_lxc" ? "VMID" : "IP Address"}
</div>
<div className="font-medium text-foreground font-mono text-sm truncate">
{interface_.type === "vm_lxc"
? (interface_.vmid ?? "N/A")
: interface_.addresses.length > 0
? interface_.addresses[0].ip
: "N/A"}
</div>
</div>
<div>
<div className="text-muted-foreground text-xs">Speed</div>
<div className="font-medium text-foreground flex items-center gap-1 text-xs">
<Zap className="h-3 w-3" />
{formatSpeed(interface_.speed)}
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="text-muted-foreground text-xs">{getTimeframeLabel()}</div>
<div className="font-medium text-foreground text-xs">
{interface_.status.toLowerCase() === "up" && interface_.vm_type !== "vm" ? (
<>
<span className="text-green-500"> {formatNetworkTraffic(trafficData.received * 1024 * 1024 * 1024, networkUnit)}</span>
{" / "}
<span className="text-blue-500"> {formatNetworkTraffic(trafficData.sent * 1024 * 1024 * 1024, networkUnit)}</span>
</>
) : (
<>
<span className="text-green-500"> {formatNetworkTraffic(interface_.bytes_recv || 0, networkUnit)}</span>
{" / "}
<span className="text-blue-500"> {formatNetworkTraffic(interface_.bytes_sent || 0, networkUnit)}</span>
</>
)}
</div>
</div>
{interface_.mac_address && (
<div className="col-span-2 md:col-span-1">
<div className="text-muted-foreground text-xs">MAC</div>
<div className="font-medium text-foreground font-mono text-xs truncate">{interface_.mac_address}</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,329 @@
"use client"
import { useState, useEffect } from "react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2 } from 'lucide-react'
import { fetchApi } from "../lib/api-config"
import { getNetworkUnit } from "../lib/format-network"
interface NetworkMetricsData {
time: string
timestamp: number
netIn: number
netOut: number
}
interface NetworkTrafficChartProps {
timeframe: string
interfaceName?: string
onTotalsCalculated?: (totals: { received: number; sent: number }) => void
refreshInterval?: number // En milisegundos, por defecto 60000 (60 segundos)
networkUnit?: "Bytes" | "Bits" // Added networkUnit prop
}
const CustomNetworkTooltip = ({ active, payload, label, networkUnit }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">
{entry.value.toFixed(3)} {networkUnit === "Bits" ? "Gb" : "GB"}
</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function NetworkTrafficChart({
timeframe,
interfaceName,
onTotalsCalculated,
refreshInterval = 60000,
networkUnit: networkUnitProp, // Rename prop to avoid conflict
}: NetworkTrafficChartProps) {
const [data, setData] = useState<NetworkMetricsData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isInitialLoad, setIsInitialLoad] = useState(true)
const [visibleLines, setVisibleLines] = useState({
netIn: true,
netOut: true,
})
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">(
networkUnitProp || getNetworkUnit()
)
useEffect(() => {
const handleUnitChange = () => {
const newUnit = getNetworkUnit()
setNetworkUnit(newUnit)
}
window.addEventListener("networkUnitChanged", handleUnitChange)
window.addEventListener("storage", handleUnitChange)
return () => {
window.removeEventListener("networkUnitChanged", handleUnitChange)
window.removeEventListener("storage", handleUnitChange)
}
}, [])
useEffect(() => {
if (networkUnitProp) {
setNetworkUnit(networkUnitProp)
}
}, [networkUnitProp])
useEffect(() => {
setIsInitialLoad(true)
fetchMetrics()
}, [timeframe, interfaceName, networkUnit])
useEffect(() => {
if (refreshInterval > 0) {
const interval = setInterval(() => {
fetchMetrics()
}, refreshInterval)
return () => clearInterval(interval)
}
}, [timeframe, interfaceName, refreshInterval, networkUnit]) // Added networkUnit to dependencies
const fetchMetrics = async () => {
if (isInitialLoad) {
setLoading(true)
}
setError(null)
try {
const apiPath = interfaceName
? `/api/network/${interfaceName}/metrics?timeframe=${timeframe}`
: `/api/node/metrics?timeframe=${timeframe}`
const result = await fetchApi<any>(apiPath)
if (!result.data || !Array.isArray(result.data)) {
throw new Error("Invalid data format received from server")
}
if (result.data.length === 0) {
setData([])
setLoading(false)
return
}
const transformedData = result.data.map((item: any, index: number) => {
const date = new Date(item.time * 1000)
let timeLabel = ""
if (timeframe === "hour") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "day") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "week") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
hour12: false,
})
} else if (timeframe === "year") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})
} else {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
})
}
let intervalSeconds = 60
if (index > 0) {
intervalSeconds = item.time - result.data[index - 1].time
}
const netInBytes = (item.netin || 0) * intervalSeconds
const netOutBytes = (item.netout || 0) * intervalSeconds
if (networkUnit === "Bits") {
return {
time: timeLabel,
timestamp: item.time,
netIn: Number(((netInBytes * 8) / 1024 / 1024 / 1024).toFixed(4)),
netOut: Number(((netOutBytes * 8) / 1024 / 1024 / 1024).toFixed(4)),
}
}
return {
time: timeLabel,
timestamp: item.time,
netIn: Number((netInBytes / 1024 / 1024 / 1024).toFixed(4)),
netOut: Number((netOutBytes / 1024 / 1024 / 1024).toFixed(4)),
}
})
setData(transformedData)
const totalReceivedGB = result.data.reduce((sum: number, item: any, index: number) => {
const intervalSeconds = index > 0 ? item.time - result.data[index - 1].time : 60
const netInBytes = (item.netin || 0) * intervalSeconds
return sum + (netInBytes / 1024 / 1024 / 1024)
}, 0)
const totalSentGB = result.data.reduce((sum: number, item: any, index: number) => {
const intervalSeconds = index > 0 ? item.time - result.data[index - 1].time : 60
const netOutBytes = (item.netout || 0) * intervalSeconds
return sum + (netOutBytes / 1024 / 1024 / 1024)
}, 0)
if (onTotalsCalculated) {
onTotalsCalculated({ received: totalReceivedGB, sent: totalSentGB })
}
if (isInitialLoad) {
setIsInitialLoad(false)
}
} catch (err: any) {
console.error("[v0] Error fetching network metrics:", err)
setError(err.message || "Error loading metrics")
} finally {
setLoading(false)
}
}
const tickInterval = Math.ceil(data.length / 8)
const handleLegendClick = (dataKey: string) => {
setVisibleLines((prev) => ({
...prev,
[dataKey as keyof typeof prev]: !prev[dataKey as keyof typeof prev],
}))
}
const renderLegend = (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-4 pb-2 flex-wrap">
{payload.map((entry: any, index: number) => {
const isVisible = visibleLines[entry.dataKey as keyof typeof visibleLines]
return (
<div
key={`legend-${index}`}
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleLegendClick(entry.dataKey)}
style={{ opacity: isVisible ? 1 : 0.4 }}
>
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: entry.color }} />
<span className="text-sm text-foreground">{entry.value}</span>
</div>
)
})}
</div>
)
}
if (loading && isInitialLoad) {
return (
<div className="flex items-center justify-center h-[300px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
<p className="text-muted-foreground text-sm">Network metrics not available yet</p>
<p className="text-xs text-red-500">{error}</p>
</div>
)
}
if (data.length === 0) {
return (
<div className="flex items-center justify-center h-[300px]">
<p className="text-muted-foreground text-sm">No network metrics available</p>
</div>
)
}
return (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 80 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={{
value: networkUnit === "Bits" ? "Gb" : "GB", // Dynamic label based on unit
angle: -90,
position: "insideLeft",
fill: "currentColor",
}}
domain={[0, "auto"]}
/>
<Tooltip content={<CustomNetworkTooltip networkUnit={networkUnit} />} /> // Pass networkUnit to tooltip
<Legend verticalAlign="top" height={36} content={renderLegend} />
<Area
type="monotone"
dataKey="netIn"
stroke="#10b981"
strokeWidth={2}
fill="#10b981"
fillOpacity={0.3}
name="Received"
hide={!visibleLines.netIn}
isAnimationActive={true}
animationDuration={300}
animationEasing="ease-in-out"
/>
<Area
type="monotone"
dataKey="netOut"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.3}
name="Sent"
hide={!visibleLines.netOut}
isAnimationActive={true}
animationDuration={300}
animationEasing="ease-in-out"
/>
</AreaChart>
</ResponsiveContainer>
)
}
+456
View File
@@ -0,0 +1,456 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
import { Loader2, TrendingUp, MemoryStick } from "lucide-react"
import { useIsMobile } from "../hooks/use-mobile"
import { fetchApi } from "@/lib/api-config"
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
{ value: "day", label: "24 Hours" },
{ value: "week", label: "7 Days" },
{ value: "month", label: "30 Days" },
]
interface NodeMetricsData {
time: string
timestamp: number
cpu: number
load: number
memoryTotal: number
memoryUsed: number
memoryFree: number
memoryZfsArc: number
}
const CustomCpuTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value}</span>
</div>
))}
</div>
</div>
)
}
return null
}
const CustomMemoryTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value} GB</span>
</div>
))}
</div>
</div>
)
}
return null
}
export function NodeMetricsCharts() {
const [timeframe, setTimeframe] = useState("day")
const [data, setData] = useState<NodeMetricsData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const isMobile = useIsMobile()
const [visibleLines, setVisibleLines] = useState({
cpu: { cpu: true, load: true },
memory: { memoryTotal: true, memoryUsed: true, memoryZfsArc: true, memoryFree: true },
})
// Check if ZFS ARC or Free memory have any non-zero values to decide if we should show them
const hasZfsArc = data.some(d => d.memoryZfsArc > 0)
const hasMemoryFree = data.some(d => d.memoryFree > 0)
useEffect(() => {
fetchMetrics()
}, [timeframe])
const fetchMetrics = async () => {
setLoading(true)
setError(null)
try {
const result = await fetchApi<any>(`/api/node/metrics?timeframe=${timeframe}`)
if (!result.data || !Array.isArray(result.data)) {
console.error("[v0] Invalid data format - data is not an array:", result)
throw new Error("Invalid data format received from server")
}
if (result.data.length === 0) {
console.warn("[v0] No data points received")
setData([])
setLoading(false)
return
}
if (result.data[0]?.loadavg) {
}
const transformedData = result.data.map((item: any) => {
const date = new Date(item.time * 1000)
let timeLabel = ""
if (timeframe === "hour") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "day") {
timeLabel = date.toLocaleString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
} else if (timeframe === "week") {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
hour12: false,
})
} else {
timeLabel = date.toLocaleString("en-US", {
month: "short",
day: "numeric",
})
}
return {
time: timeLabel,
timestamp: item.time,
cpu: item.cpu ? Number((item.cpu * 100).toFixed(2)) : 0,
load: item.loadavg
? typeof item.loadavg === "number"
? Number(item.loadavg.toFixed(2))
: Array.isArray(item.loadavg) && item.loadavg.length > 0
? Number(item.loadavg[0].toFixed(2))
: 0
: 0,
memoryTotal: item.memtotal ? Number((item.memtotal / 1024 / 1024 / 1024).toFixed(2)) : 0,
memoryUsed: item.memused ? Number((item.memused / 1024 / 1024 / 1024).toFixed(2)) : 0,
memoryFree: item.memfree ? Number((item.memfree / 1024 / 1024 / 1024).toFixed(2)) : 0,
memoryZfsArc: item.zfsarc ? Number((item.zfsarc / 1024 / 1024 / 1024).toFixed(2)) : 0,
}
})
setData(transformedData)
} catch (err: any) {
console.error("[v0] Error fetching node metrics:", err)
console.error("[v0] Error message:", err.message)
console.error("[v0] Error stack:", err.stack)
setError(err.message || "Error loading metrics")
} finally {
setLoading(false)
}
}
const tickInterval = Math.ceil(data.length / 8)
const handleLegendClick = (chartType: "cpu" | "memory", dataKey: string) => {
setVisibleLines((prev) => ({
...prev,
[chartType]: {
...prev[chartType],
[dataKey as keyof (typeof prev)[typeof chartType]]:
!prev[chartType][dataKey as keyof (typeof prev)[typeof chartType]],
},
}))
}
const renderLegend = (chartType: "cpu" | "memory") => (props: any) => {
const { payload } = props
return (
<div className="flex justify-center gap-4 pb-2 flex-wrap">
{payload.map((entry: any, index: number) => {
// For memory chart, hide ZFS ARC and Free from legend if they have no data
if (chartType === "memory") {
if (entry.dataKey === "memoryZfsArc" && !hasZfsArc) return null
if (entry.dataKey === "memoryFree" && !hasMemoryFree) return null
}
const isVisible = visibleLines[chartType][entry.dataKey as keyof (typeof visibleLines)[typeof chartType]]
return (
<div
key={`legend-${index}`}
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleLegendClick(chartType, entry.dataKey)}
style={{ opacity: isVisible ? 1 : 0.4 }}
>
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: entry.color }} />
<span className="text-sm text-foreground">{entry.value}</span>
</div>
)
})}
</div>
)
}
if (loading) {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</CardContent>
</Card>
</div>
)
}
if (error) {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
<p className="text-muted-foreground text-sm">Metrics data not available yet</p>
<p className="text-xs text-red-500">{error}</p>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center h-[300px] gap-2">
<p className="text-muted-foreground text-sm">Metrics data not available yet</p>
<p className="text-xs text-red-500">{error}</p>
</div>
</CardContent>
</Card>
</div>
)
}
if (data.length === 0) {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<p className="text-muted-foreground text-sm">No metrics data available</p>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardContent className="p-6">
<div className="flex items-center justify-center h-[300px]">
<p className="text-muted-foreground text-sm">No metrics data available</p>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
{/* Timeframe Selector */}
<div className="flex justify-end">
<Select value={timeframe} onValueChange={setTimeframe}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* CPU Usage + Load Average Chart */}
<Card className="bg-card border-border">
<CardHeader className="px-4 md:px-6">
<CardTitle className="text-foreground flex items-center">
<TrendingUp className="h-5 w-5 mr-2" />
CPU Usage & Load Average
</CardTitle>
</CardHeader>
<CardContent className="px-0 md:px-6">
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 60, left: 0, right: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
/>
<YAxis
yAxisId="left"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={
isMobile ? undefined : { value: "CPU %", angle: -90, position: "insideLeft", fill: "currentColor" }
}
domain={[0, "dataMax"]}
/>
<YAxis
yAxisId="right"
orientation="right"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={
isMobile ? undefined : { value: "Load", angle: 90, position: "insideRight", fill: "currentColor" }
}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomCpuTooltip />} />
<Legend verticalAlign="top" height={36} content={renderLegend("cpu")} />
<Area
yAxisId="left"
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.3}
name="CPU %"
hide={!visibleLines.cpu.cpu}
/>
<Area
yAxisId="right"
type="monotone"
dataKey="load"
stroke="#10b981"
strokeWidth={2}
fill="#10b981"
fillOpacity={0.3}
name="Load Avg"
hide={!visibleLines.cpu.load}
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Memory Usage Chart */}
<Card className="bg-card border-border">
<CardHeader className="px-4 md:px-6">
<CardTitle className="text-foreground flex items-center">
<MemoryStick className="h-5 w-5 mr-2" />
Memory Usage
</CardTitle>
</CardHeader>
<CardContent className="px-0 pr-2 md:px-6">
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data} margin={{ bottom: 60, left: 0, right: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
angle={-45}
textAnchor="end"
height={60}
interval={tickInterval}
/>
<YAxis
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: 12 }}
label={
isMobile ? undefined : { value: "GB", angle: -90, position: "insideLeft", fill: "currentColor" }
}
domain={[0, "dataMax"]}
/>
<Tooltip content={<CustomMemoryTooltip />} />
<Legend verticalAlign="top" height={36} content={renderLegend("memory")} />
<Area
type="monotone"
dataKey="memoryTotal"
stroke="#3b82f6"
strokeWidth={2}
fill="#3b82f6"
fillOpacity={0.1}
name="Total"
hide={!visibleLines.memory.memoryTotal}
/>
<Area
type="monotone"
dataKey="memoryUsed"
stroke="#10b981"
strokeWidth={2}
fill="#10b981"
fillOpacity={0.3}
name="Used"
hide={!visibleLines.memory.memoryUsed}
/>
{/* Only show ZFS ARC if there's data */}
{hasZfsArc && (
<Area
type="monotone"
dataKey="memoryZfsArc"
stroke="#f59e0b"
strokeWidth={2}
fill="#f59e0b"
fillOpacity={0.3}
name="ZFS ARC"
hide={!visibleLines.memory.memoryZfsArc}
/>
)}
{/* Only show Free memory if there's data */}
{hasMemoryFree && (
<Area
type="monotone"
dataKey="memoryFree"
stroke="#06b6d4"
strokeWidth={2}
fill="#06b6d4"
fillOpacity={0.3}
name="Free"
hide={!visibleLines.memory.memoryFree}
/>
)}
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+290
View File
@@ -0,0 +1,290 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import {
ChevronLeft,
ChevronRight,
X,
Sparkles,
LayoutDashboard,
HardDrive,
Network,
Box,
Cpu,
FileText,
Rocket,
} from "lucide-react"
import Image from "next/image"
import { Checkbox } from "./ui/checkbox"
interface OnboardingSlide {
id: number
title: string
description: string
image?: string
icon: React.ReactNode
gradient: string
}
const slides: OnboardingSlide[] = [
{
id: 0,
title: "Welcome to ProxMenux Monitor!",
description:
"Your new monitoring tool for Proxmox. Discover all the features that will help you manage and supervise your infrastructure efficiently.",
icon: <Sparkles className="h-16 w-16" />,
gradient: "from-blue-500 via-purple-500 to-pink-500",
},
{
id: 1,
title: "System Overview",
description:
"Monitor your server's status in real-time: CPU, memory, temperature, system load and more. Everything in an intuitive and easy-to-understand dashboard.",
image: "/images/onboarding/imagen1.png",
icon: <LayoutDashboard className="h-12 w-12" />,
gradient: "from-blue-500 to-cyan-500",
},
{
id: 2,
title: "Storage Management",
description:
"Visualize the status of all your disks and volumes. Detailed information on capacity, usage, SMART health, temperature and performance of each storage device.",
image: "/images/onboarding/imagen2.png",
icon: <HardDrive className="h-12 w-12" />,
gradient: "from-cyan-500 to-teal-500",
},
{
id: 3,
title: "Network Metrics",
description:
"Monitor network traffic in real-time. Bandwidth statistics, active interfaces, transfer speeds and historical usage graphs.",
image: "/images/onboarding/imagen3.png",
icon: <Network className="h-12 w-12" />,
gradient: "from-teal-500 to-green-500",
},
{
id: 4,
title: "Virtual Machines & Containers",
description:
"Manage all your VMs and LXC containers from one place. Status, allocated resources, current usage and quick controls for each virtual machine.",
image: "/images/onboarding/imagen4.png",
icon: <Box className="h-12 w-12" />,
gradient: "from-green-500 to-emerald-500",
},
{
id: 5,
title: "Hardware Information",
description:
"Complete details of your server hardware: CPU, RAM, GPU, disks, network, UPS and more. Technical specifications, models, serial numbers and status of each component.",
image: "/images/onboarding/imagen5.png",
icon: <Cpu className="h-12 w-12" />,
gradient: "from-emerald-500 to-blue-500",
},
{
id: 6,
title: "System Logs",
description:
"Access system logs in real-time. Filter by event type, search for specific errors and keep complete track of your server activity. Download the displayed logs for further analysis.",
image: "/images/onboarding/imagen6.png",
icon: <FileText className="h-12 w-12" />,
gradient: "from-blue-500 to-indigo-500",
},
{
id: 7,
title: "Ready for the Future!",
description:
"ProxMenux Monitor is prepared to receive updates and improvements that will be added gradually, improving the user experience and being able to execute ProxMenux functions from the web panel.",
icon: <Rocket className="h-16 w-16" />,
gradient: "from-indigo-500 via-purple-500 to-pink-500",
},
]
export function OnboardingCarousel() {
const [open, setOpen] = useState(false)
const [currentSlide, setCurrentSlide] = useState(0)
const [direction, setDirection] = useState<"next" | "prev">("next")
const [dontShowAgain, setDontShowAgain] = useState(false)
useEffect(() => {
const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
if (!hasSeenOnboarding) {
setOpen(true)
}
}, [])
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setDirection("next")
setCurrentSlide(currentSlide + 1)
} else {
if (dontShowAgain) {
localStorage.setItem("proxmenux-onboarding-seen", "true")
}
setOpen(false)
}
}
const handlePrev = () => {
if (currentSlide > 0) {
setDirection("prev")
setCurrentSlide(currentSlide - 1)
}
}
const handleSkip = () => {
if (dontShowAgain) {
localStorage.setItem("proxmenux-onboarding-seen", "true")
}
setOpen(false)
}
const handleClose = () => {
if (dontShowAgain) {
localStorage.setItem("proxmenux-onboarding-seen", "true")
}
setOpen(false)
}
const handleDotClick = (index: number) => {
setDirection(index > currentSlide ? "next" : "prev")
setCurrentSlide(index)
}
const slide = slides[currentSlide]
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl p-0 gap-0 overflow-hidden border-0 bg-transparent">
<DialogTitle className="sr-only">ProxMenux Onboarding</DialogTitle>
<div className="relative bg-card rounded-lg overflow-hidden shadow-2xl">
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
onClick={handleClose}
>
<X className="h-4 w-4" />
</Button>
<div
className={`relative h-48 md:h-64 bg-gradient-to-br ${slide.gradient} flex items-center justify-center overflow-hidden`}
>
<div className="absolute inset-0 bg-black/10" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
<div className="relative z-10 text-white">
{slide.image ? (
<div className="relative w-full h-36 md:h-48 flex items-center justify-center px-4">
<Image
src={slide.image || "/placeholder.svg"}
alt={slide.title}
width={600}
height={400}
className="rounded-lg shadow-2xl object-cover max-h-36 md:max-h-48"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.parentElement?.querySelector(".fallback-icon")
if (fallback) {
fallback.classList.remove("hidden")
}
}}
/>
<div className="fallback-icon hidden">{slide.icon}</div>
</div>
) : (
<div className="animate-pulse">{slide.icon}</div>
)}
</div>
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
</div>
<div className="p-4 md:p-8 space-y-3 md:space-y-6 max-h-[60vh] md:max-h-none overflow-y-auto">
<div className="space-y-2 md:space-y-3">
<h2 className="text-xl md:text-3xl font-bold text-foreground text-balance">{slide.title}</h2>
<p className="text-sm md:text-lg text-muted-foreground leading-relaxed text-pretty">
{slide.description}
</p>
</div>
<div className="flex items-center justify-center gap-2 py-2 md:py-4">
{slides.map((_, index) => (
<button
key={index}
onClick={() => handleDotClick(index)}
className={`transition-all duration-300 rounded-full ${
index === currentSlide
? "w-8 h-2.5 bg-blue-500 shadow-lg shadow-blue-500/50"
: "w-2.5 h-2.5 bg-muted-foreground/60 hover:bg-muted-foreground/80 border border-muted-foreground/40"
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 md:gap-4">
<Button
variant="ghost"
onClick={handlePrev}
disabled={currentSlide === 0}
className="gap-2 w-full sm:w-auto text-sm"
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<div className="flex gap-2 w-full sm:w-auto">
{currentSlide < slides.length - 1 ? (
<>
<Button
variant="outline"
onClick={handleSkip}
className="flex-1 sm:flex-none bg-transparent text-sm"
>
Skip
</Button>
<Button
onClick={handleNext}
className="gap-2 bg-blue-500 hover:bg-blue-600 flex-1 sm:flex-none text-sm"
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</>
) : (
<Button
onClick={handleNext}
className="gap-2 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 w-full sm:w-auto text-sm"
>
Get Started!
<Sparkles className="h-4 w-4" />
</Button>
)}
</div>
</div>
<div className="flex items-center justify-center gap-2 pt-2 pb-1">
<Checkbox
id="dont-show-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
/>
<label
htmlFor="dont-show-again"
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
>
Don't show this again
</label>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}
+467
View File
@@ -0,0 +1,467 @@
"use client"
import { useEffect, useRef, useState } from "react"
import {
User as UserIcon,
Upload,
Trash2,
Loader2,
Check,
AlertCircle,
Shield,
Lock,
X,
Settings2,
CheckCircle2,
} from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { fetchApi, getApiUrl, getAuthToken } from "../lib/api-config"
interface ProfileData {
success: boolean
username?: string | null
display_name?: string | null
has_avatar?: boolean
avatar_mtime?: number | null
avatar_content_type?: string | null
message?: string
}
interface ProfileProps {
/** Optional navigation hook so the page can link to Security for
* password / 2FA changes without redirecting through a URL. */
onOpenSecurity?: () => void
}
/**
* Profile page (Fase 2, v1.2.2).
*
* Lets the operator edit their **display name** and upload / remove
* their **avatar**. Username is read-only (changing it requires
* disabling and reconfiguring auth from Security). Password / 2FA
* are intentionally not editable from this page those live in
* Security to keep the "account security" surface in one place.
*
* Layout: centered, two cards (Profile + Account security shortcut).
* Display name uses the same Edit / Save / Cancel pattern as the
* Health Thresholds / Notifications panels read-only by default,
* the operator hits Edit to start typing.
*/
export function Profile({ onOpenSecurity }: ProfileProps) {
const [profile, setProfile] = useState<ProfileData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Display name: read-only by default, editable after pressing Edit.
// Mirrors the editMode pattern used in HealthThresholds / Notifications
// so the operator never types into a field that isn't ready to be saved.
const [displayEditMode, setDisplayEditMode] = useState(false)
const [displayDraft, setDisplayDraft] = useState("")
const [savingDisplay, setSavingDisplay] = useState(false)
const [savedDisplay, setSavedDisplay] = useState(false)
// Avatar state.
const [uploadingAvatar, setUploadingAvatar] = useState(false)
const [avatarError, setAvatarError] = useState<string | null>(null)
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const loadProfile = async () => {
try {
const data = await fetchApi<ProfileData>("/api/auth/profile")
setProfile(data)
setDisplayDraft(data.display_name || "")
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
}
useEffect(() => {
loadProfile()
}, [])
// Avatar fetch. Same blob-URL pattern as in AvatarMenu — the endpoint
// requires the Bearer header, which <img src=…> can't send. Plain
// `<img>` would render a broken image icon (the bug the user reported).
useEffect(() => {
let cancelled = false
let currentBlobUrl: string | null = null
if (profile?.has_avatar) {
const token = getAuthToken()
const url = `${getApiUrl("/api/auth/profile/avatar")}?v=${profile.avatar_mtime || ""}`
fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
.then(r => (r.ok ? r.blob() : null))
.then(blob => {
if (cancelled || !blob) return
currentBlobUrl = URL.createObjectURL(blob)
setAvatarBlobUrl(currentBlobUrl)
})
.catch(() => {
if (!cancelled) setAvatarBlobUrl(null)
})
} else {
setAvatarBlobUrl(null)
}
return () => {
cancelled = true
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl)
}
}, [profile?.has_avatar, profile?.avatar_mtime])
const initial = (profile?.display_name || profile?.username || "U")
.trim()
.charAt(0)
.toUpperCase()
const hasDisplayChanges = displayDraft !== (profile?.display_name || "")
const handleEditDisplay = () => {
setDisplayEditMode(true)
setSavedDisplay(false)
setError(null)
}
const handleCancelDisplay = () => {
setDisplayDraft(profile?.display_name || "")
setDisplayEditMode(false)
setError(null)
}
const handleSaveDisplayName = async () => {
if (!hasDisplayChanges) {
setDisplayEditMode(false)
return
}
setSavingDisplay(true)
setError(null)
setSavedDisplay(false)
try {
const data = await fetchApi<ProfileData>("/api/auth/profile", {
method: "PUT",
body: JSON.stringify({ display_name: displayDraft }),
})
if (!data.success) {
setError(data.message || "Failed to save display name")
return
}
setProfile(data)
setDisplayEditMode(false)
setSavedDisplay(true)
setTimeout(() => setSavedDisplay(false), 2500)
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("proxmenux:profile-changed"))
}
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setSavingDisplay(false)
}
}
const handleAvatarPick = () => fileInputRef.current?.click()
const handleAvatarFile = async (file: File) => {
setUploadingAvatar(true)
setAvatarError(null)
try {
const token = getAuthToken()
const headers: Record<string, string> = {}
if (token) headers["Authorization"] = `Bearer ${token}`
// Raw upload (Content-Type = the image's own MIME) — simpler than
// multipart and the backend handles both.
headers["Content-Type"] = file.type
const r = await fetch(getApiUrl("/api/auth/profile/avatar"), {
method: "POST",
headers,
body: file,
})
const data: ProfileData = await r.json().catch(() => ({ success: false }))
if (!r.ok || !data.success) {
setAvatarError(data.message || `Upload failed (${r.status})`)
return
}
setProfile(data)
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("proxmenux:profile-changed"))
}
} catch (e) {
setAvatarError(e instanceof Error ? e.message : String(e))
} finally {
setUploadingAvatar(false)
// Reset the input so picking the same file twice in a row still
// fires the change event.
if (fileInputRef.current) fileInputRef.current.value = ""
}
}
const handleAvatarDelete = async () => {
setUploadingAvatar(true)
setAvatarError(null)
try {
const token = getAuthToken()
const headers: Record<string, string> = {}
if (token) headers["Authorization"] = `Bearer ${token}`
const r = await fetch(getApiUrl("/api/auth/profile/avatar"), {
method: "DELETE",
headers,
})
const data: ProfileData = await r.json().catch(() => ({ success: false }))
if (!r.ok || !data.success) {
setAvatarError(data.message || `Delete failed (${r.status})`)
return
}
setProfile(data)
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("proxmenux:profile-changed"))
}
} catch (e) {
setAvatarError(e instanceof Error ? e.message : String(e))
} finally {
setUploadingAvatar(false)
}
}
if (loading) {
return (
<div className="max-w-2xl mx-auto">
<Card>
<CardContent className="p-8 flex items-center justify-center text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading profile
</CardContent>
</Card>
</div>
)
}
if (error && !profile) {
return (
<div className="max-w-2xl mx-auto">
<Card>
<CardContent className="p-6">
<div className="flex items-start gap-2 text-red-500">
<AlertCircle className="h-5 w-5 shrink-0 mt-0.5" />
<div>
<div className="font-medium">Failed to load profile</div>
<div className="text-xs text-muted-foreground mt-1 break-all">{error}</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="max-w-2xl mx-auto space-y-6">
<Card>
<CardHeader>
{/* Edit / Save / Cancel sit in the card header same pattern
as Health Thresholds and Notifications. Avatar actions
(upload / remove) stay independent of editMode because
they're explicit one-shot actions, not field edits. */}
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<UserIcon className="h-5 w-5 text-cyan-500" />
<CardTitle>User Profile</CardTitle>
</div>
<div className="flex items-center gap-2">
{savedDisplay && (
<span className="flex items-center gap-1 text-xs text-green-500">
<Check className="h-3.5 w-3.5" />
Saved
</span>
)}
{displayEditMode ? (
<>
<Button
variant="outline"
size="sm"
onClick={handleCancelDisplay}
disabled={savingDisplay}
className="h-7 text-xs"
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSaveDisplayName}
disabled={savingDisplay || !hasDisplayChanges}
className="h-7 text-xs bg-blue-600 hover:bg-blue-700"
>
{savingDisplay ? (
<Loader2 className="h-3 w-3 mr-1.5 animate-spin" />
) : (
<CheckCircle2 className="h-3 w-3 mr-1.5" />
)}
Save
</Button>
</>
) : (
<Button
variant="outline"
size="sm"
onClick={handleEditDisplay}
className="h-7 text-xs"
>
<Settings2 className="h-3 w-3 mr-1.5" />
Edit
</Button>
)}
</div>
</div>
<CardDescription>
Personal details rendered in the header avatar menu. None of this is required
the username already covers identity. Display name and avatar are decorative.
</CardDescription>
</CardHeader>
<CardContent className="space-y-8">
{/* Avatar section
Big preview (160×160) so the operator can see the actual
image they uploaded. `object-cover` keeps the aspect
ratio and crops to fit the circle. */}
<div>
<Label className="text-sm">Avatar</Label>
<div className="flex flex-col sm:flex-row items-start gap-6 mt-3">
<div className="relative shrink-0">
{avatarBlobUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatarBlobUrl}
alt=""
className="w-40 h-40 rounded-full object-cover border border-border bg-cyan-500/5"
/>
) : (
<span className="w-40 h-40 rounded-full bg-cyan-500/15 text-cyan-600 dark:text-cyan-300 flex items-center justify-center text-6xl font-semibold border border-border">
{initial}
</span>
)}
{uploadingAvatar && (
<div className="absolute inset-0 rounded-full bg-black/50 flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-white" />
</div>
)}
</div>
<div className="flex flex-col gap-2 min-w-0">
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp,image/gif"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) handleAvatarFile(file)
}}
/>
<Button
variant="outline"
size="sm"
onClick={handleAvatarPick}
disabled={uploadingAvatar}
className="justify-start"
>
<Upload className="h-3.5 w-3.5 mr-2" />
{profile?.has_avatar ? "Replace avatar" : "Upload avatar"}
</Button>
{profile?.has_avatar && (
<Button
variant="outline"
size="sm"
onClick={handleAvatarDelete}
disabled={uploadingAvatar}
className="justify-start text-red-500 hover:text-red-500 hover:bg-red-500/10"
>
<Trash2 className="h-3.5 w-3.5 mr-2" />
Remove avatar
</Button>
)}
<p className="text-[11px] text-muted-foreground leading-relaxed max-w-xs">
PNG, JPEG, WebP or GIF. Up to 2 MB. The image isn&apos;t resized
render it square or pre-crop for best results in the header.
</p>
</div>
</div>
{avatarError && (
<div className="mt-3 text-xs text-red-500 flex items-start gap-1.5">
<X className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span className="break-all">{avatarError}</span>
</div>
)}
</div>
{/* ─── Username (read-only) ─── */}
<div>
<Label className="text-sm" htmlFor="profile-username">Username</Label>
<Input
id="profile-username"
value={profile?.username || ""}
disabled
className="mt-2 max-w-sm disabled:opacity-100 disabled:cursor-default"
/>
<p className="text-[11px] text-muted-foreground mt-1">
The login name. To change it, disable authentication and reconfigure from
Security.
</p>
</div>
{/* ─── Display name (Edit controls live in the card header) ─── */}
<div>
<Label className="text-sm" htmlFor="profile-display">
Display name <span className="text-muted-foreground font-normal">(optional)</span>
</Label>
<Input
id="profile-display"
value={displayDraft}
onChange={(e) => setDisplayDraft(e.target.value)}
placeholder={profile?.username || "Display name"}
maxLength={64}
disabled={!displayEditMode || savingDisplay}
className="mt-2 max-w-sm disabled:opacity-100 disabled:cursor-default"
/>
<p className="text-[11px] text-muted-foreground mt-1">
Shown above the username inside the avatar menu. Leave empty to show the
username itself. Up to 64 characters.
</p>
{error && displayEditMode && (
<div className="mt-2 text-xs text-red-500 flex items-start gap-1.5">
<X className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span className="break-all">{error}</span>
</div>
)}
</div>
</CardContent>
</Card>
{/* ─── Account security shortcut ─── */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-orange-500" />
<CardTitle>Account security</CardTitle>
</div>
<CardDescription>
Password, two-factor authentication and API tokens live in the Security panel.
</CardDescription>
</CardHeader>
<CardContent>
{onOpenSecurity ? (
<Button variant="outline" onClick={onOpenSecurity}>
<Lock className="h-4 w-4 mr-2" />
Open Security settings
</Button>
) : (
<p className="text-xs text-muted-foreground">
Open the Security tab from the navigation.
</p>
)}
</CardContent>
</Card>
</div>
)
}
+878
View File
@@ -0,0 +1,878 @@
"use client"
import { useState, useEffect, useMemo, useCallback } from "react"
import { Badge } from "./ui/badge"
import { Button } from "./ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { SystemOverview } from "./system-overview"
import { StorageOverview } from "./storage-overview"
import { NetworkMetrics } from "./network-metrics"
import { VirtualMachines } from "./virtual-machines"
import Hardware from "./hardware"
import { SystemLogs } from "./system-logs"
import { Settings } from "./settings"
import { Security } from "./security"
import { Profile } from "./profile"
import { About } from "./about"
import { OnboardingCarousel } from "./onboarding-carousel"
import { HealthStatusModal } from "./health-status-modal"
import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal"
import { getApiUrl, fetchApi } from "../lib/api-config"
import { TerminalPanel } from "./terminal-panel"
import { AvatarMenu } from "./avatar-menu"
import {
RefreshCw,
AlertTriangle,
CheckCircle,
XCircle,
Server,
Menu,
LayoutDashboard,
HardDrive,
NetworkIcon,
Box,
Cpu,
FileText,
SettingsIcon,
Terminal,
ShieldCheck,
Info,
} from "lucide-react"
import Image from "next/image"
import { ThemeToggle } from "./theme-toggle"
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"
interface SystemStatus {
status: "healthy" | "warning" | "critical"
uptime: string
lastUpdate: string
serverName: string
nodeId: string
}
interface FlaskSystemData {
hostname: string
node_id: string
uptime: string
cpu_usage: number
memory_usage: number
temperature: number
load_average: number[]
}
interface FlaskSystemInfo {
hostname: string
node_id: string
uptime: string
health: {
status: "healthy" | "warning" | "critical"
}
}
export function ProxmoxDashboard() {
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
status: "healthy",
uptime: "Loading...",
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
serverName: "Loading...",
nodeId: "Loading...",
})
const [isRefreshing, setIsRefreshing] = useState(false)
const [isServerConnected, setIsServerConnected] = useState(true)
const [componentKey, setComponentKey] = useState(0)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [activeTab, setActiveTab] = useState("overview")
const [infoCount, setInfoCount] = useState(0)
const [updateAvailable, setUpdateAvailable] = useState(false)
const [showNavigation, setShowNavigation] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0)
const [showHealthModal, setShowHealthModal] = useState(false)
const { showReleaseNotes, setShowReleaseNotes } = useVersionCheck()
// Category keys for health info count calculation
const HEALTH_CATEGORY_KEYS = [
{ key: "cpu", category: "temperature" },
{ key: "memory", category: "memory" },
{ key: "storage", category: "storage" },
{ key: "disks", category: "disks" },
{ key: "network", category: "network" },
{ key: "vms", category: "vms" },
{ key: "services", category: "pve_services" },
{ key: "logs", category: "logs" },
{ key: "updates", category: "updates" },
{ key: "security", category: "security" },
]
// Fetch ProxMenux update status
const fetchUpdateStatus = useCallback(async () => {
try {
const response = await fetchApi("/api/proxmenux/update-status")
if (response?.success && response?.update_available) {
const { stable, beta } = response.update_available
setUpdateAvailable(stable || beta)
}
} catch (error) {
// Silently fail - updateAvailable will remain false
}
}, [])
// Fetch health info count independently (for initial load and refresh)
const fetchHealthInfoCount = useCallback(async () => {
try {
const response = await fetchApi("/api/health/full")
let calculatedInfoCount = 0
if (response && response.health?.details) {
// Get categories that have dismissed items (these become INFO)
const customCats = new Set((response.custom_suppressions || []).map((cs: { category: string }) => cs.category))
const filteredDismissed = (response.dismissed || []).filter((item: { category: string }) => !customCats.has(item.category))
const categoriesWithDismissed = new Set<string>()
filteredDismissed.forEach((item: { category: string }) => {
const catMeta = HEALTH_CATEGORY_KEYS.find(c => c.category === item.category || c.key === item.category)
if (catMeta) {
categoriesWithDismissed.add(catMeta.key)
}
})
// Count effective INFO categories (original INFO + OK categories with dismissed)
HEALTH_CATEGORY_KEYS.forEach(({ key }) => {
const cat = response.health.details[key as keyof typeof response.health.details]
if (cat) {
const originalStatus = cat.status?.toUpperCase()
// Count as INFO if: originally INFO OR (originally OK and has dismissed items)
if (originalStatus === "INFO" || (originalStatus === "OK" && categoriesWithDismissed.has(key))) {
calculatedInfoCount++
}
}
})
}
setInfoCount(calculatedInfoCount)
} catch (error) {
// Silently fail - infoCount will remain at 0
}
}, [])
const fetchSystemData = useCallback(async () => {
try {
const data: FlaskSystemInfo = await fetchApi("/api/system-info")
const uptimeValue =
data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A"
const backendStatus = data.health?.status?.toUpperCase() || "OK"
let healthStatus: "healthy" | "warning" | "critical"
if (backendStatus === "CRITICAL") {
healthStatus = "critical"
} else if (backendStatus === "WARNING") {
healthStatus = "warning"
} else {
healthStatus = "healthy"
}
setSystemStatus({
status: healthStatus,
uptime: uptimeValue,
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
serverName: data.hostname || "Unknown",
nodeId: data.node_id || "Unknown",
})
setIsServerConnected(true)
} catch (error) {
// Expected to fail in v0 preview (no Flask server)
setIsServerConnected(false)
setSystemStatus((prev) => ({
...prev,
status: "critical",
serverName: "Server Offline",
nodeId: "Server Offline",
uptime: "N/A",
lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }),
}))
}
}, [])
useEffect(() => {
// Siempre fetch inicial
fetchSystemData()
fetchHealthInfoCount()
fetchUpdateStatus()
// En overview: cada 30 segundos para actualización frecuente del estado de salud
// En otras tabs: cada 60 segundos para reducir carga
let interval: ReturnType<typeof setInterval> | null = null
let healthInterval: ReturnType<typeof setInterval> | null = null
if (activeTab === "overview") {
interval = setInterval(fetchSystemData, 30000) // 30 segundos
healthInterval = setInterval(fetchHealthInfoCount, 30000) // Also refresh info count
} else {
interval = setInterval(fetchSystemData, 60000) // 60 segundos
healthInterval = setInterval(fetchHealthInfoCount, 60000) // Also refresh info count
}
return () => {
if (interval) clearInterval(interval)
if (healthInterval) clearInterval(healthInterval)
}
}, [fetchSystemData, fetchHealthInfoCount, fetchUpdateStatus, activeTab])
useEffect(() => {
const handleChangeTab = (event: CustomEvent) => {
const { tab } = event.detail
if (tab) {
setActiveTab(tab)
}
}
window.addEventListener("changeTab", handleChangeTab as EventListener)
return () => {
window.removeEventListener("changeTab", handleChangeTab as EventListener)
}
}, [])
// Auto-refresh terminal on mobile devices
// This fixes the issue where terminal doesn't connect properly on mobile/VPN
useEffect(() => {
if (activeTab === "terminal") {
const isMobileDevice = window.innerWidth < 768 ||
('ontouchstart' in window && navigator.maxTouchPoints > 0)
if (isMobileDevice) {
// Delay to allow initial connection attempt, then refresh to ensure proper connection
const timeoutId = setTimeout(() => {
setComponentKey(prev => prev + 1)
}, 500)
return () => clearTimeout(timeoutId)
}
}
}, [activeTab])
useEffect(() => {
const handleHealthStatusUpdate = (event: CustomEvent) => {
const { status, infoCount: newInfoCount } = event.detail
let healthStatus: "healthy" | "warning" | "critical"
if (status === "CRITICAL") {
healthStatus = "critical"
} else if (status === "WARNING") {
healthStatus = "warning"
} else {
healthStatus = "healthy"
}
setSystemStatus((prev) => ({
...prev,
status: healthStatus,
}))
// Update info count (INFO categories + dismissed items)
if (typeof newInfoCount === "number") {
setInfoCount(newInfoCount)
}
}
window.addEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
return () => {
window.removeEventListener("healthStatusUpdated", handleHealthStatusUpdate as EventListener)
}
}, [])
useEffect(() => {
if (
systemStatus.serverName &&
systemStatus.serverName !== "Loading..." &&
systemStatus.serverName !== "Server Offline"
) {
document.title = `${systemStatus.serverName} - ProxMenux Monitor`
} else {
document.title = "ProxMenux Monitor"
}
}, [systemStatus.serverName])
useEffect(() => {
let hideTimeout: ReturnType<typeof setTimeout> | null = null
let lastPosition = window.scrollY
const handleScroll = () => {
const currentScrollY = window.scrollY
const delta = currentScrollY - lastPosition
if (currentScrollY < 50) {
setShowNavigation(true)
} else if (delta > 2) {
if (hideTimeout) clearTimeout(hideTimeout)
hideTimeout = setTimeout(() => setShowNavigation(false), 20)
} else if (delta < -2) {
if (hideTimeout) clearTimeout(hideTimeout)
setShowNavigation(true)
}
lastPosition = currentScrollY
}
window.addEventListener("scroll", handleScroll, { passive: true })
return () => {
window.removeEventListener("scroll", handleScroll)
if (hideTimeout) clearTimeout(hideTimeout)
}
}, [])
const refreshData = async () => {
setIsRefreshing(true)
await fetchSystemData()
setComponentKey((prev) => prev + 1)
await new Promise((resolve) => setTimeout(resolve, 500))
setIsRefreshing(false)
}
const statusIcon = useMemo(() => {
switch (systemStatus.status) {
case "healthy":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "critical":
return <XCircle className="h-4 w-4 text-red-500" />
}
}, [systemStatus.status])
const statusColor = useMemo(() => {
switch (systemStatus.status) {
case "healthy":
return "bg-green-500/10 text-green-500 border-green-500/20"
case "warning":
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
case "critical":
return "bg-red-500/10 text-red-500 border-red-500/20"
}
}, [systemStatus.status])
const getActiveTabLabel = () => {
switch (activeTab) {
case "overview":
return "Overview"
case "storage":
return "Storage"
case "network":
return "Network"
case "vms":
return "VMs & LXCs"
case "hardware":
return "Hardware"
case "terminal":
return "Terminal"
case "logs":
return "System Logs"
case "security":
return "Security"
case "settings":
return "Settings"
case "profile":
return "Profile"
default:
return "Navigation Menu"
}
}
return (
<div className="min-h-screen bg-background">
<OnboardingCarousel />
<ReleaseNotesModal open={showReleaseNotes} onClose={() => setShowReleaseNotes(false)} />
{!isServerConnected && (
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
<div className="container mx-auto">
<div className="flex items-center space-x-2 text-red-500 mb-2">
<XCircle className="h-5 w-5" />
<span className="font-medium">ProxMenux Server Connection Failed</span>
</div>
<div className="text-sm text-red-500/80 space-y-1 ml-7">
<p> Check that the monitor.service is running correctly.</p>
<p> The ProxMenux server should start automatically on port 8008</p>
<p>
Try accessing:{" "}
<a href={getApiUrl("/api/health")} target="_blank" rel="noopener noreferrer" className="underline">
{getApiUrl("/api/health")}
</a>
</p>
</div>
</div>
</div>
)}
<header
className="border-b border-border bg-card sticky top-0 z-50 shadow-sm cursor-pointer hover:bg-accent/5 transition-colors"
onClick={() => setShowHealthModal(true)}
>
<div className="container mx-auto px-4 md:px-6 py-4 md:py-4">
{/* Logo and Title */}
<div className="flex items-start justify-between gap-3">
{/* Logo and Title */}
<div className="flex items-center space-x-2 md:space-x-3 min-w-0">
<div className="w-16 h-16 md:w-10 md:h-10 relative flex items-center justify-center bg-primary/10 flex-shrink-0">
<Image
src={updateAvailable ? "/images/proxmenux_update-logo.png" : "/images/proxmenux-logo.png"}
alt="ProxMenux Logo"
width={64}
height={64}
className="object-contain md:w-10 md:h-10"
priority
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.parentElement?.querySelector(".fallback-icon")
if (fallback) {
fallback.classList.remove("hidden")
}
}}
/>
<Server className="h-8 w-8 md:h-6 md:w-6 text-primary absolute fallback-icon hidden" />
</div>
<div className="min-w-0">
<h1 className="text-lg md:text-xl font-semibold text-foreground truncate">ProxMenux Monitor</h1>
<p className="text-xs md:text-sm text-muted-foreground">Proxmox System Dashboard</p>
<div className="lg:hidden flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
<Server className="h-3 w-3" />
<span className="truncate">Node: {systemStatus.serverName}</span>
</div>
</div>
</div>
{/* Desktop Actions */}
<div className="hidden lg:flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Server className="h-4 w-4 text-muted-foreground" />
<div className="text-sm">
<div className="font-medium text-foreground">Node: {systemStatus.serverName}</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className={statusColor}>
{statusIcon}
<span className="ml-1 capitalize">{systemStatus.status}</span>
</Badge>
{systemStatus.status === "healthy" && infoCount > 0 && (
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20">
<Info className="h-4 w-4" />
<span className="ml-1">{infoCount} info</span>
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground whitespace-nowrap">
Uptime: {systemStatus.uptime || "N/A"}
</div>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation()
refreshData()
}}
disabled={isRefreshing}
className="border-border/50 bg-transparent hover:bg-secondary"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
Refresh
</Button>
<div onClick={(e) => e.stopPropagation()}>
<ThemeToggle />
</div>
{/* User account dropdown Fase 1 (v1.2.2). Self-hides
when auth isn't enabled on this install. */}
<div onClick={(e) => e.stopPropagation()}>
<AvatarMenu
size="lg"
onOpenProfile={() => setActiveTab("profile")}
onOpenSecurity={() => setActiveTab("security")}
/>
</div>
</div>
{/* Mobile Actions variant D approved in demo:
Top-right: Refresh + Theme + Avatar (all with border)
Bottom row (under Node line): badges left-aligned with
the Node text column, Uptime right-aligned in the same
horizontal line. No extra row for Uptime so the
header doesn't grow vertically. */}
<div className="flex lg:hidden items-center gap-1.5 shrink-0">
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation()
refreshData()
}}
disabled={isRefreshing}
className="h-8 w-8 p-0 border-border/50 bg-transparent hover:bg-secondary"
aria-label="Refresh"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
</Button>
<div onClick={(e) => e.stopPropagation()}>
<ThemeToggle />
</div>
<div onClick={(e) => e.stopPropagation()}>
<AvatarMenu
size="lg"
onOpenProfile={() => setActiveTab("profile")}
onOpenSecurity={() => setActiveTab("security")}
/>
</div>
</div>
</div>
{/* Mobile bottom row badges (left, aligned with the title
column via pl-[3.25rem] = w-16 logo + space-x-2 gap-ish)
and Uptime (right). The pl matches the mobile logo width
+ the parent flex gap so the badges sit visually under
"Node: amd", not flush against the screen edge. */}
<div className="lg:hidden mt-2 flex items-center justify-between gap-2 pl-[4.5rem]">
<div className="flex items-center gap-1.5">
<Badge variant="outline" className={`${statusColor} text-xs px-2`}>
{statusIcon}
<span className="ml-1 capitalize">{systemStatus.status}</span>
</Badge>
{systemStatus.status === "healthy" && infoCount > 0 && (
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-xs px-2">
<Info className="h-3 w-3" />
<span className="ml-1">{infoCount}</span>
</Badge>
)}
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap">
Uptime: {systemStatus.uptime || "N/A"}
</span>
</div>
</div>
</header>
<div
className={`sticky z-40 bg-background
top-[120px] lg:top-[76px]
transition-all duration-700 ease-in-out
${showNavigation ? "translate-y-0 opacity-100" : "-translate-y-[120%] opacity-0 pointer-events-none"}
`}
>
<div className="container mx-auto px-4 lg:px-6 pt-4 lg:pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-0">
{/* Issue #191: 10 tabs after adding About. The grid wraps via
Tabs primitives so the extra column doesn't push the
triggers off-screen on common laptop widths. */}
<TabsList className="hidden lg:grid w-full grid-cols-10 bg-card border border-border">
<TabsTrigger
value="overview"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Overview
</TabsTrigger>
<TabsTrigger
value="storage"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Storage
</TabsTrigger>
<TabsTrigger
value="network"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Network
</TabsTrigger>
<TabsTrigger
value="vms"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
VMs & LXCs
</TabsTrigger>
<TabsTrigger
value="hardware"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Hardware
</TabsTrigger>
<TabsTrigger
value="logs"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
System Logs
</TabsTrigger>
<TabsTrigger
value="terminal"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Terminal
</TabsTrigger>
<TabsTrigger
value="security"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Security
</TabsTrigger>
<TabsTrigger
value="settings"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
Settings
</TabsTrigger>
<TabsTrigger
value="about"
className="data-[state=active]:bg-blue-500 data-[state=active]:text-white data-[state=active]:rounded-md"
>
About
</TabsTrigger>
</TabsList>
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<div className="lg:hidden">
<SheetTrigger asChild>
<Button
variant="outline"
className={`w-full justify-between border-border ${
activeTab ? "bg-blue-500/10 text-blue-500" : "bg-card"
}`}
>
<span>{getActiveTabLabel()}</span>
<Menu className="h-4 w-4" />
</Button>
</SheetTrigger>
</div>
<SheetContent side="top" className="bg-card border-border">
<div className="flex flex-col gap-2 mt-4">
<Button
variant="ghost"
onClick={() => {
setActiveTab("overview")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "overview"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<LayoutDashboard className="h-5 w-5" />
<span>Overview</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("storage")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "storage"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<HardDrive className="h-5 w-5" />
<span>Storage</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("network")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "network"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<NetworkIcon className="h-5 w-5" />
<span>Network</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("vms")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "vms"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Box className="h-5 w-5" />
<span>VMs & LXCs</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("hardware")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "hardware"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Cpu className="h-5 w-5" />
<span>Hardware</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("logs")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "logs"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<FileText className="h-5 w-5" />
<span>System Logs</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("terminal")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "terminal"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Terminal className="h-5 w-5" />
<span>Terminal</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("security")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "security"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<ShieldCheck className="h-5 w-5" />
<span>Security</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("settings")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "settings"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<SettingsIcon className="h-5 w-5" />
<span>Settings</span>
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab("about")
setMobileMenuOpen(false)
}}
className={`w-full justify-start gap-3 ${
activeTab === "about"
? "bg-blue-500/10 text-blue-500 border-l-4 border-blue-500 rounded-l-none"
: ""
}`}
>
<Info className="h-5 w-5" />
<span>About</span>
</Button>
</div>
</SheetContent>
</Sheet>
</Tabs>
</div>
</div>
<div className="container mx-auto px-4 md:px-6 py-4 md:py-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4 md:space-y-6">
<TabsContent value="overview" className="space-y-4 md:space-y-6 mt-0">
<SystemOverview key={`overview-${componentKey}`} />
</TabsContent>
<TabsContent value="storage" className="space-y-4 md:space-y-6 mt-0">
<StorageOverview key={`storage-${componentKey}`} />
</TabsContent>
<TabsContent value="network" className="space-y-4 md:space-y-6 mt-0">
<NetworkMetrics key={`network-${componentKey}`} />
</TabsContent>
<TabsContent value="vms" className="space-y-4 md:space-y-6 mt-0">
<VirtualMachines key={`vms-${componentKey}`} />
</TabsContent>
<TabsContent value="hardware" className="space-y-4 md:space-y-6 mt-0">
<Hardware key={`hardware-${componentKey}`} />
</TabsContent>
<TabsContent value="logs" className="space-y-4 md:space-y-6 mt-0">
<SystemLogs key={`logs-${componentKey}`} />
</TabsContent>
<TabsContent value="terminal" className="mt-0">
<TerminalPanel key={`terminal-${componentKey}`} />
</TabsContent>
<TabsContent value="security" className="space-y-4 md:space-y-6 mt-0">
<Security key={`security-${componentKey}`} />
</TabsContent>
{/* Profile tab not surfaced in the top tabs nav. The only
entry point is the avatar dropdown in the header (View
profile). v1.2.2 Fase 2. */}
<TabsContent value="profile" className="space-y-4 md:space-y-6 mt-0">
<Profile
key={`profile-${componentKey}`}
onOpenSecurity={() => setActiveTab("security")}
/>
</TabsContent>
<TabsContent value="settings" className="space-y-4 md:space-y-6 mt-0">
<Settings />
</TabsContent>
<TabsContent value="about" className="space-y-4 md:space-y-6 mt-0">
<About />
</TabsContent>
</Tabs>
<footer className="mt-8 md:mt-12 pt-4 md:pt-6 border-t border-border text-center text-xs md:text-sm text-muted-foreground">
<p className="font-medium mb-2">ProxMenux Monitor v1.2.1.3-beta</p>
<p>
<a
href="https://ko-fi.com/macrimi"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 hover:underline transition-colors"
>
Support and contribute to the project
</a>
</p>
</footer>
</div>
<HealthStatusModal open={showHealthModal} onOpenChange={setShowHealthModal} getApiUrl={getApiUrl} />
</div>
)
}
+291
View File
@@ -0,0 +1,291 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "./ui/button"
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog"
import { X, Sparkles, Thermometer, Activity, HardDrive, Shield, Globe, Cpu, Zap, Sliders, Wrench, RefreshCw, Server } from "lucide-react"
import { Checkbox } from "./ui/checkbox"
const APP_VERSION = "1.2.1.3-beta" // Sync with AppImage/package.json
interface ReleaseNote {
date: string
changes: {
added?: string[]
changed?: string[]
fixed?: string[]
}
}
export const CHANGELOG: Record<string, ReleaseNote> = {
"1.2.1.3-beta": {
date: "May 22, 2026",
changes: {
added: [
"LXC Update Detection - A new dedicated section in Settings (between Health Monitor Thresholds and Notifications) with a single toggle that gates the per-CT apt list --upgradable / apk list -u scan end-to-end. Default ON. When OFF the scan stops entirely (no pct exec calls), every type=lxc entry is purged from the managed-installs registry immediately, and the matching notification toggle in Notifications -> Services disappears from the UI while preserving its stored preference",
"LXC update checker auto-refresh - The checker now reads the mtime of the CT's package-manager metadata cache and runs apt-get update / apk update from outside via pct exec if it is older than 24h, with a 60s timeout and silent failure. Long-running appliance CTs whose caches were months stale now surface their real upstream backlog (a Debian 12 CT with a 524-day-old cache went from \"0 updates\" to \"117 (12 security)\" on lab hardware)",
],
changed: [
"AI Enhancement section in Notifications - Rewritten from a muted uppercase row that testers consistently scrolled past, to a normal-case foreground label with a leading Sparkles icon and a persistent badge (green Active when AI is enabled, neutral Optional when it isn't) so the feature is visible regardless of state",
],
fixed: [
"Terminal modals on HTTPS hosts - Every terminal modal (dashboard terminal, LXC terminal, script terminal) used to fail with WebSocket connection error on hosts with HTTPS enabled. Root cause: the gevent+SSL path stacked geventwebsocket's WebSocketHandler on top of flask-sock's protocol implementation, so the server emitted two consecutive HTTP/1.1 101 Switching Protocols headers and the browser closed the connection as a corrupt frame. Dropping handler_class=WebSocketHandler restores a single 101 response and lets the handshake complete normally",
"Health Monitor kernel updates on PVE 9.x (#208) - The System Updates -> Kernel/PVE row reported \"Kernel/PVE up to date\" on PVE 9.x hosts even when an update for the running kernel was waiting upstream. Three combined fixes: (a) the kernel-package prefix list now includes proxmox-kernel-* and proxmox-firmware-* (PVE 9.x ships kernels under proxmox-kernel-, not pve-kernel- as in 7.x/8.x), (b) the dry-run switched from apt-get upgrade --dry-run to apt-get dist-upgrade --dry-run so kernel updates packaged as new installs are visible at all, (c) the categoriser now reads uname -r and flags an update as a running-kernel update when the package matches the running release exactly or its branch meta-package (e.g. proxmox-kernel-6.14 for a host on 6.14.11-4-pve). The row text now distinguishes \"Running kernel update available (reboot required)\" from \"N kernel update(s) available (none for running kernel)\"",
],
},
},
"1.2.1.2-beta": {
date: "May 20, 2026",
changes: {
added: [
"Coral TPU installer - Uninstall path mirroring the NVIDIA flow, and registry-driven update notifications for both the PCIe gasket-dkms driver (tracked against feranick/gasket-driver) and the USB libedgetpu1 runtime (tracked via apt)",
"Disk I/O severity tiers - Sliding 24h window classifies dmesg ATA/SCSI errors into silent (0-10), WARNING (11-100) and CRITICAL (100+ or any hard error like UNC / Buffer I/O / Sense Key Hardware Error), so quiet days stay quiet and a single Buffer I/O event still pages immediately",
"Quiet Hours buffering - Events suppressed during a channel's quiet window are now persisted to SQLite and released as a grouped summary when the window closes, instead of being silently dropped",
],
changed: [
"Burst aggregation wording - Burst summaries now report only the additional events that arrived after the initial individual alert, so the operator no longer sees the first event counted twice (\"+N more X in window\" instead of the old \"N X in window\" overlap)",
"Known-error classifier - Word-boundary regex on ATA/UNC patterns so kernel messages like nvidia_uvm:FatalError are no longer misclassified as ATA cable issues",
"Health journal context - Excludes proxmenux-monitor.service systemd lines so internal watchdog SIGKILLs no longer leak into the body of unrelated kernel events",
"Resolved notifications severity - The \"previous severity\" now matches the severity the user actually saw in the notification, not whatever escalated value silently landed in the DB during the 24h same-key cooldown",
"log2ram apply path - The auto/update flow now restarts log2ram after writing the new size, so a configured 512M actually takes effect on the running tmpfs (previously left at 128M until a manual restart)",
"VM/CT control errors - Failed start/stop/restart now surfaces the real pvesh stderr (e.g. \"no space left on device\") in the UI toast and fires a vm_fail / ct_fail notification, instead of a bare 500 INTERNAL SERVER ERROR",
"Mobile design of Quiet Hours / Daily Digest - Time inputs are now full-height with inline labels instead of the cramped grid layout that overflowed on narrow screens",
],
fixed: [
"ATA disk error not recorded - disk_observations is now written before the SMART gate, so transient errors that don't yet trip SMART still build the per-disk history",
"Quiet Hours toggle not persisting - get_settings now returns the per-channel quiet_*/digest_* fields so the toggle's state reloads correctly after a refresh",
"Frontend 401 cascade - Login screen no longer swallows the 401 forever after a brief stale-token state; the dedup flag is cleared on mount and on successful login",
],
},
},
"1.2.1.1-beta": {
date: "May 9, 2026",
changes: {
added: [
"Post-install function update detection - The Monitor now tracks installed ProxMenux optimizations (Log2Ram, Memory Settings, System Limits, Logrotate...) and notifies when a newer version of any of them is available, with one-click apply",
"Health Monitor Thresholds - Per-category warning and critical levels for CPU, memory, temperature, storage and more, configurable from Settings",
"NVIDIA driver update notifications - Kernel-aware detection of new compatible driver versions, surfaced in the Hardware tab and as notifications when a newer build is published upstream",
"Secure Gateway update flow - One-click Tailscale update from Settings with Last-checked / Installed / Latest indicators and notification when a new version is available",
"Helper-Scripts menu - Richer context and useful information for each entry, making it easier to know what every script does before running it",
],
changed: [
"Disk temperature monitoring - Improved readings, smarter caching across SMART probes and a redesigned history modal that opens at 24h by default with min/avg/max statistics",
"VM and LXC modal - Expanded with additional information so a single panel covers the data you previously had to look up across multiple tabs",
"Page load - Faster first paint and lighter network usage on the Overview, Storage and Hardware tabs",
"Security improvements - Tighter authentication checks across notification, scripts and terminal endpoints, plus a more conservative default policy for new installs",
],
fixed: [
"NVIDIA installer - The version menu now respects the running kernel compatibility window, only offering driver branches that won't fail to compile",
"NVIDIA installer on Alpine LXC - Container-side userspace install reworked so it succeeds on Alpine hosts, and free-space detection works reliably across all storage layouts",
"NVIDIA installer with NVENC patch - When the host has the NVENC patch applied, the version menu narrows to drivers supported by the patch so reinstalling never silently loses it",
"Webhook URL - PVE notification webhook now follows the active SSL state automatically, switching between http and https when you toggle HTTPS in the panel",
],
},
},
"1.1.2-beta": {
date: "March 18, 2026",
changes: {
added: [
"Temperature & Latency Charts - Real-time visual monitoring with interactive graphs",
"WebSocket Terminal - Direct access to Proxmox host and LXC containers terminal",
"AI-Enhanced Notifications - Intelligent message formatting with multi-provider support (OpenAI, Groq, Anthropic, Ollama)",
"Security Section - Comprehensive security settings for ProxMenux and Proxmox",
"VPN Integration - Easy Tailscale VPN installation and configuration",
"GPU Scripts - Installation utilities for Intel, AMD and NVIDIA drivers",
"Disk Observations System - Track and document disk health observations over time",
"Enhanced Health Monitor - Configurable monitoring with advanced settings panel",
],
changed: [
"Improved overall performance with optimized data fetching",
"Notifications now support rich formatting with contextual emojis",
"Health monitor now configurable from Settings section",
"Better Proxmox service name translation for non-expert users",
],
fixed: [
"Fixed notification message truncation for large backup reports",
"Improved disk error deduplication to prevent repeated alerts",
"Corrected AI provider base URL handling for OpenAI-compatible APIs",
],
},
},
"1.0.1": {
date: "November 11, 2025",
changes: {
added: [
"Proxy Support - Access ProxMenux through reverse proxies with full functionality",
"Authentication System - Secure your dashboard with password protection",
"PCIe Link Speed Detection - View NVMe drive connection speeds and detect performance issues",
"Two-Factor Authentication (2FA) - Enhanced security with TOTP support",
"Health Monitoring System - Comprehensive system health checks with dismissible warnings",
],
changed: [
"Optimized VM & LXC page - Reduced CPU usage by 85% through intelligent caching",
"Storage metrics now separate local and remote storage for clarity",
],
fixed: [
"Fixed dark mode text contrast issues in various components",
"Corrected storage calculation discrepancies between Overview and Storage pages",
],
},
},
"1.0.0": {
date: "October 15, 2025",
changes: {
added: [
"Initial release of ProxMenux Monitor",
"Real-time system monitoring dashboard",
"Storage management with SMART health monitoring",
"Network metrics and bandwidth tracking",
"VM & LXC container management",
"Hardware information display",
"System logs viewer with filtering",
],
},
},
}
const CURRENT_VERSION_FEATURES = [
{
icon: <RefreshCw className="h-5 w-5" />,
text: "Post-install function update detection - The Monitor tracks installed ProxMenux optimizations and notifies when a newer version of any of them is available, with one-click apply",
},
{
icon: <Sliders className="h-5 w-5" />,
text: "Health Monitor Thresholds - Per-category warning and critical levels for CPU, memory, temperature, storage and more, fully configurable from Settings",
},
{
icon: <Cpu className="h-5 w-5" />,
text: "NVIDIA driver update notifications - Kernel-aware detection of new compatible driver versions, surfaced in the Hardware tab and as notifications when a newer build is published",
},
{
icon: <Globe className="h-5 w-5" />,
text: "Secure Gateway update flow - One-click Tailscale update from Settings, with version indicators and notification when a new release is available",
},
{
icon: <Wrench className="h-5 w-5" />,
text: "Helper-Scripts menu - Richer context and useful information for each entry, so you know what every script does before running it",
},
{
icon: <Thermometer className="h-5 w-5" />,
text: "Improved disk temperature monitoring - Better readings, smarter caching across SMART probes and a redesigned history modal that opens at 24h by default",
},
{
icon: <Server className="h-5 w-5" />,
text: "VM and LXC modal expanded - Additional information consolidated into a single panel so you don't have to look it up across multiple tabs",
},
{
icon: <Zap className="h-5 w-5" />,
text: "Faster page load and tighter security - Lighter network usage on the main tabs, plus stricter authentication checks across notification, scripts and terminal endpoints",
},
]
interface ReleaseNotesModalProps {
open: boolean
onClose: () => void
}
export function ReleaseNotesModal({ open, onClose }: ReleaseNotesModalProps) {
const [dontShowAgain, setDontShowAgain] = useState(false)
const handleClose = () => {
if (dontShowAgain) {
localStorage.setItem("proxmenux-last-seen-version", APP_VERSION)
}
onClose()
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[85vh] p-0 gap-0 border-0 bg-transparent">
<DialogTitle className="sr-only">Release Notes - Version {APP_VERSION}</DialogTitle>
<div className="relative bg-card rounded-lg shadow-2xl h-full flex flex-col max-h-[85vh]">
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 z-50 h-8 w-8 rounded-full bg-background/80 backdrop-blur-sm hover:bg-background"
onClick={handleClose}
>
<X className="h-4 w-4" />
</Button>
<div className="relative h-32 md:h-40 bg-gradient-to-br from-amber-500 via-orange-500 to-red-500 flex items-center justify-center overflow-hidden flex-shrink-0">
<div className="absolute inset-0 bg-black/10" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.1),transparent)]" />
<div className="relative z-10 text-white animate-pulse">
<Sparkles className="h-12 w-12 md:h-14 md:w-14" />
</div>
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-2xl" />
<div className="absolute bottom-10 right-10 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
</div>
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-4 md:space-y-6 min-h-0">
<div className="space-y-2">
<h2 className="text-xl md:text-2xl font-bold text-foreground text-balance">
What's New in Version {APP_VERSION}
</h2>
<p className="text-sm text-muted-foreground leading-relaxed">
We've added exciting new features and improvements to make ProxMenux Monitor even better!
</p>
</div>
<div className="space-y-2">
{CURRENT_VERSION_FEATURES.map((feature, index) => (
<div
key={index}
className="flex items-start gap-2 md:gap-3 p-3 rounded-lg bg-muted/50 border border-border/50 hover:bg-muted/70 transition-colors"
>
<div className="text-orange-500 mt-0.5 flex-shrink-0">{feature.icon}</div>
<p className="text-xs md:text-sm text-foreground leading-relaxed">{feature.text}</p>
</div>
))}
</div>
</div>
<div className="flex-shrink-0 p-6 md:p-8 pt-4 border-t border-border/50 bg-card">
<div className="flex flex-col gap-3">
<Button
onClick={handleClose}
className="w-full bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600"
>
<Sparkles className="h-4 w-4 mr-2" />
Got it!
</Button>
<div className="flex items-center justify-center gap-2">
<Checkbox
id="dont-show-version-again"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked as boolean)}
/>
<label
htmlFor="dont-show-version-again"
className="text-xs md:text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer select-none"
>
Don't show again for this version
</label>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}
export function useVersionCheck() {
const [showReleaseNotes, setShowReleaseNotes] = useState(false)
useEffect(() => {
const lastSeenVersion = localStorage.getItem("proxmenux-last-seen-version")
if (lastSeenVersion !== APP_VERSION) {
setShowReleaseNotes(true)
}
}, [])
return { showReleaseNotes, setShowReleaseNotes }
}
export { APP_VERSION }
@@ -0,0 +1,998 @@
"use client"
import type React from "react"
import { useState, useEffect, useRef, useCallback } from "react"
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Loader2,
Activity,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
CornerDownLeft,
GripHorizontal,
ChevronDown,
Copy,
Clipboard,
} from "lucide-react"
import { copyTerminalSelection, pasteFromClipboard } from "@/lib/terminal-clipboard"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu"
import "xterm/css/xterm.css"
import { API_PORT } from "@/lib/api-config"
import { getTicketedWsUrl } from "@/lib/terminal-ws"
interface WebInteraction {
type: "yesno" | "menu" | "msgbox" | "input" | "inputbox"
id: string
title: string
message: string
options?: Array<{ label: string; value: string }>
default?: string
}
interface ScriptTerminalModalProps {
open: boolean
onClose: () => void
scriptPath: string
title: string
description: string
scriptName?: string
params?: Record<string, string>
}
export function ScriptTerminalModal({
open: isOpen,
onClose,
scriptPath,
title,
description,
params = { EXECUTION_MODE: "web" },
}: ScriptTerminalModalProps) {
const termRef = useRef<any>(null)
const wsRef = useRef<WebSocket | null>(null)
// Mirrors `isOpen` for use inside async closures (initializeTerminal)
// after dynamic imports resolve — captures the latest value without
// re-binding the closure.
const isOpenRef = useRef<boolean>(false)
const fitAddonRef = useRef<any>(null)
const sessionIdRef = useRef<string>(Math.random().toString(36).substring(2, 8))
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "online" | "offline">("connecting")
const [isComplete, setIsComplete] = useState(false)
const [currentInteraction, setCurrentInteraction] = useState<WebInteraction | null>(null)
const [interactionInput, setInteractionInput] = useState("")
const checkConnectionInterval = useRef<NodeJS.Timeout | null>(null)
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const reconnectAttemptsRef = useRef(0)
const keepAliveIntervalRef = useRef<NodeJS.Timeout | null>(null)
const [isMobile, setIsMobile] = useState(false)
const [isTablet, setIsTablet] = useState(false)
const [isWaitingNextInteraction, setIsWaitingNextInteraction] = useState(false)
const waitingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const [modalHeight, setModalHeight] = useState(600)
const [isResizing, setIsResizing] = useState(false)
const resizeBarRef = useRef<HTMLDivElement>(null)
const modalHeightRef = useRef(600)
const terminalContainerRef = useRef<HTMLDivElement>(null)
const paramsRef = useRef(params)
// Keep paramsRef updated with latest params
useEffect(() => {
paramsRef.current = params
}, [params])
const attemptReconnect = useCallback(() => {
if (!isOpen || isComplete || reconnectAttemptsRef.current >= 3) {
return
}
reconnectAttemptsRef.current++
setConnectionStatus("connecting")
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
reconnectTimeoutRef.current = setTimeout(async () => {
if (wsRef.current?.readyState !== WebSocket.OPEN && termRef.current) {
if (wsRef.current) {
wsRef.current.close()
}
const wsUrl = getScriptWebSocketUrl(sessionIdRef.current)
// Single-use auth ticket appended as ?ticket=... — see lib/terminal-ws.ts.
const ws = new WebSocket(await getTicketedWsUrl(wsUrl))
wsRef.current = ws
ws.onopen = () => {
setConnectionStatus("online")
reconnectAttemptsRef.current = 0
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
}
keepAliveIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }))
}
}, 30000)
const initMessage = {
script_path: scriptPath,
params: paramsRef.current,
}
ws.send(JSON.stringify(initMessage))
setTimeout(() => {
if (fitAddonRef.current && termRef.current && ws.readyState === WebSocket.OPEN) {
const cols = termRef.current.cols
const rows = termRef.current.rows
ws.send(JSON.stringify({ type: "resize", cols, rows }))
}
}, 100)
}
ws.onmessage = (event) => {
// Filter out pong responses from heartbeat
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
return
}
try {
const msg = JSON.parse(event.data)
if (msg.type === "web_interaction" && msg.interaction) {
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
setCurrentInteraction({
type: msg.interaction.type,
id: msg.interaction.id,
title: msg.interaction.title || "",
message: msg.interaction.message || "",
options: msg.interaction.options,
default: msg.interaction.default,
})
return
}
if (msg.type === "error") {
termRef.current?.writeln(`\x1b[31m${msg.message}\x1b[0m`)
return
}
} catch {}
termRef.current?.write(event.data)
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
}
ws.onerror = () => {
setConnectionStatus("offline")
}
ws.onclose = (event) => {
setConnectionStatus("offline")
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
keepAliveIntervalRef.current = null
}
if (!isComplete && reconnectAttemptsRef.current < 3) {
reconnectTimeoutRef.current = setTimeout(attemptReconnect, 2000)
} else {
setIsComplete(true)
}
}
}
}, 1000)
}, [isOpen, isComplete, scriptPath])
const sendKey = useCallback((key: string) => {
if (!termRef.current) return
const keyMap: Record<string, string> = {
escape: "\x1b",
tab: "\t",
up: "\x1bOA",
down: "\x1bOB",
left: "\x1bOD",
right: "\x1bOC",
enter: "\r",
ctrlc: "\x03",
}
const sequence = keyMap[key]
if (sequence && wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(sequence)
}
}, [])
const initializeTerminal = async () => {
// Snapshot the open-state at call time. After the dynamic xterm
// imports resolve, bail out if the modal has since been closed —
// otherwise we attach a Terminal to a stale ref and open a WS that
// nobody reads. Audit Tier 6 — useEffect con `import("xterm")` sin
// cancelación.
const wasOpenAtCall = isOpenRef.current
const [TerminalClass, FitAddonClass] = await Promise.all([
import("xterm").then((mod) => mod.Terminal),
import("xterm-addon-fit").then((mod) => mod.FitAddon),
import("xterm/css/xterm.css"),
])
if (!wasOpenAtCall || !isOpenRef.current) return
const fontSize = window.innerWidth < 768 ? 12 : 16
const term = new TerminalClass({
rendererType: "dom",
fontFamily: '"MesloLGS NF", "FiraCode Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "Symbols Nerd Font", "Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace',
fontSize: fontSize,
lineHeight: 1,
cursorBlink: true,
scrollback: 2000,
disableStdin: false,
customGlyphs: true,
fontWeight: "500",
fontWeightBold: "700",
theme: {
background: "#000000",
foreground: "#ffffff",
cursor: "#ffffff",
cursorAccent: "#000000",
black: "#2e3436",
red: "#cc0000",
green: "#4e9a06",
yellow: "#c4a000",
blue: "#3465a4",
magenta: "#75507b",
cyan: "#06989a",
white: "#d3d7cf",
brightBlack: "#555753",
brightRed: "#ef2929",
brightGreen: "#8ae234",
brightYellow: "#fce94f",
brightBlue: "#729fcf",
brightMagenta: "#ad7fa8",
brightCyan: "#34e2e2",
brightWhite: "#eeeeec",
},
})
const fitAddon = new FitAddonClass()
term.loadAddon(fitAddon)
if (terminalContainerRef.current) {
term.open(terminalContainerRef.current)
}
termRef.current = term
fitAddonRef.current = fitAddon
setTimeout(() => {
if (fitAddonRef.current && termRef.current) {
fitAddonRef.current.fit()
}
}, 100)
const wsUrl = getScriptWebSocketUrl(sessionIdRef.current)
// Single-use auth ticket appended as ?ticket=... — see lib/terminal-ws.ts.
const ws = new WebSocket(await getTicketedWsUrl(wsUrl))
wsRef.current = ws
ws.onopen = () => {
setConnectionStatus("online")
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
}
keepAliveIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }))
}
}, 30000)
const initMessage = {
script_path: scriptPath,
params: paramsRef.current,
}
ws.send(JSON.stringify(initMessage))
setTimeout(() => {
if (fitAddonRef.current && termRef.current && ws.readyState === WebSocket.OPEN) {
const cols = termRef.current.cols
const rows = termRef.current.rows
ws.send(
JSON.stringify({
type: "resize",
cols: cols,
rows: rows,
}),
)
}
}, 100)
}
ws.onmessage = (event) => {
// Filter out pong responses from heartbeat - don't display in terminal
if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') {
return
}
try {
const msg = JSON.parse(event.data)
if (msg.type === "web_interaction" && msg.interaction) {
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
setCurrentInteraction({
type: msg.interaction.type,
id: msg.interaction.id,
title: msg.interaction.title || "",
message: msg.interaction.message || "",
options: msg.interaction.options,
default: msg.interaction.default,
})
return
}
if (msg.type === "error") {
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
return
}
} catch {
// Not JSON, es output normal de terminal
}
term.write(event.data)
setIsWaitingNextInteraction(false)
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
}
ws.onerror = (error) => {
setConnectionStatus("offline")
term.writeln("\x1b[31mWebSocket error occurred\x1b[0m")
}
ws.onclose = (event) => {
setConnectionStatus("offline")
term.writeln("\x1b[33mConnection closed\x1b[0m")
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
keepAliveIntervalRef.current = null
}
if (!isComplete) {
setIsComplete(true)
}
}
// Read `wsRef.current` inside the handler so reconnect (which swaps
// `wsRef.current` to a fresh WebSocket) doesn't leave us writing to the
// dead closure-captured `ws`. Without this fix, after reconnect the
// user's stdin disappears into the void. Audit residual #8.
term.onData((data) => {
const live = wsRef.current
if (live && live.readyState === WebSocket.OPEN) {
live.send(data)
}
})
checkConnectionInterval.current = setInterval(() => {
if (wsRef.current) {
setConnectionStatus(
wsRef.current.readyState === WebSocket.OPEN
? "online"
: wsRef.current.readyState === WebSocket.CONNECTING
? "connecting"
: "offline",
)
}
}, 500)
let resizeTimeout: NodeJS.Timeout | null = null
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout) clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(() => {
if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) {
fitAddonRef.current.fit()
wsRef.current.send(
JSON.stringify({
type: "resize",
cols: termRef.current.cols,
rows: termRef.current.rows,
}),
)
}
}, 100)
})
if (terminalContainerRef.current) {
resizeObserver.observe(terminalContainerRef.current)
}
}
useEffect(() => {
isOpenRef.current = isOpen
const savedHeight = localStorage.getItem("scriptModalHeight")
if (savedHeight) {
const height = Number.parseInt(savedHeight, 10)
setModalHeight(height)
modalHeightRef.current = height
}
if (isOpen) {
initializeTerminal()
} else {
if (checkConnectionInterval.current) {
clearInterval(checkConnectionInterval.current)
}
if (waitingTimeoutRef.current) {
clearTimeout(waitingTimeoutRef.current)
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
if (wsRef.current) {
wsRef.current.close()
wsRef.current = null
}
if (termRef.current) {
termRef.current.dispose()
termRef.current = null
}
if (keepAliveIntervalRef.current) {
clearInterval(keepAliveIntervalRef.current)
keepAliveIntervalRef.current = null
}
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
reconnectAttemptsRef.current = 0
setIsComplete(false)
setInteractionInput("")
setCurrentInteraction(null)
setIsWaitingNextInteraction(false)
setConnectionStatus("connecting")
}
}, [isOpen])
useEffect(() => {
const updateDeviceType = () => {
const width = window.innerWidth
const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0
const isTabletSize = width >= 768 && width <= 1366
setIsMobile(width < 768)
setIsTablet(isTouchDevice && isTabletSize)
}
updateDeviceType()
const handleResize = () => updateDeviceType()
window.addEventListener("resize", handleResize)
const handleVisibilityChange = () => {
if (!document.hidden && isOpen) {
if (wsRef.current?.readyState !== WebSocket.OPEN && !isComplete) {
attemptReconnect()
}
}
}
const handleFocus = () => {
if (isOpen && wsRef.current?.readyState !== WebSocket.OPEN && !isComplete) {
attemptReconnect()
}
}
let wakeLock: any = null
const requestWakeLock = async () => {
if ("wakeLock" in navigator && isOpen) {
try {
wakeLock = await (navigator as any).wakeLock.request("screen")
} catch (err) {
// Wake Lock no soportado o denegado, continuar sin él
}
}
}
requestWakeLock()
document.addEventListener("visibilitychange", handleVisibilityChange)
window.addEventListener("focus", handleFocus)
return () => {
window.removeEventListener("resize", handleResize)
document.removeEventListener("visibilitychange", handleVisibilityChange)
window.removeEventListener("focus", handleFocus)
if (wakeLock) {
wakeLock.release().catch(() => {})
}
}
}, [isOpen, isComplete, attemptReconnect])
const getScriptWebSocketUrl = (sid: string): string => {
if (typeof window === "undefined") {
return `ws://localhost:${API_PORT}/ws/script/${sid}`
}
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
if (isStandardPort) {
return `${wsProtocol}//${hostname}/ws/script/${sid}`
} else {
return `${wsProtocol}//${hostname}:${API_PORT}/ws/script/${sid}`
}
}
const handleInteractionResponse = (value: string) => {
if (!wsRef.current || !currentInteraction) {
return
}
if (value === "cancel" || value === "") {
setCurrentInteraction(null)
setInteractionInput("")
handleCloseModal()
return
}
const response = JSON.stringify({
type: "interaction_response",
id: currentInteraction.id,
value: value,
})
if (wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(response)
}
setCurrentInteraction(null)
setInteractionInput("")
waitingTimeoutRef.current = setTimeout(() => {
setIsWaitingNextInteraction(true)
}, 50)
}
const handleCloseModal = () => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.close()
}
if (checkConnectionInterval.current) {
clearInterval(checkConnectionInterval.current)
}
if (termRef.current) {
termRef.current.dispose()
}
onClose()
}
const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault()
e.stopPropagation()
setIsResizing(true)
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY
const startY = clientY
const startHeight = modalHeight
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
const currentY = "touches" in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY
const deltaY = currentY - startY
const newHeight = Math.max(300, Math.min(window.innerHeight - 50, startHeight + deltaY))
modalHeightRef.current = newHeight
setModalHeight(newHeight)
}
const handleEnd = () => {
const finalHeight = modalHeightRef.current
setIsResizing(false)
document.removeEventListener("mousemove", handleMove as any)
document.removeEventListener("mouseup", handleEnd)
document.removeEventListener("touchmove", handleMove as any)
document.removeEventListener("touchend", handleEnd)
document.removeEventListener("touchcancel", handleEnd)
localStorage.setItem("scriptModalHeight", finalHeight.toString())
if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) {
setTimeout(() => {
fitAddonRef.current?.fit()
wsRef.current?.send(
JSON.stringify({
type: "resize",
cols: termRef.current.cols,
rows: termRef.current.rows,
}),
)
}, 100)
}
}
document.addEventListener("mousemove", handleMove as any)
document.addEventListener("mouseup", handleEnd)
document.addEventListener("touchmove", handleMove as any, { passive: false })
document.addEventListener("touchend", handleEnd)
document.addEventListener("touchcancel", handleEnd)
}
const sendCommand = (command: string) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(command)
}
}
// Mobile clipboard helpers — see lib/terminal-clipboard.ts.
const handleCopy = async () => {
await copyTerminalSelection(termRef.current)
}
const handlePaste = async () => {
await pasteFromClipboard(sendCommand)
}
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className="max-w-7xl p-0 flex flex-col gap-0 overflow-hidden"
style={{
height: isMobile ? "80vh" : `${modalHeight}px`,
maxHeight: "none",
}}
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
hideClose
>
<DialogTitle className="sr-only">{title}</DialogTitle>
<div className="flex items-center gap-2 p-4 border-b">
<div>
<h2 className="text-lg font-semibold">{title}</h2>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
</div>
<div className="overflow-hidden relative flex-1">
<div ref={terminalContainerRef} className="w-full h-full" />
{isWaitingNextInteraction && !currentInteraction && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-sm text-muted-foreground">Processing...</p>
</div>
</div>
)}
</div>
{!isMobile && (
<div
ref={resizeBarRef}
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
className={`h-4 w-full cursor-row-resize transition-colors flex items-center justify-center group relative ${
isResizing ? "bg-blue-500" : "bg-zinc-800 hover:bg-blue-600"
}`}
style={{ touchAction: "none" }}
>
<GripHorizontal
className={`h-5 w-5 transition-colors pointer-events-none ${
isResizing ? "text-white" : "text-zinc-600 group-hover:text-white"
}`}
/>
</div>
)}
{(isMobile || isTablet) && (
<div className="flex items-center justify-center gap-1.5 px-1 py-2 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1b")
}}
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white min-w-[50px]"
>
ESC
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\t")
}}
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white min-w-[50px]"
>
TAB
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOA")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOB")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowDown className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOD")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\x1bOC")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white"
>
<ArrowRight className="h-4 w-4" />
</Button>
<Button
onPointerDown={(e) => {
e.preventDefault()
e.stopPropagation()
sendCommand("\r")
}}
variant="outline"
size="sm"
className="h-8 px-2.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border-blue-600/50 text-blue-400"
>
<CornerDownLeft className="h-4 w-4 mr-1" />
Enter
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs bg-zinc-800 hover:bg-zinc-700 border-zinc-700 text-white min-w-[65px] gap-1"
>
Ctrl
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="text-xs text-muted-foreground">Control Sequences</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => sendCommand("\x03")}>
<span className="font-mono text-xs mr-2">Ctrl+C</span>
<span className="text-muted-foreground text-xs">Cancel/Interrupt</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => sendCommand("\x18")}>
<span className="font-mono text-xs mr-2">Ctrl+X</span>
<span className="text-muted-foreground text-xs">Exit (nano)</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => sendCommand("\x12")}>
<span className="font-mono text-xs mr-2">Ctrl+R</span>
<span className="text-muted-foreground text-xs">Search history</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">Clipboard</DropdownMenuLabel>
<DropdownMenuItem onSelect={() => { void handleCopy() }}>
<Copy className="h-3.5 w-3.5 mr-2" />
<span className="text-xs">Copy selection</span>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => { void handlePaste() }}>
<Clipboard className="h-3.5 w-3.5 mr-2" />
<span className="text-xs">Paste</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<div className="flex items-center justify-between px-4 py-3 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-blue-500" />
<div
className={`w-2 h-2 rounded-full ${
connectionStatus === "online"
? "bg-green-500"
: connectionStatus === "connecting"
? "bg-blue-500"
: "bg-red-500"
}`}
title={
connectionStatus === "online"
? "Connected"
: connectionStatus === "connecting"
? "Connecting"
: "Disconnected"
}
></div>
<span className="text-xs text-muted-foreground">
{connectionStatus === "online"
? "Online"
: connectionStatus === "connecting"
? "Connecting..."
: "Offline"}
</span>
</div>
<Button
onClick={handleCloseModal}
variant="outline"
className="bg-red-600/20 hover:bg-red-600/30 border-red-600/50 text-red-400"
>
Close
</Button>
</div>
</DialogContent>
</Dialog>
{currentInteraction && (
<Dialog open={true}>
<DialogContent
className="max-w-4xl max-h-[80vh] overflow-y-auto animate-in fade-in-0 zoom-in-95 duration-100"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
hideClose
>
<DialogTitle>{currentInteraction.title}</DialogTitle>
<div className="space-y-4">
{/*
Render the interaction message as plain text. The message
comes through the WebSocket from a script running as root
a script bug or compromised author could embed `<script>` or
`<img onerror=...>` and run JS in the admin's browser, leaking
the JWT and any keys held in React state. `whitespace-pre-wrap`
already preserves the `\n` formatting we previously emulated
via `<br/>`, so we don't need any HTML conversion. See audit
Tier 2 #17b.
*/}
<p className="whitespace-pre-wrap break-words">
{currentInteraction.message.replace(/\\n/g, "\n")}
</p>
{currentInteraction.type === "yesno" && (
<div className="flex gap-2">
<Button
onClick={() => handleInteractionResponse("yes")}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white transition-all duration-150"
>
Yes
</Button>
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
)}
{currentInteraction.type === "menu" && currentInteraction.options && (
<div className="space-y-2">
{currentInteraction.options.map((option, index) => (
<Button
key={option.value}
onClick={() => handleInteractionResponse(option.value)}
variant="outline"
className="w-full justify-start hover:bg-blue-600 hover:text-white transition-all duration-100 animate-in fade-in-0 slide-in-from-left-2"
style={{ animationDelay: `${index * 30}ms` }}
>
{option.label}
</Button>
))}
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="w-full hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
)}
{(currentInteraction.type === "input" || currentInteraction.type === "inputbox") && (
<div className="space-y-2">
<Label>Your input:</Label>
<Input
value={interactionInput}
onChange={(e) => setInteractionInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleInteractionResponse(interactionInput)
}
}}
placeholder={currentInteraction.default || ""}
className="transition-all duration-150"
/>
<div className="flex gap-2">
<Button
onClick={() => handleInteractionResponse(interactionInput)}
className="flex-1 bg-blue-600 hover:bg-blue-700 transition-all duration-150"
>
Submit
</Button>
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
</div>
)}
{currentInteraction.type === "msgbox" && (
<div className="flex gap-2">
<Button
onClick={() => handleInteractionResponse("ok")}
className="flex-1 bg-blue-600 hover:bg-blue-700 transition-all duration-150"
>
OK
</Button>
<Button
onClick={() => handleInteractionResponse("cancel")}
variant="outline"
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
>
Cancel
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
)}
</>
)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
"use client"
import { LayoutDashboard, HardDrive, Network, Server, Cpu, FileText, SettingsIcon, Terminal } from "lucide-react"
const menuItems = [
{ name: "Overview", href: "/", icon: LayoutDashboard },
{ name: "Storage", href: "/storage", icon: HardDrive },
{ name: "Network", href: "/network", icon: Network },
{ name: "Virtual Machines", href: "/virtual-machines", icon: Server },
{ name: "Hardware", href: "/hardware", icon: Cpu },
{ name: "System Logs", href: "/logs", icon: FileText },
{ name: "Terminal", href: "/terminal", icon: Terminal },
{ name: "Settings", href: "/settings", icon: SettingsIcon },
]
const Sidebar = ({ currentPath, setOpen }) => {
const handleNavigation = (tabName: string) => {
// Dispatch custom event to change tab in dashboard
const event = new CustomEvent("changeTab", { detail: { tab: tabName } })
window.dispatchEvent(event)
setOpen(false)
}
return (
<div>
<button
onClick={() => handleNavigation("overview")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/" || currentPath === "/overview"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<LayoutDashboard className="h-5 w-5" />
<span>Overview</span>
</button>
<button
onClick={() => handleNavigation("storage")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/storage"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<HardDrive className="h-5 w-5" />
<span>Storage</span>
</button>
<button
onClick={() => handleNavigation("network")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/network"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Network className="h-5 w-5" />
<span>Network</span>
</button>
<button
onClick={() => handleNavigation("vms")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/virtual-machines"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Server className="h-5 w-5" />
<span>VMs & LXCs</span>
</button>
<button
onClick={() => handleNavigation("hardware")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/hardware"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Cpu className="h-5 w-5" />
<span>Hardware</span>
</button>
<button
onClick={() => handleNavigation("logs")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/logs"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<FileText className="h-5 w-5" />
<span>System Logs</span>
</button>
<button
onClick={() => handleNavigation("terminal")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/terminal"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<Terminal className="h-5 w-5" />
<span>Terminal</span>
</button>
<button
onClick={() => handleNavigation("settings")}
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
currentPath === "/settings"
? "bg-blue-500/10 text-blue-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
<SettingsIcon className="h-5 w-5" />
<span>Settings</span>
</button>
</div>
)
}
export default Sidebar
+236
View File
@@ -0,0 +1,236 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
import { formatStorage } from "@/lib/utils"
interface StorageData {
total: number
used: number
available: number
disks: DiskInfo[]
}
interface DiskInfo {
name: string
mountpoint: string
fstype: string
total: number
used: number
available: number
usage_percent: number
health: string
temperature: number
}
const fetchStorageData = async (): Promise<StorageData | null> => {
try {
const response = await fetch("/api/storage", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error("[v0] Failed to fetch storage data from Flask server:", error)
return null
}
}
export function StorageMetrics() {
const [storageData, setStorageData] = useState<StorageData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
setError(null)
const result = await fetchStorageData()
if (!result) {
setError("Flask server not available. Please ensure the server is running.")
} else {
setStorageData(result)
}
setLoading(false)
}
fetchData()
const interval = setInterval(fetchData, 60000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Loading storage data...</div>
</div>
</div>
)
}
if (error || !storageData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0
return (
<div className="space-y-6">
{/* Storage Overview Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Storage</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.total)}</div>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">
{formatStorage(storageData.used)} used {formatStorage(storageData.available)} available
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Used Storage</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.used)}</div>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Archive className="h-5 w-5 mr-2" />
Available
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{formatStorage(storageData.available)}</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Available space</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Activity className="h-5 w-5 mr-2" />
Disks
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{storageData.disks.length}</div>
<div className="flex items-center space-x-2 mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{storageData.disks.filter((d) => d.health === "healthy").length} Healthy
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Storage devices</p>
</CardContent>
</Card>
</div>
{/* Disk Details */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Database className="h-5 w-5 mr-2" />
Storage Devices
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{storageData.disks.map((disk, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
>
<div className="flex items-center space-x-4">
<HardDrive className="h-5 w-5 text-muted-foreground" />
<div>
<div className="font-medium text-foreground">{disk.name}</div>
<div className="text-sm text-muted-foreground">
{disk.fstype} {disk.mountpoint}
</div>
</div>
</div>
<div className="flex items-center space-x-6">
<div className="text-right">
<div className="text-sm font-medium text-foreground">
{formatStorage(disk.used)} / {formatStorage(disk.total)}
</div>
<Progress value={disk.usage_percent} className="w-24 mt-1" />
</div>
<div className="text-center">
<div className="text-sm text-muted-foreground">Temp</div>
<div className="text-sm font-medium text-foreground">{disk.temperature}°C</div>
</div>
<Badge
variant="outline"
className={
disk.health === "healthy"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
>
{disk.health === "healthy" ? (
<CheckCircle className="h-3 w-3 mr-1" />
) : (
<AlertTriangle className="h-3 w-3 mr-1" />
)}
{disk.health}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+838
View File
@@ -0,0 +1,838 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network } from "lucide-react"
import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { TemperatureDetailModal } from "./temperature-detail-modal"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { fetchApi } from "../lib/api-config"
import { formatNetworkTraffic, getNetworkUnit } from "../lib/format-network"
import { formatStorage } from "../lib/utils"
import { Area, AreaChart, ResponsiveContainer } from "recharts"
interface TempDataPoint {
timestamp: number
value: number
}
interface SystemData {
cpu_usage: number
memory_usage: number
memory_total: number
memory_used: number
temperature: number
temperature_sparkline?: TempDataPoint[]
uptime: string
load_average: number[]
hostname: string
node_id: string
timestamp: string
cpu_cores?: number
cpu_threads?: number
proxmox_version?: string
kernel_version?: string
available_updates?: number
}
interface VMData {
vmid: number
name: string
status: string
cpu: number
mem: number
maxmem: number
disk: number
maxdisk: number
uptime: number
type?: string
}
interface StorageData {
total: number
used: number
available: number
disk_count: number
disks: Array<{
name: string
mountpoint: string
total: number
used: number
available: number
usage_percent: number
}>
}
interface NetworkData {
interfaces: Array<{
name: string
status: string
addresses: Array<{ ip: string; netmask: string }>
}>
traffic: {
bytes_sent: number
bytes_recv: number
packets_sent: number
packets_recv: number
}
physical_active_count?: number
physical_total_count?: number
bridge_active_count?: number
bridge_total_count?: number
physical_interfaces?: Array<{
name: string
status: string
addresses: Array<{ ip: string; netmask: string }>
}>
bridge_interfaces?: Array<{
name: string
status: string
addresses: Array<{ ip: string; netmask: string }>
}>
}
interface ProxmoxStorageData {
storage: Array<{
name: string
type: string
status: string
total: number
used: number
available: number
percent: number
}>
}
const fetchSystemData = async (retries = 3, delayMs = 500): Promise<SystemData | null> => {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const data = await fetchApi<SystemData>("/api/system")
return data
} catch {
if (attempt === retries - 1) {
// Silent fail - API not available (expected in preview environment)
return null
}
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
}
return null
}
const fetchVMData = async (): Promise<VMData[]> => {
try {
const data = await fetchApi<any>("/api/vms")
return Array.isArray(data) ? data : data.vms || []
} catch {
// Silent fail - API not available
return []
}
}
const fetchStorageData = async (): Promise<StorageData | null> => {
try {
const data = await fetchApi<StorageData>("/api/storage/summary")
return data
} catch {
return null
}
}
const fetchNetworkData = async (): Promise<NetworkData | null> => {
try {
const data = await fetchApi<NetworkData>("/api/network/summary")
return data
} catch {
return null
}
}
const fetchProxmoxStorageData = async (): Promise<ProxmoxStorage[] | null> => {
try {
const data = await fetchApi<ProxmoxStorage[]>("/api/proxmox-storage")
return data
} catch {
return null
}
}
const getUnitsSettings = (): "Bytes" | "Bits" => {
if (typeof window === "undefined") return "Bytes"
const raw = window.localStorage.getItem("proxmenux-network-unit")
return raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"
}
export function SystemOverview() {
const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([])
const [storageData, setStorageData] = useState<StorageData | null>(null)
const [proxmoxStorageData, setProxmoxStorageData] = useState<ProxmoxStorageData | null>(null)
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
const [loadingStates, setLoadingStates] = useState({
system: true,
vms: true,
storage: true,
network: true,
})
const [error, setError] = useState<string | null>(null)
const [hasAttemptedLoad, setHasAttemptedLoad] = useState(false) // Added hasAttemptedLoad state
const [networkTimeframe, setNetworkTimeframe] = useState("day")
const [networkTotals, setNetworkTotals] = useState<{ received: number; sent: number }>({ received: 0, sent: 0 })
const [networkUnit, setNetworkUnit] = useState<"Bytes" | "Bits">("Bytes") // Added networkUnit state
const [tempModalOpen, setTempModalOpen] = useState(false)
useEffect(() => {
const fetchAllData = async () => {
const [systemResult, vmResult, storageResults, networkResult] = await Promise.all([
fetchSystemData().finally(() => setLoadingStates((prev) => ({ ...prev, system: false }))),
fetchVMData().finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))),
Promise.all([fetchStorageData(), fetchProxmoxStorageData()]).finally(() =>
setLoadingStates((prev) => ({ ...prev, storage: false })),
),
fetchNetworkData().finally(() => setLoadingStates((prev) => ({ ...prev, network: false }))),
])
setHasAttemptedLoad(true)
if (!systemResult) {
setError("Flask server not available. Please ensure the server is running.")
return
}
setSystemData(systemResult)
setVmData(vmResult)
setStorageData(storageResults[0])
setProxmoxStorageData(storageResults[1])
setNetworkData(networkResult)
setTimeout(async () => {
const refreshedSystemData = await fetchSystemData()
if (refreshedSystemData) {
setSystemData(refreshedSystemData)
}
}, 2000)
}
fetchAllData()
const systemInterval = setInterval(async () => {
const data = await fetchSystemData()
if (data) setSystemData(data)
}, 5000)
const vmInterval = setInterval(async () => {
const data = await fetchVMData()
setVmData(data)
}, 59000)
const storageInterval = setInterval(async () => {
const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), fetchProxmoxStorageData()])
if (storage) setStorageData(storage)
if (proxmoxStorage) setProxmoxStorageData(proxmoxStorage)
}, 59000)
const networkInterval = setInterval(async () => {
const data = await fetchNetworkData()
if (data) setNetworkData(data)
}, 59000)
setNetworkUnit(getNetworkUnit()) // Load initial setting
const handleUnitChange = (e: CustomEvent) => {
setNetworkUnit(e.detail === "Bits" ? "Bits" : "Bytes")
}
window.addEventListener("networkUnitChanged" as any, handleUnitChange)
return () => {
clearInterval(systemInterval)
clearInterval(vmInterval)
clearInterval(storageInterval)
clearInterval(networkInterval)
window.removeEventListener("networkUnitChanged" as any, handleUnitChange)
}
}, [])
if (!hasAttemptedLoad || loadingStates.system) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
<div className="relative">
<div className="h-12 w-12 rounded-full border-2 border-muted"></div>
<div className="absolute inset-0 h-12 w-12 rounded-full border-2 border-transparent border-t-primary animate-spin"></div>
</div>
<div className="text-sm font-medium text-foreground">Loading system overview...</div>
<p className="text-xs text-muted-foreground">Fetching system status and metrics</p>
</div>
)
}
if (error || !systemData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const vmStats = {
total: vmData.length,
running: vmData.filter((vm) => vm.status === "running").length,
stopped: vmData.filter((vm) => vm.status === "stopped").length,
lxc: vmData.filter((vm) => vm.type === "lxc").length,
vms: vmData.filter((vm) => vm.type === "qemu" || !vm.type).length,
}
const getTemperatureStatus = (temp: number) => {
if (temp === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
if (temp < 60) return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
if (temp < 75) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
}
const formatUptime = (seconds: number) => {
if (!seconds || seconds === 0) return "Stopped"
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
const formatBytes = (bytes: number) => {
return (bytes / 1024 ** 3).toFixed(2)
}
const tempStatus = getTemperatureStatus(systemData.temperature)
const localStorage = proxmoxStorageData?.storage.find((s) => s.name === "local")
const vmLxcStorages = proxmoxStorageData?.storage.filter(
(s) =>
(s.type === "lvm" || s.type === "lvmthin" || s.type === "zfspool" || s.type === "btrfs" || s.type === "dir") &&
s.type !== "nfs" &&
s.type !== "cifs" &&
s.type !== "iscsi" &&
s.name !== "local",
)
const vmLxcStorageTotal = vmLxcStorages?.reduce((acc, s) => acc + s.total, 0) || 0
const vmLxcStorageUsed = vmLxcStorages?.reduce((acc, s) => acc + s.used, 0) || 0
const vmLxcStorageAvailable = vmLxcStorages?.reduce((acc, s) => acc + s.available, 0) || 0
const vmLxcStoragePercent = vmLxcStorageTotal > 0 ? (vmLxcStorageUsed / vmLxcStorageTotal) * 100 : 0
const getLoadStatus = (load: number, cores: number) => {
if (load < cores) {
return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
} else if (load < cores * 1.5) {
return { status: "Moderate", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
} else {
return { status: "High", color: "bg-red-500/10 text-red-500 border-red-500/20" }
}
}
const systemAlerts = []
if (systemData.available_updates && systemData.available_updates > 0) {
systemAlerts.push({
type: "warning",
message: `${systemData.available_updates} updates available`,
})
}
if (vmStats.stopped > 0) {
systemAlerts.push({
type: "info",
message: `${vmStats.stopped} VM${vmStats.stopped > 1 ? "s" : ""} stopped`,
})
}
if (systemData.temperature > 75) {
systemAlerts.push({
type: "warning",
message: "High temperature detected",
})
}
if (localStorage && localStorage.percent > 90) {
systemAlerts.push({
type: "warning",
message: "System storage almost full",
})
}
const loadStatus = getLoadStatus(systemData.load_average[0], systemData.cpu_cores || 8)
const getTimeframeLabel = (timeframe: string): string => {
switch (timeframe) {
case "hour":
return "1h"
case "day":
return "24h"
case "week":
return "7d"
case "month":
return "30d"
case "year":
return "1y"
default:
return timeframe
}
}
return (
<div className="space-y-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.cpu_usage}%</div>
<Progress value={systemData.cpu_usage} className="mt-2 [&>div]:bg-blue-500" />
<p className="text-xs text-muted-foreground mt-2">Real-time usage</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Memory Usage</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-foreground">{systemData.memory_used.toFixed(1)} GB</div>
<Progress value={systemData.memory_usage} className="mt-2 [&>div]:bg-blue-500" />
<p className="text-xs text-muted-foreground mt-2">
<span className="text-green-500 font-medium">{systemData.memory_usage.toFixed(1)}%</span> of{" "}
{systemData.memory_total} GB
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
Active VM & LXC
</CardTitle>
</CardHeader>
<CardContent>
{loadingStates.vms ? (
<div className="space-y-2 animate-pulse">
<div className="h-8 bg-muted rounded w-12"></div>
<div className="h-5 bg-muted rounded w-24"></div>
<div className="h-4 bg-muted rounded w-32"></div>
</div>
) : (
<>
<div className="text-xl lg:text-2xl font-bold text-foreground">{vmStats.running}</div>
<div className="mt-2 flex flex-wrap gap-1">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{vmStats.running} Running
</Badge>
{vmStats.stopped > 0 && (
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20">
{vmStats.stopped} Stopped
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
Total: {vmStats.vms} VMs, {vmStats.lxc} LXC
</p>
</>
)}
</CardContent>
</Card>
<Card
className={`bg-card border-border ${systemData.temperature > 0 ? "cursor-pointer hover:bg-white/5 transition-colors" : ""}`}
onClick={() => systemData.temperature > 0 && setTempModalOpen(true)}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Temperature</CardTitle>
<Thermometer className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<span className="text-xl lg:text-2xl font-bold text-foreground">
{systemData.temperature === 0 ? "N/A" : `${Math.round(systemData.temperature * 10) / 10}°C`}
</span>
<Badge variant="outline" className={tempStatus.color}>
{tempStatus.status}
</Badge>
</div>
{systemData.temperature > 0 && systemData.temperature_sparkline && systemData.temperature_sparkline.length > 1 ? (
<div className="mt-2 h-10">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={systemData.temperature_sparkline} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="tempSparkGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={systemData.temperature >= 75 ? "#ef4444" : systemData.temperature >= 60 ? "#f59e0b" : "#22c55e"} stopOpacity={0.3} />
<stop offset="100%" stopColor={systemData.temperature >= 75 ? "#ef4444" : systemData.temperature >= 60 ? "#f59e0b" : "#22c55e"} stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="value"
stroke={systemData.temperature >= 75 ? "#ef4444" : systemData.temperature >= 60 ? "#f59e0b" : "#22c55e"}
strokeWidth={1.5}
fill="url(#tempSparkGradient)"
dot={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
) : (
<p className="text-xs text-muted-foreground mt-2">
{systemData.temperature === 0 ? "No sensor available" : "Collecting data..."}
</p>
)}
</CardContent>
</Card>
</div>
<TemperatureDetailModal
open={tempModalOpen}
onOpenChange={setTempModalOpen}
liveTemperature={systemData.temperature}
/>
<NodeMetricsCharts />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<HardDrive className="h-5 w-5 mr-2" />
Storage Overview
</CardTitle>
</CardHeader>
<CardContent>
{loadingStates.storage ? (
<div className="space-y-4 animate-pulse">
<div className="h-6 bg-muted rounded w-full"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-2/3"></div>
</div>
) : storageData ? (
<div className="space-y-4">
{(() => {
const totalCapacity = (vmLxcStorageTotal || 0) + (localStorage?.total || 0)
const totalUsed = (vmLxcStorageUsed || 0) + (localStorage?.used || 0)
const totalAvailable = (vmLxcStorageAvailable || 0) + (localStorage?.available || 0)
const totalPercent = totalCapacity > 0 ? (totalUsed / totalCapacity) * 100 : 0
return totalCapacity > 0 ? (
<div className="space-y-2 pb-4 border-b-2 border-border">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">Total Node Capacity:</span>
<span className="text-lg font-bold text-foreground">
{formatStorage(totalCapacity)}
</span>
</div>
<Progress
value={totalPercent}
className="mt-2 h-3 [&>div]:bg-gradient-to-r [&>div]:from-blue-500 [&>div]:to-purple-500"
/>
<div className="flex justify-between items-center mt-1">
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">
Used:{" "}
<span className="font-semibold text-foreground">
{formatStorage(totalUsed)}
</span>
</span>
<span className="text-xs text-muted-foreground">
Free:{" "}
<span className="font-semibold text-green-500">
{formatStorage(totalAvailable)}
</span>
</span>
</div>
<span className="text-xs font-semibold text-muted-foreground">{totalPercent.toFixed(1)}%</span>
</div>
</div>
) : null
})()}
<div className="space-y-2 pb-3 border-b border-border">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Capacity:</span>
<span className="text-lg font-semibold text-foreground">{storageData.total} TB</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Physical Disks:</span>
<span className="text-sm font-semibold text-foreground">
{storageData.disk_count} disk{storageData.disk_count !== 1 ? "s" : ""}
</span>
</div>
</div>
{vmLxcStorages && vmLxcStorages.length > 0 ? (
<div className="space-y-2 pb-3 border-b border-border">
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Used:</span>
<span className="text-sm font-semibold text-foreground">
{formatStorage(vmLxcStorageUsed)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Available:</span>
<span className="text-sm font-semibold text-green-500">
{formatStorage(vmLxcStorageAvailable)}
</span>
</div>
<Progress value={vmLxcStoragePercent} className="mt-2 [&>div]:bg-blue-500" />
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground">
{formatStorage(vmLxcStorageUsed)} /{" "}
{formatStorage(vmLxcStorageTotal)}
</span>
<span className="text-xs text-muted-foreground">{vmLxcStoragePercent.toFixed(1)}%</span>
</div>
{vmLxcStorages.length > 1 && (
<div className="text-xs text-muted-foreground mt-1">
{vmLxcStorages.length} storage volume{vmLxcStorages.length > 1 ? "s" : ""}
</div>
)}
</div>
) : (
<div className="space-y-2 pb-3 border-b border-border">
<div className="text-xs font-medium text-muted-foreground mb-2">VM/LXC Storage</div>
<div className="text-center py-4 text-muted-foreground text-sm">No VM/LXC storage configured</div>
</div>
)}
{localStorage && (
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground mb-2">Local Storage (System)</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Used:</span>
<span className="text-sm font-semibold text-foreground">
{formatStorage(localStorage.used)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground">Available:</span>
<span className="text-sm font-semibold text-green-500">
{formatStorage(localStorage.available)}
</span>
</div>
<Progress value={localStorage.percent} className="mt-2 [&>div]:bg-purple-500" />
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-muted-foreground">
{formatStorage(localStorage.used)} /{" "}
{formatStorage(localStorage.total)}
</span>
<span className="text-xs text-muted-foreground">{localStorage.percent.toFixed(1)}%</span>
</div>
</div>
)}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">Storage data not available</div>
)}
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center justify-between">
<div className="flex items-center">
<Network className="h-5 w-5 mr-2" />
Network Overview
</div>
<Select value={networkTimeframe} onValueChange={setNetworkTimeframe}>
<SelectTrigger className="w-28 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hour">1 Hour</SelectItem>
<SelectItem value="day">24 Hours</SelectItem>
<SelectItem value="week">7 Days</SelectItem>
<SelectItem value="month">30 Days</SelectItem>
<SelectItem value="year">1 Year</SelectItem>
</SelectContent>
</Select>
</CardTitle>
</CardHeader>
<CardContent>
{loadingStates.network ? (
<div className="space-y-4 animate-pulse">
<div className="h-6 bg-muted rounded w-full"></div>
<div className="h-4 bg-muted rounded w-3/4"></div>
<div className="h-4 bg-muted rounded w-2/3"></div>
</div>
) : networkData ? (
<div className="space-y-4">
<div className="flex justify-between items-center pb-3 border-b border-border">
<span className="text-sm text-muted-foreground">Active Interfaces:</span>
<span className="text-lg font-semibold text-foreground">
{(networkData.physical_active_count || 0) + (networkData.bridge_active_count || 0)}
</span>
</div>
<div className="space-y-2">
{networkData.physical_interfaces && networkData.physical_interfaces.length > 0 && (
<div className="flex flex-wrap gap-2">
{networkData.physical_interfaces
.filter((iface) => iface.status === "up")
.map((iface) => (
<Badge
key={iface.name}
variant="outline"
className="bg-blue-500/10 text-blue-500 border-blue-500/20"
>
{iface.name}
</Badge>
))}
</div>
)}
{networkData.bridge_interfaces && networkData.bridge_interfaces.length > 0 && (
<div className="flex flex-wrap gap-2">
{networkData.bridge_interfaces
.filter((iface) => iface.status === "up")
.map((iface) => (
<Badge
key={iface.name}
variant="outline"
className="bg-green-500/10 text-green-500 border-green-500/20"
>
{iface.name}
</Badge>
))}
</div>
)}
</div>
<div className="pt-2 border-t border-border space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Received:</span>
<span className="text-lg font-semibold text-green-500 flex items-center gap-1">
{" "}
{networkUnit === "Bytes"
? `${networkTotals.received.toFixed(2)} GB`
: formatNetworkTraffic(networkTotals.received * 1024 * 1024 * 1024, "Bits")}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Sent:</span>
<span className="text-lg font-semibold text-blue-500 flex items-center gap-1">
{" "}
{networkUnit === "Bytes"
? `${networkTotals.sent.toFixed(2)} GB`
: formatNetworkTraffic(networkTotals.sent * 1024 * 1024 * 1024, "Bits")}
<span className="text-xs text-muted-foreground">({getTimeframeLabel(networkTimeframe)})</span>
</span>
</div>
</div>
<div className="pt-3 border-t border-border">
<NetworkTrafficChart
timeframe={networkTimeframe}
onTotalsCalculated={setNetworkTotals}
networkUnit={networkUnit}
/>
</div>
</div>
) : (
<div className="text-center py-8 text-muted-foreground">Network data not available</div>
)}
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
System Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Uptime:</span>
<span className="text-foreground">{systemData.uptime}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Proxmox Version:</span>
<span className="text-foreground">{systemData.proxmox_version || "N/A"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Kernel:</span>
<span className="text-foreground font-mono text-sm">{systemData.kernel_version || "Linux"}</span>
</div>
{systemData.available_updates !== undefined && systemData.available_updates > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">Available Updates:</span>
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
{systemData.available_updates} packages
</Badge>
</div>
)}
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Zap className="h-5 w-5 mr-2" />
System Overview
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center pb-3 border-b border-border">
<div className="flex flex-col">
<span className="text-sm text-muted-foreground">Load Average (1m):</span>
</div>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold text-foreground font-mono">
{systemData.load_average[0].toFixed(2)}
</span>
<Badge variant="outline" className={loadStatus.color}>
{loadStatus.status}
</Badge>
</div>
</div>
<div className="flex justify-between items-center pb-3 border-b border-border">
<span className="text-sm text-muted-foreground">CPU Threads:</span>
<span className="text-lg font-semibold text-foreground">{systemData.cpu_threads || "N/A"}</span>
</div>
<div className="flex justify-between items-center pb-3 border-b border-border">
<span className="text-sm text-muted-foreground">Physical Disks:</span>
<span className="text-lg font-semibold text-foreground">{storageData?.disk_count || "N/A"}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Network Interfaces:</span>
<span className="text-lg font-semibold text-foreground">
{networkData?.physical_total_count || networkData?.physical_interfaces?.length || "N/A"}
</span>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
@@ -0,0 +1,242 @@
"use client"
import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Badge } from "./ui/badge"
import { Thermometer, TrendingDown, TrendingUp, Minus } from "lucide-react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
import { useIsMobile } from "../hooks/use-mobile"
import { fetchApi } from "@/lib/api-config"
const TIMEFRAME_OPTIONS = [
{ value: "hour", label: "1 Hour" },
{ value: "day", label: "24 Hours" },
{ value: "week", label: "7 Days" },
{ value: "month", label: "30 Days" },
]
interface TempHistoryPoint {
timestamp: number
value: number
min?: number
max?: number
}
interface TempStats {
min: number
max: number
avg: number
current: number
}
interface TemperatureDetailModalProps {
open: boolean
onOpenChange: (open: boolean) => void
liveTemperature?: number
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-gray-900/95 backdrop-blur-sm border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-sm font-semibold text-white mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: entry.color }} />
<span className="text-xs text-gray-300 min-w-[60px]">{entry.name}:</span>
<span className="text-sm font-semibold text-white">{entry.value}°C</span>
</div>
))}
</div>
</div>
)
}
return null
}
const getStatusColor = (temp: number) => {
if (temp >= 75) return "#ef4444"
if (temp >= 60) return "#f59e0b"
return "#22c55e"
}
const getStatusInfo = (temp: number) => {
if (temp === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
if (temp < 60) return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
if (temp < 75) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
}
export function TemperatureDetailModal({ open, onOpenChange, liveTemperature }: TemperatureDetailModalProps) {
const [timeframe, setTimeframe] = useState("hour")
const [data, setData] = useState<TempHistoryPoint[]>([])
const [stats, setStats] = useState<TempStats>({ min: 0, max: 0, avg: 0, current: 0 })
const [loading, setLoading] = useState(true)
const isMobile = useIsMobile()
useEffect(() => {
if (open) {
fetchHistory()
}
}, [open, timeframe])
const fetchHistory = async () => {
setLoading(true)
try {
const result = await fetchApi<{ data: TempHistoryPoint[]; stats: TempStats }>(
`/api/temperature/history?timeframe=${timeframe}`
)
if (result && result.data) {
setData(result.data)
setStats(result.stats)
}
} catch (err) {
console.error("[v0] Failed to fetch temperature history:", err)
} finally {
setLoading(false)
}
}
const formatTime = (timestamp: number) => {
const date = new Date(timestamp * 1000)
if (timeframe === "hour") {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
} else if (timeframe === "day") {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
} else {
return date.toLocaleDateString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
}
}
const chartData = data.map((d) => ({
...d,
time: formatTime(d.timestamp),
}))
// Use live temperature from the overview card (real-time) instead of last DB record
const currentTemp = liveTemperature && liveTemperature > 0 ? Math.round(liveTemperature * 10) / 10 : stats.current
const currentStatus = getStatusInfo(currentTemp)
const chartColor = getStatusColor(currentTemp)
// Calculate Y axis domain based on plotted data values only.
// Stats cards already show the real historical min/max separately.
// Using only graphed values keeps the chart readable and avoids
// large empty gaps caused by momentary spikes that get averaged out.
const values = data.map((d) => d.value)
const yMin = values.length > 0 ? Math.max(0, Math.floor(Math.min(...values) - 3)) : 0
const yMax = values.length > 0 ? Math.ceil(Math.max(...values) + 3) : 100
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl bg-card border-border px-3 sm:px-6">
<DialogHeader>
<div className="flex items-center justify-between pr-6">
<DialogTitle className="text-foreground flex items-center gap-2">
<Thermometer className="h-5 w-5" />
CPU Temperature
</DialogTitle>
<Select value={timeframe} onValueChange={setTimeframe}>
<SelectTrigger className="w-[130px] bg-card border-border">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMEFRAME_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</DialogHeader>
{/* Stats bar */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3">
<div className={`rounded-lg p-3 text-center ${currentStatus.color}`}>
<div className="text-xs opacity-80 mb-1">Current</div>
<div className="text-lg font-bold">{currentTemp}°C</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground mb-1 flex items-center justify-center gap-1">
<TrendingDown className="h-3 w-3" /> Min
</div>
<div className="text-lg font-bold text-green-500">{stats.min}°C</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground mb-1 flex items-center justify-center gap-1">
<Minus className="h-3 w-3" /> Avg
</div>
<div className="text-lg font-bold text-foreground">{stats.avg}°C</div>
</div>
<div className="bg-muted/50 rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground mb-1 flex items-center justify-center gap-1">
<TrendingUp className="h-3 w-3" /> Max
</div>
<div className="text-lg font-bold text-red-500">{stats.max}°C</div>
</div>
</div>
{/* Chart */}
<div className="h-[300px] lg:h-[350px]">
{loading ? (
<div className="h-full flex items-center justify-center">
<div className="space-y-3 w-full animate-pulse">
<div className="h-4 bg-muted rounded w-1/4 mx-auto" />
<div className="h-[250px] bg-muted/50 rounded" />
</div>
</div>
) : chartData.length === 0 ? (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Thermometer className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No temperature data available for this period</p>
<p className="text-sm mt-1">Data is collected every 60 seconds</p>
</div>
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="tempGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={chartColor} stopOpacity={0.3} />
<stop offset="100%" stopColor={chartColor} stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="currentColor" className="text-border" />
<XAxis
dataKey="time"
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: isMobile ? 10 : 12 }}
interval="preserveStartEnd"
minTickGap={isMobile ? 40 : 60}
/>
<YAxis
domain={[yMin, yMax]}
stroke="currentColor"
className="text-foreground"
tick={{ fill: "currentColor", fontSize: isMobile ? 10 : 12 }}
tickFormatter={(v) => `${v}°`}
width={isMobile ? 40 : 45}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="value"
name="Temperature"
stroke={chartColor}
strokeWidth={2}
fill="url(#tempGradient)"
dot={false}
activeDot={{ r: 4, fill: chartColor, stroke: "#fff", strokeWidth: 2 }}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
</DialogContent>
</Dialog>
)
}
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
+37
View File
@@ -0,0 +1,37 @@
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { useEffect, useState } from "react"
import { Button } from "./ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const handleThemeToggle = () => {
const newTheme = theme === "light" ? "dark" : "light"
setTheme(newTheme)
}
if (!mounted) {
return (
<Button variant="outline" size="sm" className="border-border bg-transparent w-9 h-9">
<Sun className="h-4 w-4" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
return (
<Button variant="outline" size="sm" onClick={handleThemeToggle} className="border-border bg-transparent w-9 h-9">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
+297
View File
@@ -0,0 +1,297 @@
"use client"
import { useState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { AlertCircle, CheckCircle, Copy, Shield, Check } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
interface TwoFactorSetupProps {
open: boolean
onClose: () => void
onSuccess: () => void
}
export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps) {
const [step, setStep] = useState(1)
const [qrCode, setQrCode] = useState("")
const [secret, setSecret] = useState("")
const [backupCodes, setBackupCodes] = useState<string[]>([])
const [verificationCode, setVerificationCode] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const [copiedSecret, setCopiedSecret] = useState(false)
const [copiedCodes, setCopiedCodes] = useState(false)
const handleSetupStart = async () => {
setError("")
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/setup"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Failed to setup 2FA")
}
setQrCode(data.qr_code)
setSecret(data.secret)
setBackupCodes(data.backup_codes)
setStep(2)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to setup 2FA")
} finally {
setLoading(false)
}
}
const handleVerify = async () => {
if (!verificationCode || verificationCode.length !== 6) {
setError("Please enter a 6-digit code")
return
}
setError("")
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/enable"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ token: verificationCode }),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Invalid verification code")
}
setStep(3)
} catch (err) {
setError(err instanceof Error ? err.message : "Verification failed")
} finally {
setLoading(false)
}
}
const copyToClipboard = async (text: string, type: "secret" | "codes") => {
let ok = false
// Preferred path (HTTPS / localhost). On plain HTTP the Promise rejects,
// so we catch and fall through to the textarea fallback.
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
ok = true
}
} catch {
// fall through to execCommand fallback
}
if (!ok) {
try {
const textarea = document.createElement("textarea")
textarea.value = text
textarea.style.position = "fixed"
textarea.style.left = "-9999px"
textarea.style.top = "-9999px"
textarea.style.opacity = "0"
textarea.readOnly = true
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
ok = document.execCommand("copy")
document.body.removeChild(textarea)
} catch {
ok = false
}
}
if (!ok) {
console.error("Failed to copy to clipboard")
return
}
if (type === "secret") {
setCopiedSecret(true)
setTimeout(() => setCopiedSecret(false), 2000)
} else {
setCopiedCodes(true)
setTimeout(() => setCopiedCodes(false), 2000)
}
}
const handleClose = () => {
setStep(1)
setQrCode("")
setSecret("")
setBackupCodes([])
setVerificationCode("")
setError("")
onClose()
}
const handleFinish = () => {
handleClose()
onSuccess()
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
Setup Two-Factor Authentication
</DialogTitle>
<DialogDescription>Add an extra layer of security to your account</DialogDescription>
</DialogHeader>
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{step === 1 && (
<div className="space-y-4">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
<p className="text-sm text-blue-500">
Two-factor authentication (2FA) adds an extra layer of security by requiring a code from your
authentication app in addition to your password.
</p>
</div>
<div className="space-y-2">
<h4 className="font-medium">You will need:</h4>
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
<li>An authentication app (Google Authenticator, Authy, etc.)</li>
<li>Scan a QR code or enter a key manually</li>
<li>Store backup codes securely</li>
</ul>
</div>
<Button onClick={handleSetupStart} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Starting..." : "Start Setup"}
</Button>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium">1. Scan the QR code</h4>
<p className="text-sm text-muted-foreground">Open your authentication app and scan this QR code</p>
{qrCode && (
<div className="flex justify-center p-4 bg-white rounded-lg">
<img src={qrCode || "/placeholder.svg"} alt="QR Code" width={200} height={200} className="rounded" />
</div>
)}
</div>
<div className="space-y-2">
<h4 className="font-medium">Or enter the key manually:</h4>
<div className="flex gap-2">
<Input value={secret} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(secret, "secret")}
title="Copy key"
>
{copiedSecret ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">2. Enter the verification code</h4>
<p className="text-sm text-muted-foreground">Enter the 6-digit code that appears in your app</p>
<Input
type="text"
placeholder="000000"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
className="text-center text-lg tracking-widest font-mono text-base"
maxLength={6}
disabled={loading}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleVerify} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}>
{loading ? "Verifying..." : "Verify and Enable"}
</Button>
<Button onClick={handleClose} variant="outline" className="flex-1 bg-transparent" disabled={loading}>
Cancel
</Button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-4">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 flex items-start gap-2">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-green-500">2FA Enabled Successfully</p>
<p className="text-sm text-green-500 mt-1">
Your account is now protected with two-factor authentication
</p>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium text-orange-500">Important: Save your backup codes</h4>
<p className="text-sm text-muted-foreground">
These codes will allow you to access your account if you lose access to your authentication app. Store
them in a safe place.
</p>
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium">Backup Codes</span>
<Button variant="outline" size="sm" onClick={() => copyToClipboard(backupCodes.join("\n"), "codes")}>
{copiedCodes ? (
<Check className="h-4 w-4 text-green-500 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
Copy All
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<div key={index} className="bg-background rounded px-3 py-2 font-mono text-sm text-center">
{code}
</div>
))}
</div>
</div>
</div>
<Button onClick={handleFinish} className="w-full bg-blue-500 hover:bg-blue-600">
Finish
</Button>
</div>
)}
</DialogContent>
</Dialog>
)
}
+28
View File
@@ -0,0 +1,28 @@
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
Button.displayName = "Button"
export { Button, buttonVariants }
+42
View File
@@ -0,0 +1,42 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
)
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
)
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
)
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
)
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+27
View File
@@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+102
View File
@@ -0,0 +1,102 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
hideClose?: boolean
}
>(({ className, children, hideClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
className,
)}
aria-describedby={props["aria-describedby"] || undefined}
{...props}
>
{children}
{!hideClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
+257
View File
@@ -0,0 +1,257 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-lg border border-input bg-background px-4 py-2 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:border-ring/50",
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../lib/utils"
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+25
View File
@@ -0,0 +1,25 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
+40
View File
@@ -0,0 +1,40 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
+144
View File
@@ -0,0 +1,144 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
+109
View File
@@ -0,0 +1,109 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = DialogPrimitive.Root
const SheetTrigger = DialogPrimitive.Trigger
const SheetClose = DialogPrimitive.Close
const SheetPortal = DialogPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</SheetPortal>
),
)
SheetContent.displayName = DialogPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
))
SheetTitle.displayName = DialogPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
SheetDescription.displayName = DialogPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
+29
View File
@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
{
"_description": "Verified AI models for ProxMenux notifications. Only models listed here will be shown to users. Models are tested to work with the chat/completions API format.",
"_updated": "2026-04-19",
"_verifier": "Refreshed with tools/ai-models-verifier (private). Re-run before each ProxMenux release to keep the list current. The verifier and ProxMenux share the same reasoning/thinking-model handlers so their verdicts stay aligned with runtime behaviour.",
"groq": {
"models": [
"llama-3.3-70b-versatile",
"llama-3.1-70b-versatile",
"llama-3.1-8b-instant",
"llama3-70b-8192",
"llama3-8b-8192",
"mixtral-8x7b-32768",
"gemma2-9b-it"
],
"recommended": "llama-3.3-70b-versatile",
"_note": "Not yet re-verified in 2026-04 refresh — kept from previous curation. Run the verifier with a Groq key to prune deprecated entries."
},
"gemini": {
"models": [
"gemini-2.5-flash-lite",
"gemini-2.5-flash",
"gemini-3-flash-preview"
],
"recommended": "gemini-2.5-flash-lite",
"_note": "flash-lite / flash pass the verifier consistently; pro variants reject thinkingBudget=0 and are overkill for notification translation anyway. 'latest' aliases (gemini-flash-latest, gemini-flash-lite-latest) are intentionally omitted because they resolved to different models across runs and produced timeouts in some regions.",
"_deprecated": ["gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-1.5-flash", "gemini-1.0-pro", "gemini-pro"]
},
"openai": {
"models": [
"gpt-4.1-nano",
"gpt-4.1-mini",
"gpt-4o-mini",
"gpt-4.1",
"gpt-4o",
"gpt-5-chat-latest",
"gpt-5.4-nano",
"gpt-5.4-mini"
],
"recommended": "gpt-4.1-nano",
"_note": "Reasoning models (o-series, gpt-5/5.1/5.2 non-chat variants) are supported by openai_provider.py via max_completion_tokens + reasoning_effort=minimal, but not listed here by default: their latency is higher than the chat models and they do not improve translation quality for notifications. Add specific reasoning IDs to this list only if a user explicitly wants them."
},
"anthropic": {
"models": [
"claude-3-5-haiku-latest",
"claude-3-5-sonnet-latest",
"claude-3-opus-latest"
],
"recommended": "claude-3-5-haiku-latest",
"_note": "Not re-verified in 2026-04 refresh — kept from previous curation. Add claude-4.x / claude-4.5 / claude-4.6 / claude-4.7 variants after running the verifier with an Anthropic key."
},
"openrouter": {
"models": [
"meta-llama/llama-3.3-70b-instruct",
"meta-llama/llama-3.1-70b-instruct",
"meta-llama/llama-3.1-8b-instruct",
"anthropic/claude-3.5-haiku",
"anthropic/claude-3.5-sonnet",
"google/gemini-flash-1.5",
"openai/gpt-4o-mini",
"mistralai/mistral-7b-instruct",
"mistralai/mixtral-8x7b-instruct"
],
"recommended": "meta-llama/llama-3.3-70b-instruct",
"_note": "Not re-verified in 2026-04 refresh. google/gemini-flash-2.5-flash-lite was malformed in the previous entry and has been replaced with google/gemini-flash-1.5."
},
"ollama": {
"_note": "Ollama models are local, we don't filter them. User manages their own models.",
"models": [],
"recommended": ""
}
}
+23
View File
@@ -0,0 +1,23 @@
"use client"
import { useEffect, useState } from "react"
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
}
// Check on mount
checkMobile()
// Listen for resize
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
return isMobile
}
+174
View File
@@ -0,0 +1,174 @@
/**
* API Configuration for ProxMenux Monitor
* Handles API URL generation with automatic proxy detection
*/
/**
* API Server Port Configuration
* Default: 8008 (production)
* Can be changed to 8009 for beta testing
* This can also be set via NEXT_PUBLIC_API_PORT environment variable
*/
export const API_PORT = process.env.NEXT_PUBLIC_API_PORT || "8008"
/**
* Gets the base URL for API calls
* Automatically detects if running behind a proxy by checking if we're on a standard port
*
* @returns Base URL for API endpoints
*/
export function getApiBaseUrl(): string {
if (typeof window === "undefined") {
return ""
}
const { protocol, hostname, port } = window.location
// If accessing via standard ports (80/443) or no port, assume we're behind a proxy
// In this case, use relative URLs so the proxy handles routing
const isStandardPort = port === "" || port === "80" || port === "443"
if (isStandardPort) {
return ""
} else {
return `${protocol}//${hostname}:${API_PORT}`
}
}
/**
* Constructs a full API URL
*
* @param endpoint - API endpoint path (e.g., '/api/system')
* @returns Full API URL
*/
export function getApiUrl(endpoint: string): string {
const baseUrl = getApiBaseUrl()
// Ensure endpoint starts with /
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
return `${baseUrl}${normalizedEndpoint}`
}
/**
* Gets the JWT token from localStorage
*
* @returns JWT token or null if not authenticated
*/
export function getAuthToken(): string | null {
if (typeof window === "undefined") {
return null
}
return localStorage.getItem("proxmenux-auth-token")
}
/**
* Fetches data from an API endpoint with error handling
*
* @param endpoint - API endpoint path
* @param options - Fetch options
* @returns Promise with the response data
*/
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = getApiUrl(endpoint)
const token = getAuthToken()
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options?.headers as Record<string, string>),
}
if (token) {
headers["Authorization"] = `Bearer ${token}`
}
const response = await fetch(url, {
...options,
headers,
cache: "no-store",
})
if (!response.ok) {
if (response.status === 401) {
// Token is missing, expired, or signed under a previous JWT_SECRET
// (rotated per-install). Drop the stale token and force a single
// reload so the page-level auth gate (`app/page.tsx`) can render
// <Login> instead of cascading 401s from every authenticated
// component on mount.
//
// Only react when we actually had a token to invalidate. A 401
// without any token in localStorage means the caller is the
// Login screen itself, or a leftover fetch from a recently
// unmounted Dashboard — reloading there does nothing but waste
// the user's keystrokes and can leave the cascade flag set
// forever, swallowing the very 401 that we'd want to recover
// from after a successful re-login. The fix: bail out early
// if we have no token to invalidate.
if (typeof window !== "undefined") {
let hadToken = false
try {
hadToken = !!localStorage.getItem("proxmenux-auth-token")
} catch {
// private browsing — assume yes so we attempt recovery.
hadToken = true
}
if (!hadToken) {
throw new Error(`Unauthorized: ${endpoint}`)
}
try {
localStorage.removeItem("proxmenux-auth-token")
} catch {
// localStorage might be unavailable in private browsing — ignore.
}
try {
if (!sessionStorage.getItem("proxmenux-auth-401-handled")) {
sessionStorage.setItem("proxmenux-auth-401-handled", "1")
window.location.reload()
}
} catch {
// sessionStorage unavailable — fall back to a plain reload.
window.location.reload()
}
}
throw new Error(`Unauthorized: ${endpoint}`)
}
// Try to surface the backend's JSON error payload instead of a
// bare `500 INTERNAL SERVER ERROR`. The Flask routes consistently
// return `{error: "..."}` on failure (e.g. /api/vms/<id>/control
// includes the pvesh stderr — telling the user "no space left on
// device" is infinitely more useful than the raw status text).
try {
const ct = response.headers.get("content-type") || ""
if (ct.includes("application/json")) {
const body = await response.json()
const detail =
(body && (body.error || body.message)) || ""
if (detail) {
throw new Error(detail)
}
}
} catch (parseErr) {
if (parseErr instanceof Error && parseErr.message.includes("API request failed")) {
throw parseErr
}
// JSON parse failed — fall through to the generic message.
}
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}
// Check content type to ensure we're getting JSON
const contentType = response.headers.get("content-type")
if (!contentType || !contentType.includes("application/json")) {
const text = await response.text()
console.error("[v0] fetchApi: Expected JSON but got:", contentType, "- Body preview:", text.substring(0, 200))
throw new Error(`Expected JSON response but got ${contentType || "unknown content type"}`)
}
try {
return await response.json()
} catch (jsonError) {
console.error("[v0] fetchApi: JSON parse error for", endpoint, "-", jsonError)
throw new Error(`Invalid JSON response from ${endpoint}`)
}
}
+68
View File
@@ -0,0 +1,68 @@
/**
* Utility functions for formatting network traffic data
* Supports conversion between Bytes and Bits based on user preferences
*/
export type NetworkUnit = 'Bytes' | 'Bits';
/**
* Format network traffic value with appropriate unit
* @param bytes - Value in bytes
* @param unit - Target unit ('Bytes' or 'Bits')
* @param decimals - Number of decimal places (default: 2)
* @returns Formatted string with value and unit
*/
export function formatNetworkTraffic(
bytes: number,
unit: NetworkUnit = 'Bytes',
decimals: number = 2
): string {
if (bytes === 0) return unit === 'Bits' ? '0 b' : '0 B';
const k = unit === 'Bits' ? 1000 : 1024;
const dm = decimals < 0 ? 0 : Math.min(decimals, 2);
// For Bits: convert bytes to bits first (multiply by 8)
const value = unit === 'Bits' ? bytes * 8 : bytes;
const sizes = unit === 'Bits'
? ['b', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb']
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(value) / Math.log(k));
const finalDecimals = 2; // Always use 2 decimals for consistency
const formattedValue = parseFloat((value / Math.pow(k, i)).toFixed(finalDecimals));
return `${formattedValue} ${sizes[i]}`;
}
/**
* Get the current network unit preference from localStorage
* @returns 'Bytes' or 'Bits'
*/
export function getNetworkUnit(): NetworkUnit {
if (typeof window === 'undefined') return 'Bytes';
const stored = localStorage.getItem('proxmenux-network-unit');
return stored === 'Bits' ? 'Bits' : 'Bytes';
}
/**
* Get the label for network traffic based on current unit
* @param direction - 'received' or 'sent'
* @returns Label string
*/
export function getNetworkLabel(direction: 'received' | 'sent'): string {
const unit = getNetworkUnit();
const prefix = direction === 'received' ? 'Received' : 'Sent';
return unit === 'Bits' ? `${prefix}` : `${prefix}`;
}
/**
* Get the unit suffix for displaying in charts
* @returns Unit suffix string (e.g., 'GB' or 'Gb')
*/
export function getNetworkUnitSuffix(): string {
const unit = getNetworkUnit();
return unit === 'Bits' ? 'b' : 'B';
}
+147
View File
@@ -0,0 +1,147 @@
// Shared accessor for the user-configurable health thresholds.
//
// The backend exposes the full tree at `GET /api/health/thresholds`.
// Several frontend components need *just* the disk-temperature pair
// per drive class to color badges, chart bands, and SVG bands in the
// SMART report — copy-pasting the numbers around led to two
// inconsistent versions diverging from the backend (see Sprint 14.5).
//
// This module memoises the last fetched payload (TTL 30s) and exposes:
//
// * `getDiskTempThresholdsSync(diskType)` — synchronous read with a
// conservative fallback to the backend defaults. Safe to call from
// anywhere, including a render path that can't await.
// * `loadDiskTempThresholds()` — async fetch + cache update. Returns
// the cached map; call once on mount of any component that uses
// the sync getter to ensure the cache is warm.
// * `useDiskTempThresholds()` — React hook that fires the fetch on
// mount, re-renders when fresh data arrives, and returns the
// current map (defaults until the first fetch lands).
//
// The cache is shared across components so opening multiple disk
// modals in quick succession doesn't re-hit the API for each.
import { useEffect, useState } from "react"
import { fetchApi } from "./api-config"
export type DiskClass = "HDD" | "SSD" | "NVMe" | "SAS"
export interface DiskTempThreshold {
warn: number
hot: number
}
export type DiskTempMap = Record<DiskClass, DiskTempThreshold>
// Fallback values when the API hasn't responded yet (or fails). These
// match the recommended defaults baked into `health_thresholds.py`.
// Keeping them duplicated here is intentional: the alternative is
// blocking every render until the API comes back, which is worse UX.
export const DEFAULT_DISK_TEMP: DiskTempMap = {
HDD: { warn: 60, hot: 65 },
SSD: { warn: 70, hot: 75 },
NVMe: { warn: 80, hot: 85 },
SAS: { warn: 55, hot: 65 },
}
const CACHE_TTL_MS = 30_000
// Module-level cache — shared by every component that imports this.
let cached: DiskTempMap = DEFAULT_DISK_TEMP
let cachedAt = 0
let inflight: Promise<DiskTempMap> | null = null
// Subscribers are notified when a fresh fetch lands, so the
// `useDiskTempThresholds` hook can re-render. Plain JS pub/sub —
// nothing fancier needed here.
const subscribers = new Set<(map: DiskTempMap) => void>()
interface ApiThresholdsResponse {
success: boolean
thresholds?: {
disk_temperature?: {
hdd?: { warning?: { value: number }; critical?: { value: number } }
ssd?: { warning?: { value: number }; critical?: { value: number } }
nvme?: { warning?: { value: number }; critical?: { value: number } }
sas?: { warning?: { value: number }; critical?: { value: number } }
}
}
}
function pick(node: any, key: string, fallback: number): number {
const v = node?.[key]?.value
return typeof v === "number" && isFinite(v) ? v : fallback
}
function parse(payload: ApiThresholdsResponse): DiskTempMap {
const dt = payload?.thresholds?.disk_temperature
if (!dt) return { ...DEFAULT_DISK_TEMP }
return {
HDD: {
warn: pick(dt.hdd, "warning", DEFAULT_DISK_TEMP.HDD.warn),
hot: pick(dt.hdd, "critical", DEFAULT_DISK_TEMP.HDD.hot),
},
SSD: {
warn: pick(dt.ssd, "warning", DEFAULT_DISK_TEMP.SSD.warn),
hot: pick(dt.ssd, "critical", DEFAULT_DISK_TEMP.SSD.hot),
},
NVMe: {
warn: pick(dt.nvme, "warning", DEFAULT_DISK_TEMP.NVMe.warn),
hot: pick(dt.nvme, "critical", DEFAULT_DISK_TEMP.NVMe.hot),
},
SAS: {
warn: pick(dt.sas, "warning", DEFAULT_DISK_TEMP.SAS.warn),
hot: pick(dt.sas, "critical", DEFAULT_DISK_TEMP.SAS.hot),
},
}
}
export async function loadDiskTempThresholds(force = false): Promise<DiskTempMap> {
const now = Date.now()
if (!force && cachedAt && now - cachedAt < CACHE_TTL_MS) return cached
if (inflight) return inflight
inflight = (async () => {
try {
const res = await fetchApi<ApiThresholdsResponse>("/api/health/thresholds")
if (res?.success) {
cached = parse(res)
cachedAt = Date.now()
subscribers.forEach((cb) => cb(cached))
}
} catch {
// Leave previous cache in place; defaults are good enough.
} finally {
inflight = null
}
return cached
})()
return inflight
}
export function getDiskTempThresholdsSync(diskType: string | undefined): DiskTempThreshold {
const t = (diskType || "").toUpperCase()
if (t === "HDD") return cached.HDD
if (t === "SSD") return cached.SSD
if (t === "NVME") return cached.NVMe
if (t === "SAS") return cached.SAS
// Unknown class — assume SSD-ish numbers (mid-range).
return cached.SSD
}
/** React hook: triggers a load on mount, re-renders on cache update. */
export function useDiskTempThresholds(): DiskTempMap {
const [map, setMap] = useState<DiskTempMap>(cached)
useEffect(() => {
let alive = true
const sub = (m: DiskTempMap) => { if (alive) setMap(m) }
subscribers.add(sub)
loadDiskTempThresholds().then((m) => { if (alive) setMap(m) })
return () => { alive = false; subscribers.delete(sub) }
}, [])
return map
}
/** Imperative invalidate — call after the user saves new thresholds. */
export function invalidateDiskTempThresholdsCache() {
cachedAt = 0
}
+39
View File
@@ -0,0 +1,39 @@
import { exec } from "child_process"
import { promisify } from "util"
const execAsync = promisify(exec)
interface ScriptExecutorOptions {
env?: Record<string, string>
timeout?: number
}
interface ScriptResult {
stdout: string
stderr: string
exitCode: number
}
export async function executeScript(scriptPath: string, options: ScriptExecutorOptions = {}): Promise<ScriptResult> {
const { env = {}, timeout = 300000 } = options // 5 minutes default timeout
try {
const { stdout, stderr } = await execAsync(`bash ${scriptPath}`, {
env: { ...process.env, ...env },
timeout,
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
})
return {
stdout,
stderr,
exitCode: 0,
}
} catch (error: any) {
return {
stdout: error.stdout || "",
stderr: error.stderr || error.message || "Unknown error",
exitCode: error.code || 1,
}
}
}
+127
View File
@@ -0,0 +1,127 @@
/**
* Clipboard helpers for the web terminals.
*
* Mobile browsers (iOS Safari, Android Chrome) don't expose xterm.js's text
* selection / clipboard the same way desktop does, and the mobile toolbar
* around our terminals doesn't include explicit copy/paste keys. The helpers
* below give the toolbar a robust path that:
* - Uses the modern async Clipboard API on HTTPS / localhost.
* - Falls back to a hidden <textarea> + document.execCommand on plain HTTP
* (where the async API is gated by the secure-context requirement).
* - Surfaces a user-visible cue (no toast manager in this stack yet) by
* returning a result the caller can react to.
*/
// xterm.js is imported dynamically by the terminal components and the
// `term` field is typed `any` there. We mirror that here with a minimal
// structural type so this helper has no hard dependency on @xterm/xterm.
type XtermLike = { getSelection?: () => string }
export type ClipboardResult = {
ok: boolean
/** Bytes / chars copied (only meaningful on copy). */
length?: number
/** Best-effort error string for logging — never surfaced verbatim to the user. */
error?: string
}
/**
* Copies the current xterm selection to the clipboard. If there is no active
* selection, returns ok=false with length=0 so the caller can decide whether to
* show a "select text first" hint.
*/
export async function copyTerminalSelection(term: XtermLike | null | undefined): Promise<ClipboardResult> {
const text = term?.getSelection?.() ?? ""
if (!text) {
return { ok: false, length: 0, error: "no-selection" }
}
return copyText(text)
}
/**
* Reads text from the clipboard and feeds it to the terminal via `sendFn`.
* The `sendFn` is the WebSocket sender (or any fn that takes a string and
* pushes it to the remote PTY). Any newlines remain intact so that pasting
* a multi-line block triggers as Enter on each line same as desktop xterm.
*
* Mobile users on plain HTTP (the common case for this dashboard accessed
* via `http://<host>:8008` from an iPad/phone on the LAN) hit two layers of
* blocking:
* 1. `window.isSecureContext` is false on plain HTTP, so the legacy code
* skipped the async API and surfaced a silent error.
* 2. There is no `execCommand('paste')` equivalent that works portably.
*
* The fix here:
* - Attempt `navigator.clipboard.readText()` even when not secure-context;
* many modern browsers permit it on localhost/LAN with user gesture, and
* when they don't they throw, which falls through cleanly.
* - If that fails / returns empty, fall back to `window.prompt()`. The
* native prompt accepts a long-press paste from the OS clipboard on
* every mobile platform, so the user can finish the paste manually
* with one extra tap. Empty / cancelled prompt returns ok=false.
*/
export async function pasteFromClipboard(
sendFn: (text: string) => void,
): Promise<ClipboardResult> {
// Path 1 — async Clipboard API. Try regardless of `isSecureContext` so
// browsers that allow it on LAN-HTTP (Chrome on Android, Firefox) can
// succeed. Throws on iOS Safari / strict configurations — we fall through.
try {
if (typeof navigator !== "undefined" && navigator.clipboard?.readText) {
const text = await navigator.clipboard.readText()
if (text) {
sendFn(text)
return { ok: true, length: text.length }
}
}
} catch {
// Permission denied / not focused / insecure context — fall through to prompt().
}
// Path 2 — `window.prompt()` fallback. Universally supported, accepts a
// long-press paste from the system clipboard, and works over plain HTTP.
// This is the path mobile users without HTTPS rely on.
try {
const text = typeof window !== "undefined"
? window.prompt("Paste content for the terminal:", "")
: null
if (text) {
sendFn(text)
return { ok: true, length: text.length }
}
return { ok: false, error: "user-cancelled" }
} catch (e) {
return { ok: false, error: e instanceof Error ? e.message : "prompt-failed" }
}
}
async function copyText(text: string): Promise<ClipboardResult> {
// Preferred path: async Clipboard API on HTTPS / localhost.
try {
if (typeof navigator !== "undefined" && navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
return { ok: true, length: text.length }
}
} catch {
// fall through
}
// Legacy fallback: hidden textarea + execCommand("copy"). Works on plain HTTP
// where the async API is blocked by the secure-context gate.
try {
const textarea = document.createElement("textarea")
textarea.value = text
textarea.style.position = "fixed"
textarea.style.left = "-9999px"
textarea.style.top = "-9999px"
textarea.style.opacity = "0"
textarea.readOnly = true
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
const ok = document.execCommand("copy")
document.body.removeChild(textarea)
return ok ? { ok: true, length: text.length } : { ok: false, error: "execCommand-failed" }
} catch (e) {
return { ok: false, error: e instanceof Error ? e.message : "fallback-failed" }
}
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Helpers for opening WebSocket connections that require a single-use ticket.
*
* The browser WebSocket API does not allow custom request headers, so the JWT
* Bearer token used for REST calls cannot be sent on the handshake. Instead we
* POST to /api/terminal/ticket (which does require the Bearer token), receive
* a one-shot ticket with TTL ~5s, and append it to the WebSocket URL as a
* query parameter. The backend consumes the ticket atomically on handshake.
*
* See AppImage/scripts/flask_terminal_routes.py `_issue_terminal_ticket`,
* `_consume_terminal_ticket`, `_ws_auth_check`.
*/
import { fetchApi } from "@/lib/api-config"
type TicketResponse = {
success?: boolean
ticket?: string
ttl_seconds?: number
}
/**
* Fetch a one-shot terminal ticket from the backend. Returns the ticket string
* or null if the call fails. Callers should treat null as "open without ticket"
* the backend's _ws_auth_check still accepts unticketed handshakes when auth
* is disabled or declined, so a fresh-install / no-auth setup keeps working.
*/
export async function fetchTerminalTicket(): Promise<string | null> {
try {
const res = await fetchApi<TicketResponse>("/api/terminal/ticket", { method: "POST" })
return typeof res?.ticket === "string" && res.ticket.length > 0 ? res.ticket : null
} catch {
return null
}
}
/**
* Take a base WebSocket URL (e.g. "ws://host:8008/ws/terminal") and return a
* URL with `?ticket=<value>` appended. If the ticket fetch fails the original
* URL is returned unchanged so the handshake can still succeed in unauth mode.
*/
export async function getTicketedWsUrl(baseUrl: string): Promise<string> {
const ticket = await fetchTerminalTicket()
if (!ticket) return baseUrl
const sep = baseUrl.includes("?") ? "&" : "?"
return `${baseUrl}${sep}ticket=${encodeURIComponent(ticket)}`
}
+21
View File
@@ -0,0 +1,21 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatStorage(sizeInGB: number): string {
if (sizeInGB < 1) {
// Less than 1 GB, show in MB
const mb = sizeInGB * 1024
return `${mb % 1 === 0 ? mb.toFixed(0) : mb.toFixed(1)} MB`
} else if (sizeInGB < 1024) {
// Less than 1024 GB, show in GB
return `${sizeInGB % 1 === 0 ? sizeInGB.toFixed(0) : sizeInGB.toFixed(1)} GB`
} else {
// 1024 GB or more, show in TB
const tb = sizeInGB / 1024
return `${tb % 1 === 0 ? tb.toFixed(0) : tb.toFixed(1)} TB`
}
}
+39
View File
@@ -0,0 +1,39 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
experimental: {
esmExternals: 'loose',
},
// Strip every `console.*` call in production builds except `error` and
// `warn` (we still want operators to see real errors in DevTools). Audit
// residual: ~50 leftover `console.log("[v0] ...")` from the v0.dev
// prototype were leaking object dumps to the browser console in production.
compiler: {
removeConsole: {
exclude: ['error', 'warn'],
},
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
};
}
return config;
},
};
export default nextConfig;
+80
View File
@@ -0,0 +1,80 @@
{
"name": "ProxMenux-Monitor",
"version": "1.2.1.3-beta",
"description": "Proxmox System Monitoring Dashboard",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next build"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1",
"@radix-ui/react-avatar": "1.1.2",
"@radix-ui/react-checkbox": "1.1.3",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-menubar": "1.1.4",
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4",
"@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "1.2.2",
"@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "1.1.1",
"@radix-ui/react-slider": "1.2.2",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-switch": "1.1.2",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-toast": "1.2.4",
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"date-fns": "4.1.0",
"dompurify": "^3.2.7",
"embla-carousel-react": "8.5.1",
"marked": "^15.0.7",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "15.1.6",
"next-themes": "^0.4.6",
"react": "^19",
"react-day-picker": "9.8.0",
"react-dom": "^19",
"react-hook-form": "^7.60.0",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.4",
"socket.io-client": "^4.8.1",
"sonner": "^1.7.4",
"swr": "^2.2.5",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"zod": "3.25.67"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"postcss": "^8.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+9
View File
@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200.18 69.76">
<g id="Layer_1-2" data-name="Layer 1">
<path d="M114.26.13c-13.19,0-23.88,10.68-23.88,23.88s10.68,23.9,23.88,23.9,23.88-10.68,23.88-23.88h0c-.02-13.19-10.71-23.88-23.88-23.9ZM114.26,38.94c-8.24,0-14.93-6.69-14.93-14.93s6.69-14.93,14.93-14.93,14.93,6.69,14.93,14.93c-.02,8.24-6.71,14.93-14.93,14.93h0Z"/>
<path d="M24.11,0C10.92-.11.13,10.47,0,23.66c-.13,13.19,10.47,23.98,23.66,24.11h8.31v-8.94h-7.86c-8.24.11-15-6.5-15.1-14.74-.11-8.24,6.5-15,14.74-15.1h.34c8.22,0,14.95,6.69,14.95,14.93h0v21.98h0c0,8.18-6.65,14.83-14.81,14.93-3.91-.04-7.63-1.59-10.39-4.38l-6.33,6.31c4.4,4.42,10.34,6.92,16.57,6.99h.32c13.02-.19,23.49-10.75,23.56-23.77v-22.69C47.65,10.35,37.05.02,24.11,0Z"/>
<path d="M191.28,68.74V23.43c-.32-12.96-10.92-23.28-23.88-23.3-13.19-.13-23.98,10.47-24.11,23.66-.13,13.19,10.49,23.98,23.68,24.11h8.31v-8.94h-7.86c-8.24.11-15-6.5-15.1-14.74s6.5-15,14.74-15.1h.34c8.22,0,14.95,6.69,14.95,14.93h0v44.63h0l8.92.06Z"/>
<path d="M54.8,47.9h8.92v-23.88c0-8.24,6.69-14.93,14.93-14.93,2.72,0,5.25.72,7.46,2l4.48-7.75c-3.5-2.02-7.58-3.19-11.92-3.19-13.19,0-23.88,10.68-23.88,23.88v23.88Z"/>
<path d="M198.01.74c.68.38,1.21.91,1.59,1.59.38.68.57,1.42.57,2.25s-.19,1.57-.59,2.27c-.4.68-.93,1.23-1.61,1.61-.68.4-1.44.59-2.25.59s-1.57-.19-2.25-.59c-.68-.4-1.21-.93-1.59-1.61-.38-.68-.59-1.42-.59-2.25s.19-1.57.59-2.25c.38-.68.93-1.21,1.61-1.61s1.44-.59,2.27-.59c.83,0,1.57.19,2.25.59ZM197.57,7.75c.55-.32.98-.76,1.3-1.32.32-.55.47-1.17.47-1.85s-.15-1.3-.47-1.85-.74-.98-1.27-1.3c-.55-.32-1.17-.47-1.85-.47s-1.3.17-1.85.49c-.55.32-.98.76-1.3,1.32s-.47,1.17-.47,1.85.15,1.3.47,1.85c.32.55.74,1,1.27,1.32.55.32,1.15.49,1.83.49.7-.04,1.32-.21,1.87-.53ZM197.84,4.82c-.15.25-.38.45-.68.59l1.06,1.64h-1.32l-.91-1.42h-.87v1.42h-1.32V2.17h2.12c.66,0,1.19.15,1.57.47.38.32.57.74.57,1.27,0,.34-.08.66-.23.91ZM195.85,4.65c.3,0,.53-.06.68-.19.17-.13.25-.32.25-.55s-.08-.42-.25-.57-.4-.19-.68-.19h-.74v1.53h.74v-.02Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200.18 69.76">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="M114.26.13c-13.19,0-23.88,10.68-23.88,23.88s10.68,23.9,23.88,23.9,23.88-10.68,23.88-23.88h0c-.02-13.19-10.71-23.88-23.88-23.9ZM114.26,38.94c-8.24,0-14.93-6.69-14.93-14.93s6.69-14.93,14.93-14.93,14.93,6.69,14.93,14.93c-.02,8.24-6.71,14.93-14.93,14.93h0Z"/>
<path class="cls-1" d="M24.11,0C10.92-.11.13,10.47,0,23.66c-.13,13.19,10.47,23.98,23.66,24.11h8.31v-8.94h-7.86c-8.24.11-15-6.5-15.1-14.74-.11-8.24,6.5-15,14.74-15.1h.34c8.22,0,14.95,6.69,14.95,14.93h0v21.98h0c0,8.18-6.65,14.83-14.81,14.93-3.91-.04-7.63-1.59-10.39-4.38l-6.33,6.31c4.4,4.42,10.34,6.92,16.57,6.99h.32c13.02-.19,23.49-10.75,23.56-23.77v-22.69C47.65,10.35,37.05.02,24.11,0Z"/>
<path class="cls-1" d="M191.28,68.74V23.43c-.32-12.96-10.92-23.28-23.88-23.3-13.19-.13-23.98,10.47-24.11,23.66-.13,13.19,10.49,23.98,23.68,24.11h8.31v-8.94h-7.86c-8.24.11-15-6.5-15.1-14.74s6.5-15,14.74-15.1h.34c8.22,0,14.95,6.69,14.95,14.93h0v44.63h0l8.92.06Z"/>
<path class="cls-1" d="M54.8,47.9h8.92v-23.88c0-8.24,6.69-14.93,14.93-14.93,2.72,0,5.25.72,7.46,2l4.48-7.75c-3.5-2.02-7.58-3.19-11.92-3.19-13.19,0-23.88,10.68-23.88,23.88v23.88Z"/>
<path class="cls-1" d="M198.01.74c.68.38,1.21.91,1.59,1.59.38.68.57,1.42.57,2.25s-.19,1.57-.59,2.27c-.4.68-.93,1.23-1.61,1.61-.68.4-1.44.59-2.25.59s-1.57-.19-2.25-.59c-.68-.4-1.21-.93-1.59-1.61-.38-.68-.59-1.42-.59-2.25s.19-1.57.59-2.25c.38-.68.93-1.21,1.61-1.61s1.44-.59,2.27-.59c.83,0,1.57.19,2.25.59ZM197.57,7.75c.55-.32.98-.76,1.3-1.32.32-.55.47-1.17.47-1.85s-.15-1.3-.47-1.85-.74-.98-1.27-1.3c-.55-.32-1.17-.47-1.85-.47s-1.3.17-1.85.49c-.55.32-.98.76-1.3,1.32s-.47,1.17-.47,1.85.15,1.3.47,1.85c.32.55.74,1,1.27,1.32.55.32,1.15.49,1.83.49.7-.04,1.32-.21,1.87-.53ZM197.84,4.82c-.15.25-.38.45-.68.59l1.06,1.64h-1.32l-.91-1.42h-.87v1.42h-1.32V2.17h2.12c.66,0,1.19.15,1.57.47.38.32.57.74.57,1.27,0,.34-.08.66-.23.91ZM195.85,4.65c.3,0,.53-.06.68-.19.17-.13.25-.32.25-.55s-.08-.42-.25-.57-.4-.19-.68-.19h-.74v1.53h.74v-.02Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#0d597f"><path d="M23.25 38.81v-6.745l-4.855 4.864c.474.333.968.635 1.48.906.463.243.87.434 1.303.58s.782.24 1.13.304.66.093.95.096m24.822-.562c.045.037.092.07.142.1a2.77 2.77 0 0 0 .385.203 2.93 2.93 0 0 0 .637.194c.296.06.598.088.9.087.3 0 .608-.03.955-.087a7.24 7.24 0 0 0 1.138-.301 9.96 9.96 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-3.685-3.6-12.21-12.258-5.356 5.356-7.23-7.455-18.14 17.935a13.82 13.82 0 0 0 1.5.918c.47.246.91.434 1.317.58a7.18 7.18 0 0 0 1.135.301 5.53 5.53 0 0 0 .955.087c.302.001.604-.028.9-.087a3.29 3.29 0 0 0 .637-.194 2.49 2.49 0 0 0 .385-.197l.145-.104 8.193-8.193 2.924-2.808 8.106 8.106 2.837 2.912a1.29 1.29 0 0 0 .145.101 2.52 2.52 0 0 0 .385.2c.206.085.42.15.637.194.255.052.556.087.903.087.3 0 .608-.03.955-.087a6.89 6.89 0 0 0 1.138-.301 9.95 9.95 0 0 0 1.32-.579c.52-.274 1.02-.58 1.503-.918l-6.508-6.37 1.2-1.2 5.63 5.63 3.283 3.254m-.07-33.96l15.998 27.714L48.003 59.71H15.996L-.002 31.997 15.996 4.283z"/><path d="M38.02 30.65l-4.262-4.256.304-.304 4.3 4.244z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" version="1.0">
<defs>
<linearGradient xlink:href="#a" id="d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-.39377 0 0 .39375 978.34969 416.9815)" x1="541.33502" y1="104.50665" x2="606.91248" y2="303.14029"/>
<linearGradient gradientUnits="userSpaceOnUse" id="a" y2="129.3468" x2="112.49853" y1="6.1372099" x1="112.49854" gradientTransform="translate(287 -83)">
<stop offset="0" style="stop-color:#fff;stop-opacity:0"/>
<stop offset="1" style="stop-color:#fff;stop-opacity:.27450982"/>
</linearGradient>
<linearGradient id="b">
<stop style="stop-color:#00bdec" offset="0"/>
<stop style="stop-color:#40bfde" offset="1"/>
</linearGradient>
<linearGradient id="c">
<stop style="stop-color:#6e6e6e" offset="0"/>
<stop style="stop-color:#4d4d4d" offset="1"/>
</linearGradient>
</defs>
<path style="fill:#1793d1" d="M128 0c-11.39482 27.937051-18.31337 46.237163-31 73.34375 7.7785 8.245207 17.33826 17.811753 32.84375 28.65625-16.66992-6.859577-28.03357-13.728504-36.53125-20.875C77.076039 115.00489 51.621645 163.24639 0 256c40.562707-23.41756 72.007597-37.86167 101.3125-43.375-1.25376-5.40435-1.923505-11.27752-1.875-17.375l.03125-1.28125c.64379-25.99398 14.16934-45.98224 30.1875-44.625 16.01815 1.35723 28.48754 23.53727 27.84375 49.53125-.12127 4.89622-.6905 9.60082-1.65625 13.96875C184.83328 218.51691 215.98162 232.89667 256 256c-7.89193-14.52962-14.96051-27.61983-21.6875-40.09375-10.59609-8.21269-21.64301-18.89743-44.1875-30.46875 15.4958 4.02645 26.60184 8.6825 35.25 13.875C156.97985 71.972668 151.45422 55.040376 128 0z" transform="matrix(1 0 0 1 -.000002 4e-8)"/>
<path style="fill:#fff;fill-opacity:.16568047" d="M818.22607 548.55277c-41.18143-55.89508-50.72685-100.94481-53.14467-111.70015 21.96737 50.6686 21.81733 51.28995 53.14467 111.70015z" transform="matrix(1.34737 0 0 1.34737 -902.40019 -586.944907)"/>
<path style="fill:url(#d);fill-opacity:1" d="M765.09805 436.43495c-1.05641 2.59705-2.08559 5.1172-3.06152 7.51465-1.08115 2.65585-2.10928 5.19128-3.13111 7.677-1.02174 2.48575-2.03439 4.91156-3.03833 7.30591-1.00398 2.39446-2.01068 4.76169-3.03833 7.14355-1.02758 2.38177-2.06156 4.78845-3.15429 7.23633-1.09273 2.44796-2.23335 4.94504-3.43262 7.53784-1.19937 2.59282-2.45641 5.27815-3.80371 8.09448-.18662.39008-.41312.83402-.60303 1.22925 5.75521 6.09563 12.84133 13.14976 24.28345 21.15234-12.34021-5.07792-20.76511-10.15751-27.06665-15.44677-.32717.66791-.61387 1.26431-.95093 1.94824-.44365.90024-.97632 1.92315-1.43799 2.85278-.80967 1.66032-1.65574 3.36576-2.52807 5.12574-.33524.66652-.62948 1.24283-.97413 1.92504-5.50733 11.05265-12.33962 24.28304-21.12915 40.72754 24.09557-13.57581 50.08533-33.16242 97.29615-16.30493-2.36708-4.48319-4.54319-8.68756-6.58692-12.64038-2.0437-3.95294-3.94246-7.6555-5.70556-11.15601-1.76297-3.50043-3.39212-6.80069-4.917-9.92675-1.52486-3.12599-2.93832-6.0765-4.26757-8.90625-1.32934-2.8297-2.58106-5.55264-3.75733-8.16407-1.17634-2.6114-2.29708-5.11315-3.36304-7.58422-1.06607-2.4712-2.08657-4.89718-3.08471-7.30591-.99823-2.4088-1.97267-4.81178-2.94556-7.23633-.34772-.86638-.69553-1.7689-1.0437-2.64404-2.66339-6.25269-5.3982-12.73163-8.55835-20.15503z" transform="matrix(1.34737 0 0 1.34737 -902.40019 -586.944907)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 10.0, SVG Export Plug-In . SVG Version: 3.0.0 Build 77) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
]>
<svg
xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;" i:viewOrigin="262 450" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0"
xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
width="87.041" height="108.445" viewBox="0 0 87.041 108.445" overflow="visible" enable-background="new 0 0 87.041 108.445"
xml:space="preserve">
<metadata>
<variableSets xmlns="&ns_vars;">
<variableSet varSetName="binding1" locked="none">
<variables></variables>
<v:sampleDataSets xmlns="&ns_custom;" xmlns:v="&ns_vars;"></v:sampleDataSets>
</variableSet>
</variableSets>
<sfw xmlns="&ns_sfw;">
<slices></slices>
<sliceSourceBounds y="341.555" x="262" width="87.041" height="108.445" bottomLeftOrigin="true"></sliceSourceBounds>
</sfw>
</metadata>
<g id="Layer_1" i:layer="yes" i:dimmedPercent="50" i:rgbTrio="#4F008000FFFF">
<g>
<path i:knockout="Off" fill="#A80030" d="M51.986,57.297c-1.797,0.025,0.34,0.926,2.686,1.287
c0.648-0.506,1.236-1.018,1.76-1.516C54.971,57.426,53.484,57.434,51.986,57.297"/>
<path i:knockout="Off" fill="#A80030" d="M61.631,54.893c1.07-1.477,1.85-3.094,2.125-4.766c-0.24,1.192-0.887,2.221-1.496,3.307
c-3.359,2.115-0.316-1.256-0.002-2.537C58.646,55.443,61.762,53.623,61.631,54.893"/>
<path i:knockout="Off" fill="#A80030" d="M65.191,45.629c0.217-3.236-0.637-2.213-0.924-0.978
C64.602,44.825,64.867,46.932,65.191,45.629"/>
<path i:knockout="Off" fill="#A80030" d="M45.172,1.399c0.959,0.172,2.072,0.304,1.916,0.533
C48.137,1.702,48.375,1.49,45.172,1.399"/>
<path i:knockout="Off" fill="#A80030" d="M47.088,1.932l-0.678,0.14l0.631-0.056L47.088,1.932"/>
<path i:knockout="Off" fill="#A80030" d="M76.992,46.856c0.107,2.906-0.85,4.316-1.713,6.812l-1.553,0.776
c-1.271,2.468,0.123,1.567-0.787,3.53c-1.984,1.764-6.021,5.52-7.313,5.863c-0.943-0.021,0.639-1.113,0.846-1.541
c-2.656,1.824-2.131,2.738-6.193,3.846l-0.119-0.264c-10.018,4.713-23.934-4.627-23.751-17.371
c-0.107,0.809-0.304,0.607-0.526,0.934c-0.517-6.557,3.028-13.143,9.007-15.832c5.848-2.895,12.704-1.707,16.893,2.197
c-2.301-3.014-6.881-6.209-12.309-5.91c-5.317,0.084-10.291,3.463-11.951,7.131c-2.724,1.715-3.04,6.611-4.227,7.507
C31.699,56.271,36.3,61.342,44.083,67.307c1.225,0.826,0.345,0.951,0.511,1.58c-2.586-1.211-4.954-3.039-6.901-5.277
c1.033,1.512,2.148,2.982,3.589,4.137c-2.438-0.826-5.695-5.908-6.646-6.115c4.203,7.525,17.052,13.197,23.78,10.383
c-3.113,0.115-7.068,0.064-10.566-1.229c-1.469-0.756-3.467-2.322-3.11-2.615c9.182,3.43,18.667,2.598,26.612-3.771
c2.021-1.574,4.229-4.252,4.867-4.289c-0.961,1.445,0.164,0.695-0.574,1.971c2.014-3.248-0.875-1.322,2.082-5.609l1.092,1.504
c-0.406-2.696,3.348-5.97,2.967-10.234c0.861-1.304,0.961,1.403,0.047,4.403c1.268-3.328,0.334-3.863,0.66-6.609
c0.352,0.923,0.814,1.904,1.051,2.878c-0.826-3.216,0.848-5.416,1.262-7.285c-0.408-0.181-1.275,1.422-1.473-2.377
c0.029-1.65,0.459-0.865,0.625-1.271c-0.324-0.186-1.174-1.451-1.691-3.877c0.375-0.57,1.002,1.478,1.512,1.562
c-0.328-1.929-0.893-3.4-0.916-4.88c-1.49-3.114-0.527,0.415-1.736-1.337c-1.586-4.947,1.316-1.148,1.512-3.396
c2.404,3.483,3.775,8.881,4.404,11.117c-0.48-2.726-1.256-5.367-2.203-7.922c0.73,0.307-1.176-5.609,0.949-1.691
c-2.27-8.352-9.715-16.156-16.564-19.818c0.838,0.767,1.896,1.73,1.516,1.881c-3.406-2.028-2.807-2.186-3.295-3.043
c-2.775-1.129-2.957,0.091-4.795,0.002c-5.23-2.774-6.238-2.479-11.051-4.217l0.219,1.023c-3.465-1.154-4.037,0.438-7.782,0.004
c-0.228-0.178,1.2-0.644,2.375-0.815c-3.35,0.442-3.193-0.66-6.471,0.122c0.808-0.567,1.662-0.942,2.524-1.424
c-2.732,0.166-6.522,1.59-5.352,0.295c-4.456,1.988-12.37,4.779-16.811,8.943l-0.14-0.933c-2.035,2.443-8.874,7.296-9.419,10.46
l-0.544,0.127c-1.059,1.793-1.744,3.825-2.584,5.67c-1.385,2.36-2.03,0.908-1.833,1.278c-2.724,5.523-4.077,10.164-5.246,13.97
c0.833,1.245,0.02,7.495,0.335,12.497c-1.368,24.704,17.338,48.69,37.785,54.228c2.997,1.072,7.454,1.031,11.245,1.141
c-4.473-1.279-5.051-0.678-9.408-2.197c-3.143-1.48-3.832-3.17-6.058-5.102l0.881,1.557c-4.366-1.545-2.539-1.912-6.091-3.037
l0.941-1.229c-1.415-0.107-3.748-2.385-4.386-3.646l-1.548,0.061c-1.86-2.295-2.851-3.949-2.779-5.23l-0.5,0.891
c-0.567-0.973-6.843-8.607-3.587-6.83c-0.605-0.553-1.409-0.9-2.281-2.484l0.663-0.758c-1.567-2.016-2.884-4.6-2.784-5.461
c0.836,1.129,1.416,1.34,1.99,1.533c-3.957-9.818-4.179-0.541-7.176-9.994l0.634-0.051c-0.486-0.732-0.781-1.527-1.172-2.307
l0.276-2.75C4.667,58.121,6.719,47.409,7.13,41.534c0.285-2.389,2.378-4.932,3.97-8.92l-0.97-0.167
c1.854-3.234,10.586-12.988,14.63-12.486c1.959-2.461-0.389-0.009-0.772-0.629c4.303-4.453,5.656-3.146,8.56-3.947
c3.132-1.859-2.688,0.725-1.203-0.709c5.414-1.383,3.837-3.144,10.9-3.846c0.745,0.424-1.729,0.655-2.35,1.205
c4.511-2.207,14.275-1.705,20.617,1.225c7.359,3.439,15.627,13.605,15.953,23.17l0.371,0.1
c-0.188,3.802,0.582,8.199-0.752,12.238L76.992,46.856"/>
<path i:knockout="Off" fill="#A80030" d="M32.372,59.764l-0.252,1.26c1.181,1.604,2.118,3.342,3.626,4.596
C34.661,63.502,33.855,62.627,32.372,59.764"/>
<path i:knockout="Off" fill="#A80030" d="M35.164,59.654c-0.625-0.691-0.995-1.523-1.409-2.352
c0.396,1.457,1.207,2.709,1.962,3.982L35.164,59.654"/>
<path i:knockout="Off" fill="#A80030" d="M84.568,48.916l-0.264,0.662c-0.484,3.438-1.529,6.84-3.131,9.994
C82.943,56.244,84.088,52.604,84.568,48.916"/>
<path i:knockout="Off" fill="#A80030" d="M45.527,0.537C46.742,0.092,48.514,0.293,49.803,0c-1.68,0.141-3.352,0.225-5.003,0.438
L45.527,0.537"/>
<path i:knockout="Off" fill="#A80030" d="M2.872,23.219c0.28,2.592-1.95,3.598,0.494,1.889
C4.676,22.157,2.854,24.293,2.872,23.219"/>
<path i:knockout="Off" fill="#A80030" d="M0,35.215c0.563-1.728,0.665-2.766,0.88-3.766C-0.676,33.438,0.164,33.862,0,35.215"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32" preserveAspectRatio="xMidYMid"><path d="M32 16c0 8.836-7.164 16-16 16S0 24.836 0 16 7.164 0 16 0s16 7.164 16 16z" fill="#dd4814"/><path d="M5.12 13.864c-1.18 0-2.137.956-2.137 2.137s.956 2.136 2.137 2.136S7.257 17.18 7.257 16 6.3 13.864 5.12 13.864zm15.252 9.71c-1.022.6-1.372 1.896-.782 2.917s1.895 1.372 2.917.782 1.372-1.895.782-2.917-1.896-1.37-2.917-.782zM9.76 16a6.23 6.23 0 0 1 2.653-5.105L10.852 8.28a9.3 9.3 0 0 0-3.838 5.394C7.69 14.224 8.12 15.06 8.12 16s-.432 1.776-1.106 2.326c.577 2.237 1.968 4.146 3.838 5.395l1.562-2.616A6.23 6.23 0 0 1 9.761 16zM16 9.76a6.24 6.24 0 0 1 6.215 5.687l3.044-.045a9.25 9.25 0 0 0-2.757-6.019c-.812.307-1.75.26-2.56-.208a2.99 2.99 0 0 1-1.461-2.118C17.7 6.84 16.86 6.72 16 6.72c-1.477 0-2.873.347-4.113.96l1.484 2.66c.8-.372 1.69-.58 2.628-.58zm0 12.48c-.94 0-1.83-.21-2.628-.58l-1.484 2.66c1.24.614 2.636.96 4.113.96a9.28 9.28 0 0 0 2.479-.338c.14-.858.65-1.648 1.46-2.118s1.75-.514 2.56-.207a9.25 9.25 0 0 0 2.757-6.019l-3.045-.045A6.24 6.24 0 0 1 16 22.24zm4.372-13.813c1.022.6 2.328.24 2.917-.78s.24-2.328-.78-2.918-2.328-.24-2.918.783-.24 2.327.782 2.917z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,23 @@
# Onboarding Images
Place your screenshot images here with the following names:
- `imagen1.png` - Overview section screenshot
- `imagen2.png` - Storage section screenshot
- `imagen3.png` - Network section screenshot
- `imagen4.png` - VMs & LXCs section screenshot
- `imagen5.png` - Hardware section screenshot
- `imagen6.png` - System Logs section screenshot
## Image Guidelines
- **Format**: PNG or JPG
- **Recommended size**: 1200x800px or similar aspect ratio
- **Quality**: High quality screenshots showing the main features of each section
- **Content**: Capture the full section with representative data
## Notes
- The last slide (Future Updates) doesn't need an image as it uses an icon
- If an image fails to load, the component will show a fallback icon
- Images should be optimized for web (compressed but still high quality)
Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Some files were not shown because too many files have changed in this diff Show More