Compare commits

..

579 Commits

Author SHA1 Message Date
Amir Raminfar a074aa9511 chore: release v10.0.4 2026-02-21 12:11:11 -08:00
Amir Raminfar 78114eb2d2 fix: call Start() before LoadConfig() in agent mode to prevent nil pointer panic (#4468)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 12:10:51 -08:00
Javier Pérez 5ff6d100d4 fix: use coolify.serviceName for unique container names in multi-container apps (#4466) 2026-02-21 06:34:38 -08:00
Amir Raminfar 94cfa6da1c chore: updates to settings 2026-02-20 16:24:58 -08:00
renovate[bot] ffa38bcb2f fix(deps): update all non-major dependencies (#4463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 20:32:46 +00:00
Amir Raminfar 8ab06d9a24 chore: release v10.0.3 2026-02-20 12:14:19 -08:00
Amir Raminfar c3a4583ba7 fix: fixes multiple bugs with alerting 🔔 (#4459) 2026-02-20 17:37:58 +00:00
Amir Raminfar bf16c1042d fix: fixes context menu not showing with little rows. see #4332 (#4462) 2026-02-20 17:01:32 +00:00
Amir Raminfar 1174c1d58e chore: moves container events to trace 2026-02-20 07:10:40 -08:00
renovate[bot] f3c4e806ae fix(deps): update all non-major dependencies (#4460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 21:46:40 +00:00
Amir Raminfar 374865589a fix: fixes new containers not subsrcribing when first starting (#4458) 2026-02-19 10:13:32 -08:00
Richard Hull ec7d15c6bc style: brighten orange theme color (#4457) 2026-02-18 19:56:36 -08:00
Amir Raminfar 9e128d8de8 chore: makes dispatcher configurable 2026-02-17 13:57:27 -08:00
Amir Raminfar 94ea7b39e9 chore: adds type to notifications 2026-02-17 06:25:47 -08:00
Amir Raminfar bcc8adc81b chore: fix translation 2026-02-16 16:21:34 -08:00
Amir Raminfar a79ffdaf50 feat: add metric-based alerts for container CPU/memory thresholds (#4454)
Co-authored-by: Dhaval Patel <dhavu262@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:55:35 +00:00
Amir Raminfar 51a92ba448 chore: fixes javascript tests 2026-02-16 11:47:14 -08:00
Amir Raminfar 8057bffd33 feat: supports copying logs to clipboard (#4451) 2026-02-16 01:09:50 +00:00
Amir Raminfar f30e8e748f chore: clean up (#4449) 2026-02-15 16:42:40 -08:00
Amir Raminfar 7622db0c32 chore: updates docs 2026-02-15 09:08:52 -08:00
Amir Raminfar d6109e39c3 feat: supports host when forwarding to permanent link and provide it as an option in drop down. (#4441) 2026-02-15 15:17:37 +00:00
renovate[bot] 6e38c193ef chore(deps): update dependency jsdom to ^28.1.0 (#4446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 15:14:51 +00:00
Amir Raminfar a300d84b31 fix: fixes broken shell when reading large content. tty was never set (#4443) 2026-02-15 07:14:27 -08:00
Amir Raminfar ed950481ad feat: auto completes json keys in preview (#4440) 2026-02-14 15:58:41 -08:00
renovate[bot] 7df0243533 fix(deps): update all non-major dependencies (#4439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 19:03:28 +00:00
Amir Raminfar 00132ef1cf chore: release v10.0.2 2026-02-14 10:37:57 -08:00
Amir Raminfar fed03cb4db feat: adds better cloud error (#4438) 2026-02-14 18:37:10 +00:00
Amir Raminfar 6840e5b55c fix: resolve staticcheck SA5008 ambiguous json struct tag
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:28:57 -08:00
Paul Close e9c9eca4ae docs: Update Agent Mode documentation to clarify host mappings (#4435)
Co-authored-by: Paul Close <paul@macchiato.home>
2026-02-13 16:20:17 -08:00
Amir Raminfar 161b8d91d7 chore: release v10.0.1 2026-02-12 16:20:13 -08:00
Amir Raminfar 585771b804 fix!: Removes the sha256 support. Dozzle now does a fatal exit (#4429) 2026-02-11 16:23:32 -08:00
Amir Raminfar 78362b35fd chore: fixes workflows 2026-02-11 14:54:47 -08:00
Amir Raminfar fa8f6e4aeb feat: implements more dynamic filenames when downloading 📦 (#4428) 2026-02-11 14:15:30 -08:00
Amir Raminfar c605bbc1bb fix: removes ansi colors when sending to webhook (#4427) 2026-02-11 13:29:58 -08:00
Amir Raminfar f1d4e682db chore: adds dozzle cloud 2026-02-11 07:12:19 -08:00
renovate[bot] fb65dc9b7d fix(deps): update all non-major dependencies (#4424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 14:19:32 +00:00
Amir Raminfar 2324e511b2 docs: adds v10 release (#4417) 2026-02-10 16:14:46 -08:00
Amir Raminfar cc82f9f993 chore: release v10.0.0 2026-02-10 16:14:21 -08:00
renovate[bot] e48aa3139c fix(deps): update all non-major dependencies (#4423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 23:03:25 +00:00
Amir Raminfar aef834ba06 chore: ignore dozzle 2026-02-09 13:08:31 -08:00
renovate[bot] 3678332ab2 chore(deps): update pnpm to v10.29.2 (#4419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 02:02:38 +00:00
renovate[bot] 30614269c7 fix(deps): update dependency @codemirror/view to ^6.39.13 (#4418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 18:17:33 +00:00
Amir Raminfar 6b2af1cf3a chore: fixes bugs on destination (#4416) 2026-02-08 16:00:52 +00:00
Amir Raminfar 80220f2388 chore: clean up 2026-02-08 07:33:22 -08:00
renovate[bot] 98ab88aa71 chore(deps): update all non-major dependencies (#4415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 05:31:41 +00:00
Amir Raminfar c5af5a6f5e feat: adds json sytanx to template (#4414) 2026-02-07 21:26:19 -08:00
Jordy b8f224c13d feat: Expand trace level aliases in level_guesser.go (#4413) 2026-02-07 13:40:44 -08:00
Amir Raminfar 40f335265f feat: adds shortcut for creating alerts (#4411) 2026-02-07 08:16:12 -08:00
renovate[bot] 9ac5fcc2c4 chore(deps): update all non-major dependencies (#4410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 03:18:48 +00:00
renovate[bot] 8d38034c86 chore(deps): update dependency jsdom to v28 (#4409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 15:10:28 +00:00
Amir Raminfar 8c231d7ba7 chore: implements circuit break when too many requests recevied (#4406) 2026-02-06 07:05:19 -08:00
renovate[bot] 0a1d854168 fix(deps): update all non-major dependencies (#4408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 15:03:24 +00:00
Amir Raminfar 425d9e4fdd perf: improves performance by removing watcher on containers (#4394) 2026-02-02 08:47:14 -08:00
renovate[bot] 21676d9917 fix(deps): update all non-major dependencies (#4404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 00:39:31 +00:00
Amir Raminfar c923980ae6 chore: adds host name (#4403) 2026-02-01 12:31:59 -08:00
Amir Raminfar 87ea90966e perf: implements a semaphore when too many requests happen in notificaiton manager (#4402) 2026-02-01 08:53:54 -08:00
Amir Raminfar cd15f91447 fix: fixes broken UI on smaller screens (#4400) 2026-01-31 13:03:11 -08:00
Amir Raminfar 8800bc8c37 chore: fixes typecheck (#4401) 2026-01-31 12:53:45 -08:00
Amir Raminfar ecdcd5fb26 fix: fixes alert edit not loading destination correctly (#4399) 2026-01-31 15:18:43 +00:00
Amir Raminfar ff5867eaca fix: fixes scrolling down in historical logs (#4398) 2026-01-31 07:00:35 -08:00
Amir Raminfar e5422513f7 chore: fixes typecheck 2026-01-30 11:36:53 -08:00
Amir Raminfar cd164e62c5 chore: clean up 2026-01-30 11:11:41 -08:00
Amir Raminfar 03d6c6a41a chore: fixes preview 2026-01-29 16:58:20 -08:00
Amir Raminfar 7131a6bdf8 chore: clean up 2026-01-29 09:20:02 -08:00
Amir Raminfar 4c523d5185 chore: improves README. Fixes #4395 2026-01-29 07:15:48 -08:00
Amir Raminfar 69b4d8b651 chore: makes the experience better for cloud linking (#4393) 2026-01-29 07:05:16 -08:00
renovate[bot] dc41e018ca chore(deps): update docker/login-action action to v3.7.0 (#4392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 15:51:45 +00:00
renovate[bot] ca3cd93e9a chore(deps): update all non-major dependencies (#4391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 21:02:32 +00:00
Amir Raminfar 9224a4d1f9 chore: removes graphql (#4389) 2026-01-26 17:34:06 +00:00
Amir Raminfar c826564e3f Merge remote-tracking branch 'origin/v9.x' 2026-01-26 09:18:29 -08:00
Amir Raminfar 8b4f4d1b00 feat: supports dozzle cloud (#4381) 2026-01-26 07:57:35 -08:00
Amir Raminfar 463fea4634 chore: release v9.0.3 2026-01-26 07:54:41 -08:00
Javier Pérez 4db74360e6 feat: add Coolify labels as fallbacks for container name and group (#4384) 2026-01-26 07:54:14 -08:00
MirzaSamadAhmedBaig 58caca2d68 fix: Avoid fatal on invalid container filters in auth (#4379)
Signed-off-by: Mirza-Samad-Ahmed-Baig <Mirzasamadahmedbaig@gmail.com>
2026-01-26 07:50:37 -08:00
Amir Raminfar 98a4901256 fix: fixes incorrect user scope for agent.find 2026-01-26 07:49:17 -08:00
renovate[bot] 4a6461ce2a fix(deps): update module github.com/puzpuzpuz/xsync/v4 to v4.4.0 (#4388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 00:40:55 +00:00
Amir Raminfar 620e59aa24 fix: fixes incorrect user scope for agent.find 2026-01-25 15:03:38 -08:00
Amir Raminfar c13aca6d2c chore: fixes race issue (#4385) 2026-01-25 07:16:22 -08:00
Robert Breker f3afbf843c fix: use io.Copy for exec TTY output instead of stdcopy (#4382) 2026-01-24 16:56:48 -08:00
Amir Raminfar 10e4dfd17f chore: adds notification to types 2026-01-24 09:17:09 -08:00
MirzaSamadAhmedBaig efcadfb98a fix: Avoid fatal on invalid container filters in auth (#4379)
Signed-off-by: Mirza-Samad-Ahmed-Baig <Mirzasamadahmedbaig@gmail.com>
2026-01-23 11:39:18 -08:00
renovate[bot] f831dc6ecd fix(deps): update all non-major dependencies (#4378)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 19:25:58 +00:00
Amir Raminfar 0ff0d486b1 feat: supports notifications for agents (#4372) 2026-01-22 16:54:53 -08:00
Amir Raminfar a7405ac2d6 chore: updates node (#4374) 2026-01-22 20:02:00 +00:00
Amir Raminfar 3f4baea664 chore: uses go tools instead (#4373) 2026-01-22 19:33:57 +00:00
Amir Raminfar 0d71261358 fix: fixes json objects not working for expr (#4371) 2026-01-22 01:16:00 +00:00
Amir Raminfar 1411c70c9b feat: adds test connection for destination (#4369) 2026-01-21 20:39:11 +00:00
Amir Raminfar 6c2c58345c chore: cleans up types for notifications (#4368) 2026-01-21 20:28:30 +00:00
Amir Raminfar af0407157b chore: cleans up types for notificaitons (#4367) 2026-01-21 10:18:48 -08:00
Amir Raminfar 9f6d24523c chore: css clean up 2026-01-20 14:41:06 -08:00
Amir Raminfar abeb86d285 feat: supports go templates for webhooks (#4366) 2026-01-20 14:21:46 -08:00
Amir Raminfar 4954a020f9 chore: updates info to debug 2026-01-19 19:33:20 -08:00
Amir Raminfar 5b96267f1f chore: updates info to debug 2026-01-19 19:30:52 -08:00
renovate[bot] e2109257e4 fix(deps): update all non-major dependencies (#4362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 03:26:18 +00:00
Amir Raminfar 9ac4762f1d chore: migrates releases to gql (#4361) 2026-01-20 03:18:27 +00:00
Amir Raminfar 321bf43f1c feat: implements web hooks and dispatchers with a complete new notificaiton page (#4349) 2026-01-19 17:30:36 -08:00
Amir Raminfar bca061fa7b chore: release v9.0.2 2026-01-19 10:06:06 -08:00
Amir Raminfar 8599b602dc chore: release v9.0.2 2026-01-19 10:05:41 -08:00
renovate[bot] d60a119f9e fix(deps): update all non-major dependencies to ^1.2.52 (#4359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 00:37:28 +00:00
Amir Raminfar 4b3e44fcdc fix: fixes cpu limit not working correctly (#4356) 2026-01-18 06:12:40 -08:00
Amir Raminfar 51fce48137 feat: shows net stats on mobile (#4357) 2026-01-17 16:17:23 +00:00
Amir Raminfar c1432c33a2 chore: updates deps 2026-01-16 06:53:21 -08:00
renovate[bot] 22d659f509 fix(deps): update dependency unplugin-auto-import to v21 (#4352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 06:52:26 -08:00
renovate[bot] 9c529af633 fix(deps): update all non-major dependencies (#4351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 14:48:07 +00:00
Amir Raminfar 9988271490 chore: updates docs 2026-01-14 10:23:38 -08:00
Amir Raminfar 05f76c4f57 fix: fixes host limits being miscalculated (#4341) 2026-01-13 08:54:24 -08:00
Amir Raminfar 20c1adbd7e chore: fixes short formatbytes 2026-01-12 16:26:08 -08:00
Amir Raminfar d44ab349b9 feat: adds network usage for each container (#4340) 2026-01-12 14:11:19 -08:00
Amir Raminfar 21367bcc36 chore: upgrades proto (#4339) 2026-01-12 17:05:51 +00:00
renovate[bot] 6ece0df082 fix(deps): update all non-major dependencies (#4337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 01:48:44 +00:00
Amir Raminfar b087f30fb9 chore: release v9.0.1 2026-01-07 12:05:07 -08:00
renovate[bot] 5e65e2fca5 fix(deps): update all non-major dependencies (#4329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 17:35:46 +00:00
Amir Raminfar 2303fbdfd3 fix: fixes term colors broken in latest xterm.js (#4328) 2026-01-07 09:27:19 -08:00
Amir Raminfar 078f4c4064 chore: release v9.0.0 2026-01-06 19:36:23 -08:00
Amir Raminfar 5be1a55d33 fix: fixes minor bug when there are empy lines in grouped logs 2026-01-06 19:35:33 -08:00
Amir Raminfar 09b1bae3cd chore: fixes table width for name column 2026-01-06 17:21:13 -08:00
Amir Raminfar 2a0037a667 chore: updates deps and duckdb 2026-01-06 17:15:25 -08:00
Amir Raminfar e00752b0bb feat: adds k8s menu and sets it to default in k8s mode (#4325) 2026-01-06 15:43:56 -08:00
Amir Raminfar 497e474471 perf: improves performance on the homepage (#4324) 2026-01-06 15:29:34 +00:00
Amir Raminfar 753d45d7af chore: updates claude 2026-01-05 13:23:39 -08:00
Amir Raminfar 9a2d6fc6e8 feat: support grouping of log messages when possible (#4320) 2026-01-05 21:21:12 +00:00
Amir Raminfar 4dcaa43597 fix: fixes upper case levels not working in the middle of log (#4323) 2026-01-05 12:34:36 -08:00
Amir Raminfar b51ec5bfa7 fix: fixes mobile vertical align for stats (#4322) 2026-01-05 19:48:32 +00:00
Amir Raminfar 8c8ac09521 fix: fixes container stat not working when too low (#4321) 2026-01-05 19:40:04 +00:00
Amir Raminfar 2537e16b6c chore: shows host count (#4319) 2026-01-05 17:11:05 +00:00
Amir Raminfar a8a76e29c9 feat: shows error when copy is not supported (#4318) 2026-01-04 17:57:46 -08:00
Amir Raminfar 65b2ac9181 feat: allows homepage to have collopasable sections (#4315) 2026-01-04 16:57:10 -08:00
renovate[bot] ddc4e310a8 fix(deps): update module github.com/yuin/goldmark to v1.7.14 (#4316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 00:37:51 +00:00
Amir Raminfar 0a2afcf93c chore: adds setup script 2026-01-04 16:31:55 -08:00
Amir Raminfar 41cc1eb2ff feat: support historical stats on homepage, hosts and containers. (#4307) 2026-01-03 13:28:16 -08:00
Amir Raminfar 2a63d300a4 feat: adds group counter for containers in menu (#4313) 2026-01-03 21:20:51 +00:00
renovate[bot] fb9265204d fix(deps): update all non-major dependencies (#4314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 21:08:25 +00:00
Alec Bakholdin 41d46f95b3 docs: Traefik, Docker Swarm, Dozzle issue (#4306) 2025-12-31 10:45:35 -08:00
renovate[bot] d6ad628769 fix(deps): update dependency @xterm/xterm to v6 (#4304) 2025-12-28 19:34:58 -08:00
renovate[bot] 8c4b103468 fix(deps): update all non-major dependencies (#4303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 02:41:18 +00:00
renovate[bot] 9ee66dbe80 fix(deps): update all non-major dependencies (#4299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 01:39:34 +00:00
renovate[bot] 55aeb46262 fix(deps): update all non-major dependencies (#4298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 17:55:03 +00:00
renovate[bot] 1b260019a1 chore(deps): update dependency @types/node to ^25.0.3 (#4297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 19:51:13 +00:00
renovate[bot] e07d97a28a fix(deps): update all non-major dependencies (#4296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 19:00:31 +00:00
renovate[bot] 7b53cffdae fix(deps): update all non-major dependencies (#4295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 01:49:34 +00:00
Amir Raminfar d630056a61 chore: release v8.14.12 2025-12-14 08:53:05 -08:00
Amir Raminfar 1f712b56e0 fix: fixes start event to have correct time (#4294) 2025-12-14 16:39:46 +00:00
renovate[bot] c199e1fa28 chore(deps): update actions/upload-artifact action to v6 (#4292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 08:45:30 -08:00
renovate[bot] 60c8f7a255 fix(deps): update all non-major dependencies (#4291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 16:31:17 +00:00
Amir Raminfar 6bafcc6f77 chore: release v8.14.11 2025-12-12 16:15:45 -08:00
Amir Raminfar 529fb3ad6d fix: fixes terminal height on mobile (#4288) 2025-12-12 16:15:12 -08:00
Amir Raminfar 8000b6c14e feat: support shell resize (#4287) 2025-12-12 15:07:54 -08:00
Amir Raminfar b74dc9f58a chore: updates docs 2025-12-11 09:48:48 -08:00
renovate[bot] 67156b501f fix(deps): update all non-major dependencies (#4285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 17:30:40 +00:00
Amir Raminfar fd69eae2ea feat: enables action toolbar on menu (#4284) 2025-12-11 17:25:40 +00:00
renovate[bot] 9530bfeadc fix(deps): update all non-major dependencies (#4279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 03:33:55 +00:00
Amir Raminfar 1d6b4293e7 chore: updates typecheck (#4283) 2025-12-11 03:28:37 +00:00
renovate[bot] 940bcde6c6 fix(deps): update all non-major dependencies (#4276)
Deploy VitePress site to Pages / build (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 17:06:47 +00:00
renovate[bot] 836c179b4c fix(deps): update all non-major dependencies (#4271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 06:37:25 +00:00
Amir Raminfar 91152d7a94 chore: release v8.14.10 2025-12-02 10:36:44 -08:00
Amir Raminfar 109c0e50e0 feat: localizes dropdowns (#4273) 2025-12-02 10:33:34 -08:00
Amir Raminfar a9cfa97704 feat: supports grouping containers always (#4272) 2025-12-02 09:50:50 -08:00
Amir Raminfar 4ac7bd0f05 chore: updates readme 2025-12-02 09:16:15 -08:00
renovate[bot] 512293968e chore(deps): update dependency prettier to ^3.7.3 (#4266)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 01:24:07 +00:00
Amir Raminfar e443dd4c9b chore: adds generated files 2025-11-28 08:32:21 -08:00
renovate[bot] 835ca9fbc6 chore(deps): update pnpm to v10.24.0 (#4262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 16:32:16 +00:00
Amir Raminfar cbf30f3ae0 chore: updates modules (#4264) 2025-11-28 16:27:27 +00:00
renovate[bot] 523761c424 chore(deps): update all non-major dependencies to v3.1.5 (#4260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 00:37:05 +00:00
renovate[bot] bba95b98a6 fix(deps): update all non-major dependencies (#4258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 18:23:17 +00:00
renovate[bot] 6d01128092 chore(deps): update actions/checkout action to v6 (#4256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Amir Raminfar <findamir@gmail.com>
2025-11-20 23:36:06 +00:00
renovate[bot] 4b2235e0da fix(deps): update all non-major dependencies (#4255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 20:12:19 +00:00
Amir Raminfar b361f0b772 chore: release v8.14.9 2025-11-19 14:48:21 -08:00
renovate[bot] d84cc574e3 fix(deps): update all non-major dependencies (#4252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 22:42:21 +00:00
Amir Raminfar bafcbf6d66 feat: enables filters and level filters when downloading (#4251) 2025-11-19 14:35:30 -08:00
renovate[bot] 80df836f1d fix(deps): update all non-major dependencies (#4244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 02:17:27 +00:00
Amir Raminfar 1b36d2c79d chore: release v8.14.8 2025-11-15 10:50:12 -08:00
Amir Raminfar b09d73736b feat: adds a dropdown when pages exceed 10 (#4242) 2025-11-15 18:46:34 +00:00
renovate[bot] 604133b591 fix(deps): update all non-major dependencies (#4240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 15:18:29 +00:00
Andrei Micuda 065dd0aea0 feat: update log level retrieval to support CLEF-format logs ( '@l' key) (#4236) 2025-11-10 08:27:18 -08:00
renovate[bot] b1cbd7c23e chore(deps): update pnpm to v10.21.0 (#4234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 02:14:24 +00:00
renovate[bot] 0b8b60afd9 fix(deps): update all non-major dependencies (#4233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-09 15:50:09 +00:00
Amir Raminfar da32dc9171 chore: release v8.14.7 2025-11-06 07:01:45 -08:00
Amir Raminfar 653fc8878a fix: fixes filtering of levels to backfill, similar to search (#4228) 2025-11-06 06:31:02 -08:00
renovate[bot] 3abe70b402 fix(deps): update all non-major dependencies (#4229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 06:10:49 +00:00
Amir Raminfar 6097bd49b7 fix: fixes redirect to also check the host of the container. fixes #4226 (#4227) 2025-11-05 17:51:11 +00:00
renovate[bot] 8462ee9454 chore(deps): update node.js to v24 (#4224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 09:03:02 -08:00
renovate[bot] 8e6f7f16ba fix(deps): update all non-major dependencies (#4223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 00:39:24 +00:00
Amir Raminfar 4605a4a59d docs: updates dtop 2025-10-31 14:53:42 -07:00
renovate[bot] c7615d01a3 chore(deps): update actions/upload-artifact action to v5 (#4219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:54:05 -07:00
renovate[bot] aba8746dd0 fix(deps): update all non-major dependencies (#4218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 00:40:41 +00:00
Amir Raminfar 9e7de63f26 fix: fixes dropdown placement. (#4217) 2025-10-25 20:03:21 +00:00
Amir Raminfar 313cde3ad1 chore: release v8.14.6 2025-10-24 10:01:54 -07:00
Amir Raminfar bf0148d131 chore: fixes tests 2025-10-24 09:52:57 -07:00
Amir Raminfar 72d1ecbcc6 fix: fixes linebreaks breaks. Fixes #4197 (#4214) 2025-10-24 09:34:47 -07:00
Amir Raminfar 50ce41eca3 chore: fixes tests 2025-10-24 09:22:23 -07:00
Amir Raminfar cffd4bd4e3 fix: fixes action menu being cut 2025-10-24 09:18:42 -07:00
Amir Raminfar 0f1e9cf0e6 Merge branch 'master' into bugfix/scroll-jump 2025-10-24 09:09:53 -07:00
renovate[bot] a761129aff fix(deps): update vueuse monorepo to v14 (major) (#4213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 08:45:43 -07:00
Amir Raminfar 16de645a89 chore: fixes tests 2025-10-24 08:43:46 -07:00
renovate[bot] 1c6f30bcd3 fix(deps): update all non-major dependencies (#4211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 14:44:47 +00:00
Amir Raminfar 9f29aa5923 Merge branch 'master' into bugfix/scroll-jump 2025-10-24 07:26:37 -07:00
Amir Raminfar c2a17888a7 chore: fix permissions 2025-10-24 07:22:52 -07:00
Akash R 176c173ac7 fix: scroll-jump by making dropdown position dynamic 2025-10-24 09:59:26 +05:30
renovate[bot] ce436d9697 fix(deps): update all non-major dependencies (#4207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 15:49:26 +00:00
renovate[bot] b752afb352 fix(deps): update dependency vite to v7.1.11 [security] (#4203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 22:07:54 +00:00
renovate[bot] 50dc54f4bb fix(deps): update dependency unplugin-vue-components to v30 (#4201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 10:34:59 -07:00
Amir Raminfar e6eb7eeebd chore: updates d files 2025-10-20 09:36:19 -07:00
renovate[bot] 0cd62745f6 fix(deps): update all non-major dependencies (#4200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 00:34:15 +00:00
Amir Raminfar 911f785e2c feat: support setting the path to certs (#4198) 2025-10-19 12:15:50 -07:00
renovate[bot] 074f402d0f chore(deps): update dependency jsdom to ^27.0.1 (#4199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-19 16:45:04 +00:00
Amir Raminfar 9485fa738b chore: release v8.14.5 2025-10-17 10:11:27 -07:00
renovate[bot] bb56154811 chore(deps): update actions/checkout action to v5 (#4196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 10:10:40 -07:00
renovate[bot] 78e63c39a0 fix(deps): update all non-major dependencies (#4195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 16:58:12 +00:00
Amir Raminfar c1c8e8cccf chore: changes node to node-version: 24.x (#4194) 2025-10-17 16:34:56 +00:00
Amir Raminfar f9bc915ace chore: pins node to v24 (#4193) 2025-10-17 16:26:09 +00:00
Amir Raminfar 938c6229f8 fix: fixes a nil pointer that was happening in the agent (#4183) 2025-10-17 08:30:47 -07:00
Amir Raminfar b945c35bfa chore: updates d files 2025-10-16 07:02:32 -07:00
Amir Raminfar 47adf9845a chore: adds claude 2025-10-14 16:24:08 -07:00
Amir Raminfar 6951c1cc71 chore: adds claude 2025-10-14 16:23:00 -07:00
Amir Raminfar 9dc27fa06c chore: auto gen d file 2025-10-14 16:11:14 -07:00
renovate[bot] f5786ba388 chore(deps): update all non-major dependencies (#4184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 18:42:44 +00:00
Amir Raminfar b7b9ea7c6f chore: fixes typecheck (#4187) 2025-10-14 18:37:51 +00:00
Amir Raminfar d7b6e9ee50 chore: Add Claude Code GitHub Workflow (#4180) 2025-10-14 11:35:32 -07:00
renovate[bot] 34f68849f4 chore(deps): update actions/setup-node action to v6 (#4185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 09:13:18 -07:00
Amir Raminfar fa78467698 chore: updates gitignore 2025-10-14 08:14:38 -07:00
renovate[bot] 573ee5ed03 fix(deps): update all non-major dependencies (#4176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 01:43:52 +00:00
renovate[bot] cb6267f9ae fix(deps): update all non-major dependencies (#4171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 22:20:04 +00:00
renovate[bot] 3aa3b879de fix(deps): update all non-major dependencies (#4170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 02:28:36 +00:00
Zen Voich 79bf0f01f9 chore: change hover text color from secondary to primary in StatMonitor (#4169)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-10-06 12:38:07 -07:00
renovate[bot] 6b442d26a9 chore(deps): update all non-major dependencies (#4168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 17:50:29 +00:00
renovate[bot] 67348ab514 fix(deps): update all non-major dependencies (#4165)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 00:13:09 +00:00
Amir Raminfar cb9ab3f944 chore: release v8.14.4
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-10-02 11:48:13 -07:00
renovate[bot] 0a722bd2da fix(deps): update all non-major dependencies (#4163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 16:19:40 +00:00
Amir Raminfar 3195bc0d80 feat: adds compression and bigger receive limit to agent grpc (#4162)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-10-01 15:36:25 -07:00
Amir Raminfar 7bf455ae6b chore: release v8.14.3
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-09-30 16:38:54 -07:00
renovate[bot] 285a8bf251 fix(deps): update all non-major dependencies (#4158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 23:21:54 +00:00
Amir Raminfar 65d690074c feat: support for dozzle_* roles (#4153)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-09-29 13:04:18 -07:00
renovate[bot] 264af62bff fix(deps): update all non-major dependencies (#4156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 01:39:06 +00:00
Hiren Shah c3ac804e7a Update remote-hosts.md (#4155) 2025-09-28 07:12:31 -07:00
Amir Raminfar e8e1c3ea6b chore: makes save private (#4152) 2025-09-26 18:02:17 +00:00
Dmitry Mazurov 29f06eb00d feat: add DOZZLE_AUTH_LOGOUT_URL support for ForwarderProxy (#4151) 2025-09-26 10:44:14 -07:00
Amir Raminfar 020342a1bd fix: supports roles as json, comma or pipe sepearated values (#4150) 2025-09-26 17:00:00 +00:00
Amir Raminfar 3b0b3ed5fc docs: clean up 2025-09-26 07:03:22 -07:00
Mitch B. 6b121633e9 docs: add SSO example for Pocket ID (#4147) 2025-09-26 06:58:12 -07:00
Amir Raminfar 443084964a chore: release v8.14.2 2025-09-25 14:33:39 -07:00
renovate[bot] dc1314ddc3 fix(deps): update all non-major dependencies (#4146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 21:08:04 +00:00
Amir Raminfar c4060b2206 chore: uses trim instead 2025-09-25 14:05:23 -07:00
Amir Raminfar 5675389049 fix: fixes roles for proxy to default to all (#4145) 2025-09-25 14:01:50 -07:00
Amir Raminfar bc3c812c74 chore: release v8.14.1 2025-09-23 15:36:14 -07:00
Amir Raminfar 5aea48f3ca fix: resets session for existing users who do not have roles defined (#4141) 2025-09-23 11:05:32 -07:00
Amir Raminfar ab86a6c59e chore: release v8.14.0 2025-09-23 10:11:10 -07:00
Amir Raminfar f667e5a271 fix: fixes release 2025-09-23 10:10:21 -07:00
Amir Raminfar 58146d1ac5 chore: updates docs 2025-09-23 10:00:34 -07:00
Amir Raminfar a6b65a1341 chore: release v8.14.0 2025-09-23 09:57:05 -07:00
Amir Raminfar a6003e4c1c chore: reverts release 2025-09-23 09:55:25 -07:00
Amir Raminfar 4e59bb2f8f chore: release v8.14.0 2025-09-23 09:45:13 -07:00
renovate[bot] 220dcd9416 fix(deps): update all non-major dependencies (#4140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 16:38:28 +00:00
Amir Raminfar fb359cb9d5 fix: if roles changed after a restart of Dozzle, then it resets user session (#4139) 2025-09-23 16:03:07 +00:00
Amir Raminfar 395d8641ad fix: fixes dates and labels being incorrect size when changed (#4138) 2025-09-22 23:47:42 +00:00
renovate[bot] 84ec399e50 fix(deps): update dependency unplugin-vue-components to ^29.1.0 (#4134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 01:15:56 +00:00
Amir Raminfar 2b0c9124d7 chore: adds all and throws warning if role does not exist 2025-09-21 16:43:19 -07:00
Amir Raminfar e75b4d03bc chore: quick clean up 2025-09-21 14:57:06 -07:00
Dmitry Mazurov 5f24657b41 feat: Add user roles (#4133) 2025-09-21 14:51:16 -07:00
renovate[bot] 54799b420c fix(deps): update all non-major dependencies (#4132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 14:49:34 +00:00
renovate[bot] 23ad32d05c fix(deps): update all non-major dependencies (#4128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 23:45:55 +00:00
严浩 6e4a5238d3 fix: add full width to side menu (#4127)
Co-authored-by: 严浩 <h_mini2024@oo1.dev>
2025-09-17 10:17:29 -07:00
renovate[bot] a1cedd4b16 fix(deps): update all non-major dependencies (#4126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 15:08:02 +00:00
renovate[bot] 84edb6bdab chore(deps): update actions/setup-node action to v5 (#4106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:29:38 -07:00
Amir Raminfar a8c673544f chore: adds pnpm setup (#4125) 2025-09-16 12:54:27 -07:00
renovate[bot] b2c3363659 fix(deps): update all non-major dependencies (#4124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 19:42:00 +00:00
renovate[bot] 3f376a0bce fix(deps): update all non-major dependencies (#4121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 01:27:19 +00:00
renovate[bot] 16fc75b563 chore(deps): update dependency jsdom to v27 (#4120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 11:00:42 -07:00
renovate[bot] 7ed0ec498d fix(deps): update all non-major dependencies (#4119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 17:50:55 +00:00
Amir Raminfar 11d12d1f6a Revert "feat: enables the archiving of dozzle binaries"
This reverts commit ed5090a9ec.
2025-09-10 16:49:01 -07:00
Amir Raminfar e88c412d62 Revert "more changes"
This reverts commit da7730cffd.
2025-09-10 16:48:59 -07:00
Amir Raminfar 96a02bf213 Revert "chore: release v8.13.13"
This reverts commit 10597521de.
2025-09-10 16:48:58 -07:00
Amir Raminfar 35b2c919cc Revert "chore: fix deploy"
This reverts commit 1168c03fc9.
2025-09-10 16:48:56 -07:00
Amir Raminfar 85396d7631 Revert "chore: release v8.13.14"
This reverts commit a3a6a8ec5c.
2025-09-10 16:48:49 -07:00
Amir Raminfar a3a6a8ec5c chore: release v8.13.14 2025-09-10 14:32:07 -07:00
Amir Raminfar 1168c03fc9 chore: fix deploy 2025-09-10 13:07:33 -07:00
Amir Raminfar 10597521de chore: release v8.13.13 2025-09-10 12:43:13 -07:00
Amir Raminfar da7730cffd more changes 2025-09-10 12:42:26 -07:00
Amir Raminfar ed5090a9ec feat: enables the archiving of dozzle binaries 2025-09-10 12:05:35 -07:00
dependabot[bot] c97a9cc34f chore(deps): bump @duckdb/duckdb-wasm from 1.29.2 to 1.30.0 (#4114) 2025-09-09 07:55:38 -07:00
renovate[bot] 31d7fe5c67 fix(deps): update all non-major dependencies (#4113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 01:40:38 +00:00
renovate[bot] ed8ce856ba fix(deps): update dependency entities to v7 (#4112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 15:02:09 -07:00
renovate[bot] 940ba30633 fix(deps): update all non-major dependencies (#4111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 01:19:39 +00:00
bakerboy448 a4b0b05978 docs: Fix typo in container groups (#4109) 2025-09-07 08:50:58 -07:00
renovate[bot] 146ff180e0 fix(deps): update all non-major dependencies (#4107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 19:16:15 +00:00
renovate[bot] db4f11c68e chore(deps): update actions/setup-go action to v6 (#4105) 2025-09-04 10:39:53 -07:00
renovate[bot] e702b62324 fix(deps): update all non-major dependencies (#4104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 17:31:38 +00:00
Amir Raminfar cf97100b71 chore: updates npm modules to latest (#4103) 2025-09-04 10:25:52 -07:00
Amir Raminfar b5cc4bd514 chore: release v8.13.12 2025-09-03 09:42:45 -07:00
Akash R f6e3d487ac fix: panic - waitGroup counter goes negative (#4101) 2025-09-03 09:42:08 -07:00
renovate[bot] a2cc759538 fix(deps): update dependency daisyui to v5.1.6 (#4100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 13:44:54 +00:00
renovate[bot] cfd757e876 fix(deps): update all non-major dependencies (#4097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 15:00:33 +00:00
uponminiature 01c564228b feat: release check mode (#4095) 2025-09-01 14:28:49 -07:00
Amir Raminfar 0b401aacd1 chore: release v8.13.11 2025-08-31 08:41:31 -07:00
Amir Raminfar d7335a8cbe chore: updates d files 2025-08-29 14:33:42 -07:00
uponminiature 6df66dade9 feat: implement ability to disable avatars (#4091)
Co-authored-by: uponminiature <226739065+uponminiature@users.noreply.github.com>
2025-08-29 21:20:03 +00:00
Amir Raminfar ae083e955a chore: release v8.13.10 2025-08-28 11:30:00 -07:00
renovate[bot] 99faac6e8e fix(deps): update all non-major dependencies (#4089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-28 18:21:32 +00:00
Amir Raminfar 16495a66a0 feat: Changes the fetching of containers from serial to parallel. (#4088) 2025-08-27 20:24:45 -07:00
renovate[bot] 03a2eb2f3b fix(deps): update all non-major dependencies (#4086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-26 17:29:50 +00:00
renovate[bot] bf18fdd1d8 chore(deps): update actions/upload-pages-artifact action to v4 (#4082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-26 10:22:42 -07:00
renovate[bot] 912595f781 fix(deps): update module github.com/stretchr/testify to v1.11.0 (#4084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 02:09:15 +00:00
Amir Raminfar 2a5a0b0c55 chore: clean up 2025-08-22 15:56:58 -07:00
renovate[bot] 4e9667e8cf fix(deps): update all non-major dependencies (#4081)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-22 16:20:36 +00:00
renovate[bot] 8130889484 fix(deps): update all non-major dependencies (#4078)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 19:36:31 +00:00
Amir Raminfar 013f7c5cde chore: release v8.13.9
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-08-15 10:36:48 -07:00
Amir Raminfar 37c1fd032f chore: fixes env in Dockerfile
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-08-15 08:38:51 -07:00
renovate[bot] cd4f98fd66 fix(deps): update all non-major dependencies (#4075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 15:35:36 +00:00
renovate[bot] b78dab6da1 chore(deps): update amannn/action-semantic-pull-request action to v6 (#4076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 15:34:59 +00:00
Amir Raminfar 7be0515648 chore: uses wg.Go where possible (#4074)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-08-15 15:29:25 +00:00
Amir Raminfar af93918f8c chore: enables json-v2 for go (#4073) 2025-08-15 07:57:29 -07:00
Adedamola 9fa0b5256c docs: multiple namespaces use DOZZLE_NAMESPACE (comma-separated), not DOZZLE_NAMESPACES (#4072)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-08-14 06:17:23 -07:00
renovate[bot] f037b81c92 fix(deps): update all non-major dependencies (#4070)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 23:53:33 +00:00
renovate[bot] a97d976c4f chore(deps): update actions/checkout action to v5 (#4071)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-08-12 21:17:09 -07:00
Amir Raminfar 6f87c42d2b fix: fixes context menu to be to the side instead of bottom (#4069)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-08-12 20:29:30 +00:00
renovate[bot] 1064af3175 fix(deps): update all non-major dependencies (#4068)
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-10 15:20:00 +00:00
renovate[bot] 9939b0001e fix(deps): update dependency unplugin-vue-components to v29 (#4064)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 10:12:24 -07:00
renovate[bot] a54dd42531 fix(deps): update all non-major dependencies (#4063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 16:21:41 +00:00
Amir Raminfar ba9faa7691 chore: updates generated files
Test / Go Staticcheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-08-05 09:43:25 -07:00
renovate[bot] e044ecd4a1 fix(deps): update dependency unplugin-auto-import to v20 (#4059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 09:32:30 -07:00
renovate[bot] da2d554bee fix(deps): update all non-major dependencies (#4058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 16:23:41 +00:00
renovate[bot] 9f0791a38b fix(deps): update all non-major dependencies (#4057)
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 00:58:35 +00:00
renovate[bot] eb20f9e4fa fix(deps): update all non-major dependencies (#4056)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 16:04:29 +00:00
renovate[bot] 3ca6495404 fix(deps): update module github.com/docker/docker to v28.3.3+incompatible [security] (#4054)
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 20:07:48 +00:00
renovate[bot] 44d30b8451 fix(deps): update all non-major dependencies to ^13.6.0 (#4052)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 18:57:55 +00:00
renovate[bot] 1f4410f257 fix(deps): update all non-major dependencies (#4051)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 09:44:40 +00:00
renovate[bot] bd3e789d4c fix(deps): update all non-major dependencies (#4050)
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 14:33:26 +00:00
Amir Raminfar 5d32e04af8 chore: release v8.13.8
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-07-24 08:58:59 -07:00
Amir Raminfar 26c85a42b5 fix: fixes search z-index (#4048) 2025-07-24 15:58:36 +00:00
renovate[bot] 4aa06778d7 fix(deps): update all non-major dependencies (#4046)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 02:35:52 +00:00
renovate[bot] 5755afcedd fix(deps): update all non-major dependencies (#4044)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-19 15:12:07 +00:00
Amir Raminfar 482c5ae445 chore: release v8.13.7 2025-07-19 07:32:40 -07:00
Zen Voich 8506f6123e feat: group containers by dev.dozzle.group label (#4043) 2025-07-19 06:09:31 -07:00
renovate[bot] 4d5f94c2bb fix(deps): update all non-major dependencies (#4040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 15:05:30 +00:00
Amir Raminfar 90ca59d283 chore: release v8.13.6 2025-07-15 11:22:30 -07:00
Amir Raminfar 84e608eab1 docs: adds dtop to menu 2025-07-15 11:11:09 -07:00
renovate[bot] 1dddbec30a chore(deps): update dependency @types/node to ^24.0.14 (#4039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 17:04:38 +00:00
Cavazza Tommaso 0d9ff396d6 fix: star conflicting with dropdown (#4038) 2025-07-15 09:58:40 -07:00
Amir Raminfar dc9c6b727d docs: add dtop 2025-07-15 08:35:20 -07:00
Amir Raminfar 4bf342da87 chore: release v8.13.5 2025-07-14 09:26:41 -07:00
renovate[bot] 44c02f6676 fix(deps): update all non-major dependencies (#4036)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-13 17:40:48 +00:00
Jérémy Touati f843635060 fix: allows dots inside URLs (not at end) (#4035) 2025-07-13 10:17:13 -07:00
renovate[bot] bcc8506d8c chore(deps): update pnpm to v10.13.1 (#4032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 15:12:40 +00:00
renovate[bot] c106808c1d chore(deps): update all non-major dependencies (#4030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 02:06:52 +00:00
Amir Raminfar f7ff41881a chore: updates deps 2025-07-08 10:46:20 -07:00
renovate[bot] b58648d562 chore(deps): update dependency @apache-arrow/esnext-esm to v21 (#4029) 2025-07-08 07:14:54 -07:00
renovate[bot] 6d829b9cae fix(deps): update all non-major dependencies (#4028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 03:01:17 +00:00
renovate[bot] 9250db2c7e fix(deps): update all non-major dependencies (#4024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 14:51:18 +00:00
Amir Raminfar 0c41da9e65 chore: release v8.13.4
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-06-30 08:39:20 -07:00
Amir Raminfar 0c25f0f82e fix: removes period at the end of URLs (#4016) 2025-06-30 15:25:08 +00:00
Amir Raminfar 500ffa6b5b fix: fixes null values breaking log viewer (#4014) 2025-06-30 08:22:45 -07:00
renovate[bot] 0d61493e29 fix(deps): update all non-major dependencies (#4012)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 02:13:45 +00:00
Amir Raminfar f8db11a4df chore: release v8.13.3
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-06-27 06:49:08 -07:00
Amir Raminfar f596ebc5f3 chore: updates expire 2025-06-27 06:48:26 -07:00
renovate[bot] 46b48b0f1f fix(deps): update all non-major dependencies (#4009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 13:47:25 +00:00
renovate[bot] a80337177f fix(deps): update all non-major dependencies (#4006)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 21:56:38 +00:00
renovate[bot] b6cf75020f fix(deps): update all non-major dependencies (#4005)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 20:37:47 +00:00
renovate[bot] 135fffabbc chore(deps): update dependency @vitejs/plugin-vue to v6 (#4003)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-06-24 14:45:27 +00:00
renovate[bot] d0fbebea30 fix(deps): update dependency vite to v7 (#4004) 2025-06-24 07:39:37 -07:00
renovate[bot] ae058d3b09 fix(deps): update all non-major dependencies (#4002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 14:28:21 +00:00
renovate[bot] 573afcfd3b chore(deps): update all non-major dependencies (#4001)
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 03:12:26 +00:00
Amir Raminfar 5de0e23b9e chore: release v8.13.2
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-22 15:41:02 -07:00
Amir Raminfar d5b5b92701 chore: moves log to trace 2025-06-22 08:40:27 -07:00
renovate[bot] def47b560e fix(deps): update all non-major dependencies (#4000)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 14:50:08 +00:00
renovate[bot] 3916e3aec8 fix(deps): update all non-major dependencies (#3998)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 22:29:41 +00:00
renovate[bot] 743678da33 fix(deps): update module github.com/go-chi/chi/v5 to v5.2.2 [security] (#3997)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 17:04:59 +00:00
renovate[bot] 7291448d90 fix(deps): update all non-major dependencies (#3992)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 17:15:44 +00:00
renovate[bot] 99d0af05d5 fix(deps): update all non-major dependencies (#3991)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 22:08:39 +00:00
Amir Raminfar a87908d957 fix: fixes z-index on some menu and improves scroll progress fading (#3989)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-17 16:06:08 -07:00
renovate[bot] 8d38f36745 fix(deps): update all non-major dependencies (#3990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 23:05:26 +00:00
Amir Raminfar 100204022d docs: updates guide
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-17 07:24:01 -07:00
Amir Raminfar 35dea10a1c chore: docs
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-16 16:23:20 -07:00
Amir Raminfar 176d41aff3 chore: moves log to debug
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-16 10:07:31 -07:00
Amir Raminfar cd9a771cab chore: release v8.13.1 2025-06-16 09:18:00 -07:00
Amir Raminfar 633d794539 feat: supports wrn level. see #3984 (#3985) 2025-06-16 16:12:07 +00:00
Amir Raminfar c1955a3301 feat: supports nested arrays and objects (#3979) 2025-06-16 16:07:36 +00:00
renovate[bot] 3a4f0a6e79 chore(deps): update all non-major dependencies (#3982)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 21:01:36 +00:00
Amir Raminfar 2c50d870d2 fixes tests
Test / Typecheck (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-06-14 08:23:00 -07:00
renovate[bot] 229758c1db chore(deps): update all non-major dependencies 2025-06-14 09:13:41 +00:00
blaze6x6 6ee4a8594c chore: adds locale for menu items (#3969) (#3977)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-13 07:06:49 -07:00
Amir Raminfar e2b35b3c8c chore: release v8.13.0
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-12 13:47:04 -07:00
Amir Raminfar 2d39629de1 fix: fixes incorrect escaping of array strings. fixes #3962 (#3973) 2025-06-12 11:47:34 -07:00
Amir Raminfar 61af442264 chore: updates types (#3974)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-12 16:13:09 +00:00
Amir Raminfar 8dde2fe9ac feat: changes log actions to hover just a little and take up less space (#3972) 2025-06-12 09:06:36 -07:00
Amir Raminfar da83f8af11 chore: small drop shadow 2025-06-12 06:51:46 -07:00
Amir Raminfar eadf6c98fe chore: reverts playwright 2025-06-12 06:49:18 -07:00
renovate[bot] 931c85afb6 fix(deps): update all non-major dependencies (#3968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 06:39:28 -07:00
Amir Raminfar a47be8730a chore: adds locale for menu items (#3969)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-12 02:59:59 +00:00
Amir Raminfar bcb859509b chore: fixes scroll down 2025-06-11 19:29:32 -07:00
Amir Raminfar d98000b35a feat: adds the ability to show a specific log from the past with a permanent link (#3958)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-11 23:23:48 +00:00
Nebulea a8dced5c2b docs: remove superfluous run in docker command (#3967) 2025-06-11 14:09:34 -07:00
Amir Raminfar 3c2a2119d8 fix: adds scoll to menu (#3966)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-11 08:33:58 -07:00
renovate[bot] a5ed9b65d5 chore(deps): update all non-major dependencies (#3965)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 15:27:08 +00:00
Amir Raminfar e01baf7119 feat: makes shadow on dark mode light (#3961)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-09 14:31:22 -07:00
Amir Raminfar 064d5c21f2 feat: adds icon to log out link (#3960) 2025-06-09 12:02:07 -07:00
Amir Raminfar fae248b066 chore: clean up 2025-06-09 10:12:11 -07:00
renovate[bot] f22c51690b fix(deps): update all non-major dependencies (#3959)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 02:45:09 +00:00
renovate[bot] 373f9ae025 fix(deps): update all non-major dependencies (#3957)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 14:29:34 +00:00
Jungeon d4dab0ad1a feat(i18n): add Korean translation (#3956)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-07 21:06:36 -07:00
renovate[bot] 098f032d03 fix(deps): update all non-major dependencies (#3952)
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 16:33:45 +00:00
Amir Raminfar 9db559c9ce chore: refactors code by moving loader to a log entry (#3951) 2025-06-06 16:28:57 +00:00
Amir Raminfar 7257e35f1b chore: release v8.12.21
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-05 12:19:05 -07:00
GJS 1c20aba5d7 fix: Enhance accuracy of CPU usage metrics in the UI (#3944) 2025-06-05 12:17:24 -07:00
Amir Raminfar 252d865460 fix: fixes really big logs being split in Docker that exceeded 16KB (#3946) 2025-06-04 13:54:13 -07:00
renovate[bot] 02c28fd001 fix(deps): update all non-major dependencies (#3949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 16:02:58 +00:00
renovate[bot] b39f2ae6a8 chore(deps): update all non-major dependencies (#3947)
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 21:14:52 +00:00
GJS 7e2dd1c69e fix: Adjusted <meter> thresholds for CPU and Memory usage display (#3940)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-06-02 05:46:05 -07:00
renovate[bot] 7e5b413c48 chore(deps): update dependency @types/node to ^22.15.29 (#3941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 00:51:36 +00:00
Amir Raminfar ec513adde2 chore: release v8.12.20 2025-06-01 07:46:42 -07:00
GJS 186b4504fc fix: Improve Dutch translation for help support message in nl.yml (#3938)
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-05-30 21:20:51 -07:00
Amir Raminfar 9dc286643e chore: locale for collapse (#3937) 2025-05-30 13:22:24 -07:00
Amir Raminfar 553cfd558d chore: fixes settings page broken detailed page 2025-05-30 12:06:08 -07:00
Amir Raminfar 31174a5214 feat: adds a option to collapse all in side menu (#3930) 2025-05-30 10:20:58 -07:00
renovate[bot] 515849e72a fix(deps): update all non-major dependencies (#3936)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 16:23:46 +00:00
Amir Raminfar f442635f55 chore: release v8.12.19 2025-05-30 08:46:22 -07:00
Amir Raminfar a8f2dc17d6 fix: fixes raw message not being there from agents (#3935) 2025-05-30 08:37:32 -07:00
Amir Raminfar 1faa84abc5 chore: fixes settings page and moves donation to top (#3929) 2025-05-29 17:01:13 -07:00
renovate[bot] bdcb1673e1 fix(deps): update all non-major dependencies (#3928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 15:32:04 +00:00
Amir Raminfar 4de23926eb chore: fix github link 2025-05-28 12:52:53 -07:00
Amir Raminfar d4defe680e chore: release v8.12.18 2025-05-28 10:58:12 -07:00
Amir Raminfar 4c2b459fa8 feat: adds support for settings page and adds links to github (#3922) 2025-05-28 16:01:37 +00:00
renovate[bot] aeadae0d30 fix(deps): update all non-major dependencies (#3921)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 15:51:59 +00:00
Amir Raminfar f6f1b5207e feat: adds a button to fetch logs when skipping happens (#3920) 2025-05-28 08:47:01 -07:00
renovate[bot] cfe7285bf7 fix(deps): update all non-major dependencies (#3919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-27 16:29:23 +00:00
renovate[bot] defaba29c2 fix(deps): update dependency vue-i18n to ^11.1.4 (#3914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 01:49:23 +00:00
Amir Raminfar 2aad18d729 fix: fixes logfmt in details (#3912) 2025-05-24 13:47:13 -07:00
Amir Raminfar 9c52d604bc chore: release v8.12.17 2025-05-23 09:01:39 -07:00
renovate[bot] 7f936a9a38 fix(deps): update all non-major dependencies (#3910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 16:00:23 +00:00
Amir Raminfar 0c9065a210 feat: uses colored meters instead of progress bar for homepage (#3908) 2025-05-23 08:54:44 -07:00
Amir Raminfar b70c2da290 fix: fixes search and urls in detail panel (#3907) 2025-05-21 19:59:00 -07:00
Amir Raminfar 804199aa9a fix: fixes url pattern matcher with quotes (#3902) 2025-05-21 08:52:26 -07:00
Amir Raminfar 50c3608a5e chore: release v8.12.16 2025-05-19 06:18:36 -07:00
renovate[bot] 65b68ad907 chore(deps): update dependency @types/node to ^22.15.19 (#3900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 01:18:47 +00:00
Amir Raminfar fd663b9c0e feat: adds option to instantly redirect to new container (#3897) 2025-05-18 18:14:06 -07:00
Amir Raminfar 7a0296fc30 fix: fixes broken relative time in scroller (#3899) 2025-05-18 11:15:03 -07:00
renovate[bot] b8a3325ea7 fix(deps): update all non-major dependencies (#3896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-18 14:24:36 +00:00
Maciej Wakuła 0ade181ab2 docs: remove leading tcp:// (#3893)
Co-authored-by: Maciej Wakula <adderek.pl@gmail.com>
2025-05-16 13:25:12 -07:00
renovate[bot] 7d60d951ff fix(deps): update all non-major dependencies (#3892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-16 16:30:50 +00:00
Amir Raminfar 66b0dfe280 chore: adds shell to analytics (#3891) 2025-05-15 15:21:53 +00:00
renovate[bot] c273cb5320 fix(deps): update all non-major dependencies (#3890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 15:18:42 +00:00
renovate[bot] 570ef771dd fix(deps): update all non-major dependencies (#3888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-14 18:14:33 +00:00
renovate[bot] 07014470e9 chore(deps): update dependency @apache-arrow/esnext-esm to v20 (#3889)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-14 11:09:29 -07:00
Amir Raminfar bd3c2f5c24 chore: typo 2025-05-11 18:39:22 -07:00
Amir Raminfar 6f45faf27a docs: updates guide 2025-05-11 18:36:57 -07:00
StevenMcElligott e94a245440 docs(Agents): Add more explanations about Socket Proxies and agents (#3883) 2025-05-11 11:24:06 -07:00
Fadilah Riczky daa02dd899 feat(i18n): add Indonesian translation (#3884) 2025-05-11 11:23:28 -07:00
renovate[bot] 6a27f9f367 chore(deps): update dependency lint-staged to v16 (#3882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 12:13:04 -07:00
renovate[bot] 9d7b8f26a6 fix(deps): update all non-major dependencies (#3881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 19:09:13 +00:00
Amir Raminfar 82576ae452 chore: release v8.12.15 2025-05-08 07:20:49 -07:00
Amir Raminfar 2ca71a49ba chore: updates esbuild 2025-05-08 07:15:41 -07:00
Amir Raminfar 3abeeec814 chore: updates esbuild 2025-05-08 07:13:40 -07:00
renovate[bot] 5619201886 fix(deps): update all non-major dependencies (#3877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-07 15:48:15 +00:00
Amir Raminfar 0c06d25ae3 chore: clean up 2025-05-07 08:41:36 -07:00
Amir Raminfar 7a070b90ed fix: improves translations (#3876) 2025-05-07 15:40:24 +00:00
Amir Raminfar 9c70416aec feat: localizes distance time (#3874) 2025-05-07 08:37:17 -07:00
GJS 68ca6d3040 feat: Add Dutch Translation (I18N) Support (#3872) 2025-05-05 12:17:49 -07:00
Amir Raminfar 711745eb1f fix: i18n for no-logs (#3873) 2025-05-05 18:49:42 +00:00
Amir Raminfar c172f6f73f chore: updates docs
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-05-05 08:49:13 -07:00
renovate[bot] 7941bd7b6f fix(deps): update module github.com/puzpuzpuz/xsync/v4 to v4.1.0 (#3870)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 03:18:53 +00:00
Amir Raminfar 595e39a139 chore: release v8.12.14
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-05-01 16:50:29 -07:00
renovate[bot] 8e074df145 fix(deps): update dependency daisyui to v5.0.35 (#3869)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 15:21:07 +00:00
Amir Raminfar 54c6e1ff93 chore: fixes tests
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-05-01 05:52:46 -07:00
Amir Raminfar 10e819fc09 chore: removes width and height 2025-05-01 05:47:37 -07:00
iamdabe c16cfa2c71 updated logo and favicon for docs 2025-05-01 12:23:05 +02:00
iamdabe 56eebf784a Updated SVG logo 2025-05-01 12:13:51 +02:00
Amir Raminfar 5c206b6de3 chore: fix css bug (#3865)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-05-01 03:56:55 +00:00
renovate[bot] 2add15bf42 fix(deps): update all non-major dependencies (#3864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 03:40:02 +00:00
Amir Raminfar 7ff1ccdffb chore: cleans up css (#3862) 2025-05-01 03:29:19 +00:00
Amir Raminfar b961b11499 feat: updates dozzle logos and favicons (#3858) 2025-04-30 15:50:27 -07:00
Amir Raminfar 514c690c7a chore: updates deps
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-30 11:17:05 -07:00
Amir Raminfar e8b3e72265 chore: fixes dates on mobile 2025-04-30 08:41:23 -07:00
Amir Raminfar e8d8b72406 feat: makes container name narrower on mobile (#3853)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-29 06:19:56 -07:00
Amir Raminfar 0c962eec43 chore: centers host title
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-27 14:33:59 -07:00
Amir Raminfar c7f318cc5c chore: release v8.12.13
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-04-27 07:15:44 -07:00
Amir Raminfar 0e8452c146 feat: supports multiple namespaces for k8s (#3848) 2025-04-27 14:02:03 +00:00
renovate[bot] 2d3f6e2b1b fix(deps): update all non-major dependencies (#3850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 13:58:21 +00:00
Amir Raminfar 92665a76de chore: cleans up css styles (#3849)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-26 20:35:14 -07:00
Amir Raminfar 78ace3178b chore: typo fix 2025-04-26 13:58:47 -07:00
renovate[bot] a13b7b0e9d fix(deps): update all non-major dependencies (#3847)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-26 15:10:32 +00:00
Amir Raminfar 4da0486295 chore: little improvements to the login page (#3846)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-25 09:37:38 -07:00
Amir Raminfar 11bc1c7fc2 fix: fixes total limit memory with multiple containers on the same host (#3842) 2025-04-25 06:23:26 -07:00
Amir Raminfar f001b48fa3 chore: release v8.12.12
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-24 15:15:45 -07:00
Amir Raminfar baf9f74fd0 fix: adds html content type to index.html (#3845) 2025-04-24 15:14:37 -07:00
Amir Raminfar f817452fb5 chore: remove @oddbird/css-anchor-positioning 2025-04-24 14:37:50 -07:00
Amir Raminfar 95b27f60cb fix: fixes width login page (#3843) 2025-04-24 20:45:33 +00:00
Amir Raminfar 9daa3459ad chore: release v8.12.11 2025-04-24 12:23:37 -07:00
Amir Raminfar 01343b74bf feat: improves dropdowns for other containers (#3840) 2025-04-24 19:12:53 +00:00
Amir Raminfar 5433939690 fix: fixes searching escaped html and by unescaping before searching (#3838)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-24 11:27:53 -07:00
renovate[bot] effeb5aa52 fix(deps): update all non-major dependencies (#3841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-24 16:35:20 +00:00
Amir Raminfar 7c59060e1e chore: minor clean up
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-23 14:57:28 -07:00
Amir Raminfar 85c42eed11 chore: makes animation better 2025-04-23 14:38:15 -07:00
Amir Raminfar 433c0eb59e feat: makes homepage more responsive and adds some borders for better visibility (#3837) 2025-04-23 13:52:32 -07:00
Amir Raminfar 044ef7a2b2 chore: updates docs
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-23 08:44:36 -07:00
Amir Raminfar 847c4ee115 chore: clean up
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-22 15:20:38 -07:00
Amir Raminfar 4f912b6fdc fix: fixes icons on mobile for close (#3833)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-22 10:13:07 -07:00
Amir Raminfar 5616c07bad fix: fixes drawer being broken on pwa mode (#3832) 2025-04-22 15:11:04 +00:00
Amir Raminfar 31c51f1939 fix: fixes top padding for drawer in PWA mode (#3829)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-21 22:15:03 +00:00
Amir Raminfar 10705f05a6 fix: fixes potential nil pointer when no container exists (#3828) 2025-04-21 12:55:52 -07:00
Amir Raminfar 3645100402 fix: fixes mobile menu having the wrong margin in pwa (#3826) 2025-04-21 12:13:28 -07:00
Amir Raminfar 757c1385f7 chore: fixes static checker 2025-04-21 11:19:58 -07:00
Amir Raminfar ccfab708d4 feat: uses brotli on the fly for static files and gzip for streaming (#3825) 2025-04-21 18:14:57 +00:00
renovate[bot] 55168fd81a fix(deps): update all non-major dependencies (#3827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 18:04:29 +00:00
renovate[bot] bea1876c7c fix(deps): update dependency @intlify/unplugin-vue-i18n to ^6.0.7 (#3822)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 02:34:50 +00:00
Amir Raminfar ae7eee9bca chore: release v8.12.10
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-20 08:02:41 -07:00
Amir Raminfar d98b84e24a fix: fixes sql analytics when error occurs (#3820)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-19 16:44:40 +00:00
Amir Raminfar 18c87256cc fix: fixes alerts on mobile (#3819) 2025-04-19 09:41:57 -07:00
Amir Raminfar e96631930e chore: adds some shadows and makes colors better (#3818)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-19 08:04:59 -07:00
Amir Raminfar 76b1229341 chore: release v8.12.9
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-18 14:24:57 -07:00
renovate[bot] 440f9394d6 fix(deps): update all non-major dependencies (#3817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 20:58:05 +00:00
Amir Raminfar ec792318e6 feat: adds better disabled colors and loader while fetching big logs (#3816) 2025-04-18 20:52:53 +00:00
Amir Raminfar 16db90b660 docs: updates docs to Ed25519
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-17 17:21:33 -07:00
Amir Raminfar eb22752b9a chore: updates to Ed25519 2025-04-17 17:20:19 -07:00
Amir Raminfar 642ab45ef7 chore: fixes tests
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-17 14:24:34 -07:00
renovate[bot] 12fadb9348 fix(deps): update all non-major dependencies (#3813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 14:09:33 -07:00
Amir Raminfar 1cf178e37d feat: more improvements to analytic and gzips logs for better performance (#3814) 2025-04-17 14:09:06 -07:00
Amir Raminfar 061cd0c445 feat: creates betters messaging for analytics mode (#3812) 2025-04-17 17:32:20 +00:00
renovate[bot] e8f796a7d6 fix(deps): update all non-major dependencies (#3811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 15:42:02 +00:00
Amir Raminfar 78f94f1f16 chore: updates d.ts
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-04-15 15:16:35 -07:00
renovate[bot] cc980361fd fix(deps): update module github.com/yuin/goldmark to v1.7.10 (#3809)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 18:55:20 +00:00
Amir Raminfar 736ad8ed5b chore: updates modules (#3808) 2025-04-15 11:28:30 -07:00
renovate[bot] d0dbd2051e fix(deps): update all non-major dependencies (#3807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 15:54:30 +00:00
Amir Raminfar 40a17b4088 chore: release v8.12.8
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-14 08:33:33 -07:00
renovate[bot] b3da3d70a9 chore(deps): update dependency jsdom to ^26.1.0 (#3806)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 03:44:40 +00:00
Amir Raminfar 28588fdfd2 chore: fixes center css (#3804) 2025-04-13 15:31:42 -07:00
Amir Raminfar a33f7bf3b2 fix: reverts confirm password 2025-04-13 15:31:28 -07:00
Amir Raminfar febdbff0de fix(docs): updates typos in docs 2025-04-13 15:27:36 -07:00
Amir Raminfar 0fee11e036 fix: fixes broken link on container title (#3803)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-13 12:54:20 -07:00
Amir Raminfar b768cbfc89 docs: updates generate command 2025-04-13 12:33:10 -07:00
Amir Raminfar d43de49b6d fix: fixes collapse color visually hard to see on light theme (#3801) 2025-04-13 09:42:49 -07:00
Amir Raminfar facc137a93 chore: updates duckdb (#3799)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-12 21:05:28 +00:00
renovate[bot] a052f55dc6 fix(deps): update all non-major dependencies (#3798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 21:00:54 +00:00
Amir Raminfar 481f2bd592 chore: format 2025-04-12 13:44:40 -07:00
Amir Raminfar b33ae4fa4e fix: Use /tmp instead of ./ to ensure that user permissions can be mounted correctly. (#3797) 2025-04-12 13:44:21 -07:00
Amir Raminfar f35c472088 chore: release v8.12.7
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-04-11 09:42:13 -07:00
Amir Raminfar 7455a4c53c feat: improves generate with confirm to catch typos (#3794) 2025-04-11 09:16:04 -07:00
renovate[bot] 84101620f8 fix(deps): update dependency vite to v6.2.6 [security] (#3792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 15:53:32 +00:00
renovate[bot] 51695b486e fix(deps): update dependency daisyui to v5.0.18 (#3793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 15:53:16 +00:00
Piyush Dixit 2f84190faa feat: Add support for both .yml and .yaml files and prioritize .yml for users file (#3789)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-10 21:46:03 -07:00
Amir Raminfar ebcedb2a67 chore: cleans up proto tranformation (#3791)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-10 16:31:23 -07:00
Amir Raminfar a514168e45 chore: adds fully loaded to agent proto (#3787)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-09 17:33:44 -07:00
Amir Raminfar 6bb3777a64 chore: release v8.12.6 2025-04-09 15:18:37 -07:00
Amir Raminfar d22e6f9014 chore: removes unused imports 2025-04-09 15:17:42 -07:00
renovate[bot] b779998476 fix(deps): update all non-major dependencies (#3786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-09 22:07:32 +00:00
Amir Raminfar 70c23b0b1e fix: fixes broken cpu and memory limits on swarm mode. correctly implements limits with agents (#3785) 2025-04-09 15:02:38 -07:00
Amir Raminfar ddff265023 fix: fixes cpu limit being miscalculated (#3782)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-09 08:58:21 -07:00
Amir Raminfar 52edcce8d8 chore: updates documentation
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-08 11:00:34 -07:00
Amir Raminfar 0f75010b35 chore: release v8.12.5 2025-04-08 10:51:06 -07:00
Amir Raminfar 5e228756f3 chore: fixes alpha on background 2025-04-08 10:50:43 -07:00
Amir Raminfar 3775170457 fix: fixes backdrop on fuzzy search (#3776) 2025-04-08 10:49:12 -07:00
renovate[bot] 42edc4c623 fix(deps): update all non-major dependencies (#3773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 16:04:46 +00:00
Amir Raminfar f363b6f366 feat: adds limit to docker containers (#3766) 2025-04-08 15:57:33 +00:00
Amir Raminfar df7e461876 chore: improves logging for duplicate hosts (#3772) 2025-04-08 08:50:02 -07:00
Andrea Titolo f691a22d05 fix: update and improve it locale (#3771) 2025-04-08 07:43:32 -07:00
Amir Raminfar 9c2e03727b fix: fixes mobile menu having the wrong padding (#3765)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
2025-04-07 23:57:50 +00:00
renovate[bot] 2177320259 fix(deps): update all non-major dependencies (#3763)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 03:37:35 +00:00
Agneev Mukherjee 2a2f6bc9c6 fix: use light mode bg color as theme color (#3757)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-04-05 09:29:17 -07:00
renovate[bot] e4fab590b9 fix(deps): update all non-major dependencies (#3759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 14:42:59 +00:00
Amir Raminfar cf750b6e39 chore: release v8.12.4 2025-04-03 13:43:49 -07:00
renovate[bot] 8e2ae35ad2 fix(deps): update all non-major dependencies (#3755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 20:42:37 +00:00
Amir Raminfar 8b4f5ea32d fix: trims spaces around remote agents and hosts (#3754) 2025-04-03 10:16:13 -07:00
Amir Raminfar 2fc848254d chore: updates sync package to v4 (#3751) 2025-04-02 20:25:19 +00:00
renovate[bot] fa2dc863ea fix(deps): update module github.com/puzpuzpuz/xsync/v3 to v4 (#3749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 09:52:22 -07:00
renovate[bot] 11a78629ef fix(deps): update all non-major dependencies (#3748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 16:46:45 +00:00
Amir Raminfar 8b058cae07 feat: adds shell support for k8s mode (#3747) 2025-04-02 09:27:57 -07:00
liutianping 8c4fcc6a7a fix(docs): update nginx websocket config example (#3745) 2025-04-01 21:25:03 -07:00
Amir Raminfar 9b5aaad109 fix(docs): updates docs home page 2025-04-01 10:20:14 -07:00
Amir Raminfar aad9287b97 chore: clean up mods 2025-04-01 08:50:32 -07:00
renovate[bot] 1e6b7da245 fix(deps): update all non-major dependencies (#3741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 15:49:35 +00:00
Amir Raminfar b8bc681f7a fix(docs): updates docs for shell 2025-03-31 16:50:22 -07:00
Amir Raminfar c646281e46 chore: release v8.12.3 2025-03-31 16:35:24 -07:00
Amir Raminfar 27478eb652 feat: adds shell support for agents and swarm mode (#3739) 2025-03-31 15:42:26 -07:00
Amir Raminfar 492c5bfd32 chore: release v8.12.2 2025-03-31 13:49:52 -07:00
Amir Raminfar 397f42a223 chore: fixes wording 2025-03-31 13:48:50 -07:00
Amir Raminfar ddcc5ccbe1 fix(docs): updates docs for shell 2025-03-31 13:41:33 -07:00
Amir Raminfar 9ae7c4cbd8 fix: fixes cursor color on shell (#3738) 2025-03-31 20:36:04 +00:00
Amir Raminfar 076f5d0fc9 feat: adds shell behind a flag or env var (#3737) 2025-03-31 13:31:42 -07:00
Peter Dave Hello b8a8781a9b fix: Update and improve zh-tw Traditional Chinese locale (#3736) 2025-03-31 16:49:13 +00:00
renovate[bot] d5abf1868a fix(deps): update all non-major dependencies (#3735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 16:31:09 +00:00
Amir Raminfar d5a2841e02 chore: release v8.12.1 2025-03-31 08:54:29 -07:00
Amir Raminfar 410f5f305c fix: only shows attach and shell for docker (#3731)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-03-30 10:19:35 -07:00
295 changed files with 22626 additions and 6922 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/dozzle"
cmd = "go build -race -o ./tmp/dozzle ."
cmd = "GOEXPERIMENT=jsonv2 go build -race -o ./tmp/dozzle ."
delay = 1000
exclude_dir = [
"assets",
@@ -35,7 +35,7 @@ pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = true
stop_on_error = false
stop_on_error = true
[color]
app = ""
+30
View File
@@ -0,0 +1,30 @@
# Bug Hunter Agent Memory
## Codebase Patterns
### Notification System Architecture
- `notification.Manager` owns subscriptions (xsync.Map) and dispatchers (xsync.Map)
- Two processing goroutines: `processLogEvents` and `processStatEvents` started in `NewManager`
- Stats listener has start/stop lifecycle; log listener is always-on once started
- `MultiHostService` wraps Manager and handles config persistence + agent broadcast
### Known Bug-Prone Areas
- **broadcastNotificationConfig**: Has historically missed fields when converting between internal and types packages (APIKey, Prefix, ExpiresAt were missed)
- **TriggeredContainerIDs lazy init**: Race condition risk - initialized lazily in AddTriggeredContainer without sync
- **Channel close handling**: enrich() in stats_listener doesn't check for closed channel, can hot-spin
- **LogAlertFields canSave**: Allows empty logExpression (no error = valid), creates dead subscriptions
### Type Mapping Gotchas
- `container.ContainerStat` -> `types.NotificationStat`: field names differ (CPUPercent vs cpu expr tag)
- `notification.DispatcherConfig` -> `types.DispatcherConfig`: must copy ALL fields including APIKey, Prefix, ExpiresAt
- Frontend `NotificationRule.cooldown` is optional, backend defaults to 300 via `GetCooldownSeconds()`
### Concurrency Model
- xsync.Map used throughout for concurrent access (subscriptions, dispatchers, activeStreams, containers)
- Subscription fields use atomic types: TriggerCount (atomic.Int64), LastTriggeredAt (atomic.Pointer)
- MetricCooldowns uses xsync.Map for per-container cooldown tracking
- sendSem (semaphore.Weighted=5) limits concurrent notification sends
@@ -0,0 +1,30 @@
# Go Performance Reviewer Memory
## Hot Paths Identified
- **Stats processing pipeline**: `ContainerStatsListener.enrich()` -> channel -> `Manager.processStatEvents()` -> `processStatEvent()`. Runs continuously for every container stat tick (~1/sec per container).
- **Log processing pipeline**: `ContainerLogListener` -> `logChannel` (buffered 1000) -> `Manager.processLogEvents()` -> `processLogEvent()`. Every log line from matched containers flows here.
- Both pipelines do `expr.Run()` per subscription per event -- compiled programs are cached but still run per-event.
## Architecture Notes
- `ContainerStore.SubscribeStats` and `SubscribeEvents` both call `statsCollector.Start/Stop` -- multiple subscribers share the same collector. Stop-on-cancel can interfere across subscribers.
- `xsync.Map` (from puzpuzpuz/xsync/v4) used extensively for concurrent maps. No built-in TTL/eviction.
- `ContainerStatEvent` carries full `Container` + `Host` structs by value through channels. `Container` is ~200+ bytes (strings, map, RingBuffer pointer, times).
- TTL caches in both listeners (`cachedContainerInfo`) lack eviction -- grow unboundedly with container churn.
## Existing Patterns
- Semaphore-based concurrency limiting: `sendSem` (weighted 5) for notification dispatch, `maxFetchParallelism` (30) for container fetching.
- Lazy stats collection: stats collector starts on first subscriber, stops when last unsubscribes (but the multi-subscriber Stop race exists).
- `sync.Pool` not currently used in notification/stats paths.
- Cooldown tracking via `xsync.Map[string, time.Time]` per subscription -- also no eviction.
## Key File Paths
- `internal/container/container_store.go` -- Container store with stats collector lifecycle
- `internal/notification/processing.go` -- Log + stat event processing (hot path)
- `internal/notification/stats_listener.go` -- Stats subscription and enrichment
- `internal/notification/log_listener.go` -- Log stream management
- `internal/notification/manager.go` -- Subscription CRUD and listener orchestration
- `internal/notification/types.go` -- Subscription matching (expr evaluation)
+152
View File
@@ -0,0 +1,152 @@
---
name: bug-hunter
description: "Use this agent when you need to review recently written or modified code for bugs, logic errors, edge cases, and unexpected behavior. This includes both Go backend code and Vue/TypeScript frontend code. This agent should be used after writing new features, fixing bugs, or refactoring code to catch issues before they reach production.\\n\\nExamples:\\n\\n- User writes a new HTTP handler in Go:\\n user: \"I just added a new endpoint for container health checks\"\\n assistant: \"Let me review the new code for potential bugs and edge cases.\"\\n <uses Task tool to launch bug-hunter agent to review the recently changed files>\\n\\n- User implements a new Vue composable:\\n user: \"I created a new composable for managing WebSocket reconnection\"\\n assistant: \"I'll launch the bug hunter to review your new composable for edge cases and potential issues.\"\\n <uses Task tool to launch bug-hunter agent to analyze the composable>\\n\\n- User modifies log parsing logic:\\n user: \"I updated the event_generator.go to handle a new log format\"\\n assistant: \"Let me have the bug hunter review your changes to ensure all edge cases in log parsing are covered.\"\\n <uses Task tool to launch bug-hunter agent to review the modified parsing code>\\n\\n- After a chunk of code is written during a feature implementation:\\n assistant: \"I've finished implementing the notification dispatcher. Let me run the bug hunter to check for issues.\"\\n <uses Task tool to launch bug-hunter agent proactively to review the new code>"
model: opus
color: blue
memory: project
---
You are an elite bug-hunting code reviewer with deep expertise in both Go backend systems and Vue 3/TypeScript frontend applications. You have extensive experience finding subtle bugs, race conditions, nil pointer dereferences, unhandled edge cases, type safety issues, and logic errors that slip past typical review. You think adversarially — always asking "what could go wrong here?"
## Your Mission
Review recently written or modified code to find bugs, logic errors, and uncovered edge cases. You focus on code that could fail at runtime, produce incorrect results, or behave unexpectedly under specific conditions.
## Review Process
1. **Identify Changed/New Code**: Use git status, git diff, or examine the files the user points you to. Focus on recently written code, not the entire codebase.
2. **Analyze Each File Systematically**: For every file with changes, examine:
- Control flow paths (all branches of if/else, switch, select)
- Error handling (are errors checked? propagated correctly? logged?)
- Nil/undefined checks (pointer dereference in Go, optional chaining in TS)
- Concurrency safety (goroutine leaks, race conditions, channel operations)
- Resource cleanup (deferred closes, stream cleanup, event listener removal)
- Boundary conditions (empty arrays, zero values, max values, negative numbers)
- Type safety (type assertions in Go, type narrowing in TS)
- API contract adherence (correct HTTP status codes, proper SSE format, GraphQL schema alignment)
3. **Cross-Layer Analysis**: Check that frontend and backend changes are consistent:
- API request/response shapes match between Go handlers and TypeScript types
- SSE event names and payload structures align
- GraphQL schema, resolvers, and frontend queries are in sync
- WebSocket message formats match on both sides
## Go-Specific Bug Patterns to Check
- **Nil pointer dereference**: Especially after type assertions, map lookups, and interface conversions
- **Goroutine leaks**: Goroutines blocked on channels that are never closed or written to
- **Race conditions**: Shared state accessed from multiple goroutines without synchronization
- **Deferred closure in loops**: `defer` inside loops won't execute until function returns
- **Error shadowing**: Using `:=` that shadows an outer `err` variable
- **Slice/map initialization**: Operating on nil slices/maps (nil map write panics)
- **Context cancellation**: Not respecting context.Done() in long-running operations
- **HTTP response body leaks**: Not closing response bodies after HTTP calls
- **Integer overflow**: Especially in stats calculations with uint64
- **String/byte slice sharing**: Modifying a slice that shares underlying array
- **Channel operations on nil channels**: Blocking forever on nil channel send/receive
- **Mutex copy**: Passing sync.Mutex by value instead of pointer
## Vue/TypeScript-Specific Bug Patterns to Check
- **Reactive reference unwrapping**: Using `.value` correctly with `ref()` vs `reactive()`
- **Computed dependency tracking**: Missing reactive dependencies in computed properties
- **Watch cleanup**: Not cleaning up watchers, event listeners, or timers in `onUnmounted`
- **SSE/EventSource cleanup**: Connections not properly closed on component unmount
- **Array reactivity**: Using index assignment instead of reactive methods
- **Optional chaining gaps**: Accessing nested properties without null checks
- **Promise error handling**: Unhandled promise rejections, missing `.catch()` or try/catch
- **Type narrowing issues**: Assuming a type without proper guards
- **Stale closure references**: Callbacks capturing outdated reactive values
- **Memory leaks**: Growing arrays without bounds (check maxLogs enforcement, statsHistory rolling window)
- **Race conditions in async operations**: Component unmounted before async operation completes
- **Incorrect v-if/v-show usage**: Rendering components that depend on data not yet loaded
- **Event buffer overflow**: Not handling backpressure in SSE streams
## Project-Specific Concerns
- **EMA calculations**: Alpha=0.2 for stats smoothing — verify division by zero, NaN handling
- **Rolling window (300 items)**: Ensure proper eviction and no off-by-one errors
- **Log entry type discrimination**: `LogEntry.create()` must handle all `logEvent.t` values
- **Multi-host operations**: Container/host lookups via `FindContainer()`/`FindHost()` may return nil
- **Docker API version negotiation**: Calls may fail on older Docker versions
- **Protobuf serialization**: `FromProto()` methods must handle nil/empty fields
- **Authentication modes**: Code must work correctly in all three auth modes (none, simple, forward-proxy)
- **SSE buffering**: 250ms debounce with 1000ms max — verify timer cleanup
- **CPU normalization**: Division by `cpuLimit` or `nCPU` — check for zero values
## Output Format
Follow ultra-brief mode as specified in the project guidelines:
- Critical issues only (bugs, security, blockers)
- Brief bullet points, no lengthy explanations
- Skip verbose sections (no "Strengths", "Summary", etc.)
- Include file:line references when relevant
- Maximum ~10-15 lines per response
For each bug found, report:
- **File:line** — Brief description of the bug
- **Severity**: 🔴 Critical (will crash/corrupt) | 🟡 Warning (could fail under specific conditions) | 🟠 Edge case (uncovered scenario)
- One-line fix suggestion if obvious
If no bugs are found, say so concisely. Do not fabricate issues.
## Approach
1. First, determine what code was recently changed (check git diff or ask the user)
2. Read the changed files carefully
3. For each file, apply the relevant bug pattern checklist above
4. Cross-reference frontend and backend changes for consistency
5. Report findings in the ultra-brief format
Do NOT review code style, naming conventions, or suggest refactors unless they mask a bug. Focus exclusively on correctness.
**Update your agent memory** as you discover recurring bug patterns, common error-prone code paths, areas of the codebase with historical issues, and edge cases specific to this project's architecture. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
Examples of what to record:
- Recurring nil-check omissions in specific packages
- Components that frequently have cleanup issues
- API endpoints with known edge case gaps
- Stats calculation patterns that are error-prone
- Log parsing paths that have caused issues before
# Persistent Agent Memory
You have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/bug-hunter/`. Its contents persist across conversations.
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
Guidelines:
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
- Update or remove memories that turn out to be wrong or outdated
- Organize memory semantically by topic, not chronologically
- Use the Write and Edit tools to update your memory files
What to save:
- Stable patterns and conventions confirmed across multiple interactions
- Key architectural decisions, important file paths, and project structure
- User preferences for workflow, tools, and communication style
- Solutions to recurring problems and debugging insights
What NOT to save:
- Session-specific context (current task details, in-progress work, temporary state)
- Information that might be incomplete — verify against project docs before writing
- Anything that duplicates or contradicts existing CLAUDE.md instructions
- Speculative or unverified conclusions from reading a single file
Explicit user requests:
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
## MEMORY.md
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
+145
View File
@@ -0,0 +1,145 @@
---
name: go-perf-reviewer
description: "Use this agent when Go code has been written or modified and needs review for performance issues, inefficient patterns, or unnecessary resource loading. This agent focuses exclusively on Go backend code and should be triggered after changes to `.go` files.\\n\\nExamples:\\n\\n- user: \"I just added a new endpoint that fetches container stats\"\\n assistant: \"Let me use the go-perf-reviewer agent to check the new endpoint for performance issues.\"\\n <commentary>Since Go code was written for a new endpoint, use the Task tool to launch the go-perf-reviewer agent to review the changes for inefficient patterns.</commentary>\\n\\n- user: \"Can you review my changes to the Docker client wrapper?\"\\n assistant: \"I'll launch the go-perf-reviewer agent to analyze your Docker client changes for performance concerns.\"\\n <commentary>The user is asking for a review of Go code changes. Use the Task tool to launch the go-perf-reviewer agent to identify any performance anti-patterns.</commentary>\\n\\n- user: \"I refactored the log streaming pipeline\"\\n assistant: \"Let me run the go-perf-reviewer agent against your refactored streaming code to catch any performance regressions.\"\\n <commentary>Streaming code is performance-critical. Use the Task tool to launch the go-perf-reviewer agent to ensure the refactor doesn't introduce inefficiencies.</commentary>"
model: opus
color: green
memory: project
---
You are an expert Go performance engineer with deep knowledge of runtime internals, memory allocation patterns, garbage collector behavior, and idiomatic high-performance Go. You specialize in reviewing Go code for performance anti-patterns in long-running server applications that interact with Docker, gRPC, and streaming APIs.
**Core Philosophy**: Dozzle must be lean and lazy by default. It should never load, allocate, compute, or fetch anything until it is actually needed. Every byte of memory and every CPU cycle matters in a lightweight monitoring tool.
## Your Review Process
1. **Examine only the changed Go files**. Use available tools to read the recent changes or diffs.
2. **Identify specific performance issues** — do not comment on style, naming, or correctness unless it directly causes a performance problem.
3. **Provide actionable feedback** with file:line references and concrete fix suggestions.
## Patterns to Flag
### Memory & Allocation
- Unnecessary allocations in hot paths (loops, stream handlers, per-request code)
- Slice/map pre-allocation missing when size is known or estimable
- `append` in tight loops without pre-sized slices
- String concatenation with `+` instead of `strings.Builder` in loops
- Returning large structs by value instead of pointer when appropriate
- Unnecessary copies of large data (e.g., ranging over slice of structs by value)
- `[]byte``string` conversions that could be avoided
- Creating closures in loops that capture loop variables unnecessarily
- Allocating buffers per-call that could be pooled via `sync.Pool`
### Lazy Loading & Eager Initialization
- **Top priority**: Loading data, making API calls, or initializing resources before they are actually needed
- Fetching all containers/stats when only a subset is requested
- Initializing clients, connections, or caches at startup that may never be used
- Reading entire files/streams into memory when streaming/pagination would suffice
- Computing derived data eagerly when it could be computed on demand
- Pre-populating maps/caches with all possible entries instead of lazy-filling
### Concurrency & Goroutines
- Goroutine leaks: goroutines without proper cancellation via `context.Context`
- Missing `defer cancel()` after `context.WithCancel/WithTimeout`
- Unbounded goroutine spawning without semaphore/worker pool
- Channel misuse: unbuffered channels causing unnecessary blocking, or oversized buffered channels wasting memory
- Holding locks longer than necessary; lock contention in hot paths
- Using `sync.Mutex` where `sync.RWMutex` would reduce contention
### I/O & Streaming
- Not using `bufio.Reader`/`bufio.Writer` for I/O operations
- Reading entire HTTP response bodies into memory (`io.ReadAll`) when streaming is possible
- Not closing response bodies, readers, or connections (resource leaks)
- Blocking I/O without timeouts or context cancellation
- Serializing/deserializing JSON repeatedly when it could be done once
- Using `encoding/json` in ultra-hot paths where a faster serializer is warranted
### Docker/gRPC Specific
- Making redundant Docker API calls (e.g., inspecting a container multiple times)
- Not using Docker API filters to narrow results server-side
- Fetching all container logs when `tail` or `since` parameters should limit scope
- gRPC streams not properly drained or closed
- Creating new Docker/gRPC clients per request instead of reusing
### General Go Anti-Patterns
- `reflect` usage in hot paths
- `fmt.Sprintf` for simple string operations where direct concatenation suffices
- `interface{}` / `any` boxing causing heap escapes
- Unnecessary use of `defer` in tight loops (small but real overhead)
- `time.After` in select loops (creates new timer each iteration; use `time.NewTimer` + `Reset`)
- Regex compilation inside functions instead of package-level `var` with `regexp.MustCompile`
- Sorting large slices repeatedly instead of maintaining sorted order
## Output Format
Use ultra-brief mode as specified by the project:
- Critical performance issues only
- Brief bullet points with file:line references
- Concrete suggestion for each issue
- Maximum ~10-15 lines per response
- No praise sections, no summaries, no fluff
Example output:
```
- `internal/docker/client.go:142` — `io.ReadAll(resp.Body)` reads entire log stream into memory. Stream with `bufio.Scanner` instead.
- `internal/web/logs.go:87` — New `json.Encoder` created per log line in hot loop. Reuse encoder or use `sync.Pool`.
- `internal/support/docker/manager.go:53` — All hosts initialized eagerly at startup. Defer client creation until first access.
```
If no performance issues are found, state that clearly in one line.
**Update your agent memory** as you discover performance patterns, hot paths, allocation-heavy code paths, and architectural decisions in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
Examples of what to record:
- Hot paths identified (log streaming, stats collection, event processing)
- Existing `sync.Pool` usage or buffer reuse patterns
- Known allocation-heavy areas
- Docker API call patterns and caching strategies
- gRPC streaming patterns used in agent mode
- Areas where lazy loading is already implemented vs. missing
# Persistent Agent Memory
You have a persistent Persistent Agent Memory directory at `/Users/araminfar/Workspace/dozzle/.claude/agent-memory/go-perf-reviewer/`. Its contents persist across conversations.
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
Guidelines:
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
- Update or remove memories that turn out to be wrong or outdated
- Organize memory semantically by topic, not chronologically
- Use the Write and Edit tools to update your memory files
What to save:
- Stable patterns and conventions confirmed across multiple interactions
- Key architectural decisions, important file paths, and project structure
- User preferences for workflow, tools, and communication style
- Solutions to recurring problems and debugging insights
What NOT to save:
- Session-specific context (current task details, in-progress work, temporary state)
- Information that might be incomplete — verify against project docs before writing
- Anything that duplicates or contradicts existing CLAUDE.md instructions
- Speculative or unverified conclusions from reading a single file
Explicit user requests:
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project
## MEMORY.md
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
+34
View File
@@ -0,0 +1,34 @@
{
"permissions": {
"allow": [
"mcp__acp__Edit",
"mcp__acp__Bash",
"mcp__acp__Write",
"Bash(gh pr view:*)",
"Bash(gh api:*)",
"WebFetch(domain:github.com)",
"Bash(make test:*)",
"Bash(go test:*)",
"Bash(go run -e -c 'package main; import \\(\"\"fmt\"\"; \"\"golang.org/x/crypto/bcrypt\"\"\\); func main\\(\\) { h, _ := bcrypt.GenerateFromPassword\\([]byte\\(\"\"password\"\"\\), bcrypt.DefaultCost\\); fmt.Println\\(string\\(h\\)\\) }')",
"Bash(python3:*)",
"Bash(pnpm test:*)",
"Bash(node --version:*)",
"Bash(pnpm list:*)",
"Bash(git rebase:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git checkout:*)",
"Bash(pnpm build:*)",
"Bash(go build:*)",
"Bash(gh issue view:*)",
"Bash(xargs ls:*)",
"Bash(gh pr list:*)",
"Bash(docker run:*)",
"Bash(docker stats:*)",
"Bash(curl:*)",
"Bash(docker rm:*)",
"Bash(pnpm typecheck:*)",
"Bash(git push:*)"
]
}
}
+8 -7
View File
@@ -48,17 +48,18 @@ body:
id: environment
attributes:
label: 💻 Environment
description: Run `docker system info` and paste the output in the field below..
description: Run `docker system info` and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: 📜 Dozzle logs are required for debugging purposes. You may need to enable debug mode. See https://dozzle.dev/guide/debugging.
description: Drag log files or paste the logs into the text input below
validations:
required: true
- type: textarea
attributes:
label: 📸 If applicable, add screenshots to help explain your bug
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: 📜 If applicable, attach your Dozzle logs. You many need to enable debug mode. See https://dozzle.dev/guide/debugging.
description: Drag screenshots into the text input below
validations:
required: false
@@ -0,0 +1,30 @@
name: "\u2601\uFE0F Dozzle Cloud Bug Report"
labels: ["bug", "cloud"]
description: |
Use this template for issues related to Dozzle Cloud.
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` on it.
options:
- label: Completed
required: true
- type: input
attributes:
label: Dozzle version
description: The version of Dozzle connected to Dozzle Cloud.
validations:
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Screenshots or logs
description: Drag screenshots or paste logs into the text input below.
validations:
required: false
+57
View File
@@ -0,0 +1,57 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
+53
View File
@@ -0,0 +1,53 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions:
id-token: write
contents: read
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
+36 -21
View File
@@ -3,21 +3,27 @@ on:
tags:
- "v*"
name: Test and Release
permissions:
contents: read
jobs:
npm-test:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: latest
node-version: 24.13.1
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: latest
node-version: 24.13.1
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
@@ -29,12 +35,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.24.1
go-version: 1.26.0
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Protoc
uses: arduino/setup-protoc@v3
- name: Install gRPC and Go
@@ -48,16 +54,18 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: latest
node-version: 24.13.1
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: latest
node-version: 24.13.1
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
@@ -78,7 +86,7 @@ jobs:
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
@@ -88,22 +96,25 @@ jobs:
needs: [go-test, npm-test, int-test]
name: Release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to DockerHub
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the Container registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -121,7 +132,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v6.19.2
with:
push: true
context: .
@@ -135,13 +146,17 @@ jobs:
needs: [buildx]
name: Github Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
name: Install pnpm
- name: Install Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+10 -5
View File
@@ -6,6 +6,11 @@ on:
branches:
- master
name: Push container
permissions:
contents: read
packages: write
jobs:
buildx:
name: Push branches and PRs
@@ -13,20 +18,20 @@ jobs:
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to DockerHub
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the Container registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -39,7 +44,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v6.19.2
with:
context: .
push: true
+4 -4
View File
@@ -24,14 +24,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
- uses: pnpm/action-setup@v2
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: latest
node-version: 24.13.1
cache: pnpm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v5
@@ -42,7 +42,7 @@ jobs:
pnpm docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: docs/.vitepress/dist
+1 -1
View File
@@ -15,6 +15,6 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+31 -25
View File
@@ -6,21 +6,27 @@ on:
branches:
- master
name: Test
permissions:
contents: read
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: latest
node-version: 24.13.1
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: latest
node-version: 24.13.1
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
@@ -31,16 +37,14 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: latest
node-version: 24.13.1
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
@@ -52,12 +56,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24.1"
go-version: "1.26.0"
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Protoc
uses: arduino/setup-protoc@v3
with:
@@ -73,16 +77,16 @@ jobs:
name: Go Staticcheck
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.24.1"
go-version: "1.26.0"
check-latest: true
- name: Generate dependencies
run: make fake_assets shared_key.pem shared_cert.pem
- name: Stactic checker
uses: dominikh/staticcheck-action@v1.3.1
uses: dominikh/staticcheck-action@v1.4.0
with:
install-go: false
int-test:
@@ -90,16 +94,18 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v6
name: Install Node
with:
node-version: latest
node-version: 24.13.1
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: latest
node-version: 24.13.1
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
@@ -118,7 +124,7 @@ jobs:
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report
+2
View File
@@ -1,5 +1,7 @@
auto-imports.d.ts
components.d.ts
typed-router.d.ts
docs/.vitepress/cache
docs/.vitepress/dist
dist
assets/types/graphql.ts
+437
View File
@@ -0,0 +1,437 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Comment Style
**Always use ultra-brief mode for all PR reviews and responses.**
Format:
- Critical issues only (bugs, security, blockers)
- Brief bullet points, no lengthy explanations
- Skip verbose sections (no "Strengths", "Summary", etc.)
- Include file:line references when relevant
- Maximum ~10-15 lines per response
## Project Overview
Dozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:
- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)
- **Frontend**: Vue 3 (SPA with Vite, TypeScript)
The application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).
## Development Commands
### Setup
```bash
# Install dependencies
pnpm install
# Generate certificates and protobuf files
make generate
```
### Development
```bash
# Run full development environment (backend + frontend with hot reload)
make dev
# Alternative: Run backend and frontend separately
pnpm run watch:backend # Go backend with air (port 3100)
pnpm run watch:frontend # Vite dev server (port 3100)
# Run in agent mode for development
pnpm run agent:dev
```
### Building
```bash
# Build frontend assets
pnpm build
# or
make dist
# Build entire application (includes frontend build)
make build
# Build Docker image
make docker
```
### Testing
```bash
# Run Go tests
make test
# Run frontend tests (Vitest)
pnpm test
# Run in watch mode
TZ=UTC pnpm test --watch
# Type checking
pnpm typecheck
```
### Preview & Other
```bash
# Preview production build locally
pnpm preview
# or
make preview
# Run integration tests (Playwright)
make int
```
## Architecture
### Backend (Go)
The Go backend is organized into these key packages:
- **`internal/web/`** - HTTP server and routing layer
- Routes defined in `routes.go` using chi router
- WebSocket/SSE handlers for log streaming (`logs.go`)
- Authentication middleware and token management (`auth.go`)
- Container action handlers (`actions.go`)
- **`internal/docker/`** - Docker API client implementation
- `client.go`: Main Docker client wrapper with container operations
- `log_reader.go`: Streaming container logs
- `stats_collector.go`: Real-time container stats collection
- **`internal/agent/`** - gRPC agent for multi-host support
- Uses Protocol Buffers (protos defined in `protos/`)
- Enables distributed log collection across Docker hosts
- **`internal/k8s/`** - Kubernetes client support
- Alternative to Docker client for k8s deployments
- **`internal/support/`** - Support utilities
- `cli/`: Command-line argument parsing and validation
- `docker/`: Multi-host Docker management and Swarm support
- `container/`: Container service abstractions
- `web/`: Web service utilities
- **`internal/auth/`** - Authentication providers
- Simple file-based auth (`simple.go`)
- Forward proxy auth (`proxy.go`)
- Role-based authorization (`roles.go`)
- **`internal/container/`** - Container domain models and interfaces
- `event_generator.go`: Log parsing and grouping logic (multi-line, JSON detection)
- **`internal/notification/`** - Alert and notification system
- `manager.go`: Notification rule evaluation and dispatching
- `log_listener.go`: Log pattern matching for alerts
- `dispatcher/`: Notification channel implementations (email, webhook, etc.)
- **`graph/`** - GraphQL API layer
- `schema.graphqls`: GraphQL schema definitions
- `*.resolvers.go`: GraphQL resolver implementations
- **`main.go`** - Application entry point with mode switching (server/swarm/k8s/agent)
### Frontend (Vue 3)
The frontend uses file-based routing with these conventions:
- **`assets/pages/`** - File-based routes (unplugin-vue-router)
- `container/[id].vue`: Single container view
- `merged/[ids].vue`: Multi-container merged view
- `host/[id].vue`: Host-level logs
- `service/[name].vue`: Swarm service logs
- `stack/[name].vue`: Docker stack logs
- `group/[name].vue`: Custom grouped logs
- **`assets/components/`** - Vue components (auto-imported)
- `LogViewer/`: Core log viewing components
- `SimpleLogItem.vue`: Single-line log entries
- `ComplexLogItem.vue`: JSON/structured log entries
- `GroupedLogItem.vue`: Multi-line grouped log entries
- `ContainerEventLogItem.vue`: Container lifecycle events
- `SkippedEntriesLogItem.vue`: Placeholder for skipped logs
- `LoadMoreLogItem.vue`: Load more historical logs
- `ContainerViewer/`: Container-specific UI
- `common/`: Reusable UI components
- `BarChart.vue`: Lightweight bar chart with automatic downsampling
- `HostCard.vue`: Host overview card with metrics
- `MetricCard.vue`: Reusable metric display component
- `ContainerTable.vue`: Container table with historical stat visualization
- **`assets/stores/`** - Pinia stores (auto-imported)
- `config.ts`: App configuration and feature flags (injected from backend HTML, frozen immutable)
- `container.ts`: Container state management with EventSource streaming (`/api/events/stream`)
- `hosts.ts`: Multi-host state
- `settings.ts`: User preferences (localStorage-backed via profileStorage)
- `pinned.ts`: Pinned container logs for side-by-side viewing
- `swarm.ts`, `k8s.ts`: Deployment mode-specific state
- `announcements.ts`: Feature announcements
- **`assets/composable/`** - Vue composables (auto-imported)
- `eventStreams.ts`: SSE connection management with buffer-based flushing (250ms debounce)
- `historicalLogs.ts`: Historical log fetching
- `logContext.ts`: Log filtering and search context (provide/inject pattern)
- `scrollContext.ts`: Scroll state management (paused, progress, currentDate)
- `storage.ts`: LocalStorage abstractions with reactivity
- `visible.ts`: Log filtering by visible keys for complex logs
- `containerActions.ts`: Container control operations
- `duckdb.ts`: DuckDB WASM for SQL queries on logs
- **`assets/modules/`** - Vue plugins
- `router.ts`: Vue Router configuration
- `pinia.ts`: Pinia store setup
- `i18n.ts`: Internationalization
### Communication Flow
1. **Real-time Logs**: Frontend establishes SSE connections to `/api/hosts/{host}/containers/{id}/logs/stream`
2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events
3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events
4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)
5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`
6. **GraphQL**: POST to `/api/graphql` for queries and mutations (container metadata, historical logs, notifications)
### Build System
- **Frontend**: Vite builds to `dist/` with manifest
- **Backend**: Embeds `dist/` using Go embed directive
- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem
- **Makefile**: Orchestrates builds and dependency generation
## Important Development Notes
### Frontend
- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)
- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)
- Tailwind CSS with DaisyUI for styling
- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`
- **Log Entry Types**: Three types of log messages supported
- `SimpleLogEntry`: Single-line text logs (`string`)
- `ComplexLogEntry`: Structured JSON logs (`JSONObject`)
- `GroupedLogEntry`: Multi-line grouped logs (`string[]`)
- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages
- **Log Entry Factory Pattern**: Use `LogEntry.create(logEvent)` to instantiate the correct entry type based on `logEvent.t` field
- **EventSource Buffering**: Log streams use buffer-based flushing (250ms debounce, 1000ms max) to batch UI updates
- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)
- `BarChart.vue`: Self-contained bar chart with responsive downsampling
- Downsampling algorithm: Averages data into buckets based on available screen width
- All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)
### Backend
- The application uses Go 1.25+ with module support
- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)
- Protocol buffer generation happens via `go generate` directive in `main.go`
- Docker client uses API version negotiation for compatibility
- **GraphQL API**: Uses gqlgen with schema in `graph/schema.graphqls`, generated code in `graph/generated.go`
- Run `pnpm codegen` to regenerate GraphQL types
- Resolvers follow-schema layout in `graph/*.resolvers.go`
- **Service Layer Architecture**:
- `ClientService` interface abstracts Docker/K8s/Agent backends
- `MultiHostService` orchestrates multi-host operations
- `ClientManager` implementations: `RetriableClientManager` (server mode), `SwarmClientManager` (swarm mode)
### Authentication
- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)
- JWT tokens for simple auth with configurable TTL
- User file location: `./data/users.yml` or `./data/users.yaml`
### Testing
- Go tests use standard `testing` package with testify assertions
- Frontend uses Vitest with `@vue/test-utils`
- Integration tests with Playwright in `e2e/`
- Tests must run with `TZ=UTC` for consistent timestamps
### Container Stats & Metrics
- Stats are tracked using exponential moving average (EMA) with alpha=0.2
- History stored in rolling window (300 items max) via `useSimpleRefHistory`
- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)
- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)
- Stats visualization uses adaptive downsampling for performance
### Container Labels
- `dev.dozzle.name`: Custom container display name
- `dev.dozzle.group`: Group containers together
- Label-based filtering throughout the application
### Deployment Modes
- **Server mode** (default): Single or multi-host Docker monitoring
- Uses `RetriableClientManager` with local + remote agent clients
- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API
- Creates gRPC agent server on each node (port 7007)
- Uses `SwarmClientManager` for node discovery
- **K8s mode**: Pod log monitoring in Kubernetes cluster
- Implements `container.Client` interface via Kubernetes API
- **Agent mode**: Lightweight gRPC agent for remote log collection
- Run with `dozzle agent` or `pnpm run agent:dev`
- Listens on port 7007 with TLS certificate authentication
## Key Architectural Patterns
### Backend Abstraction Layers
The backend follows a clean layered architecture:
```
HTTP Handlers (internal/web)
HostService Interface (MultiHostService)
ClientService Interface (per host)
container.Client Interface
Implementation (DockerClient, K8sClient, AgentClient)
```
**When adding new container operations:**
1. Define method in `container.Client` interface (`internal/container/client.go`)
2. Implement in `internal/docker/client.go` (and `internal/k8s/client.go` if applicable)
3. Add wrapper method in `ClientService` interface (`internal/support/container/service.go`)
4. Add HTTP handler in `internal/web/` with appropriate route
### Frontend Data Flow
**Real-time Log Viewing:**
1. User navigates to `/container/{id}` route
2. Page component calls `useContainerStream(container)` composable
3. Composable creates EventSource connection to `/api/hosts/{host}/containers/{id}/logs/stream`
4. Backend streams `LogEvent` objects via SSE
5. Frontend buffers events (250ms debounce, max 1000ms)
6. Batched buffer flushes update reactive `messages` array
7. `LogViewer.vue` renders using appropriate component (`SimpleLogItem`, `ComplexLogItem`, `GroupedLogItem`)
8. When messages exceed `maxLogs` (400), oldest entries replaced or marked as `SkippedLogsEntry`
**Stats Streaming:**
1. `container.ts` store connects to `/api/events/stream` on app init
2. Backend multiplexes container events and stats into single SSE stream
3. `container-stat` events update `Container._stat` and append to `_statsHistory`
4. EMA calculation provides smoothed `movingAverageStat` (alpha=0.2)
5. `ContainerTable.vue` displays mini bar charts using `statsHistory` with downsampling
### Protocol Buffer Flow (Agent Mode)
1. Main server creates `agent.NewClient(endpoint, certs)` for each remote host
2. AgentClient implements `container.Client` interface
3. Method calls translate to gRPC requests defined in `protos/rpc.proto`
4. Remote agent receives gRPC call, delegates to local `DockerClient`
5. Streaming RPCs (logs, stats, events) use bidirectional channels
6. Responses converted back to domain models via `FromProto()` methods
### Log Parsing Pipeline
1. Docker API returns multiplexed stream (8-byte headers + payload)
2. `log_reader.go` parses headers, extracts stdout/stderr type
3. `event_generator.go` receives raw log lines
4. Detection logic identifies:
- JSON structure → `ComplexLogEntry`
- Multi-line patterns (stack traces) → `GroupedLogEntry`
- Single lines → `SimpleLogEntry`
5. Log level extraction via regex patterns
6. `LogEvent` serialized to JSON and sent via SSE
7. Frontend deserializes and renders with appropriate component
## Adding New Features
### Adding a New HTTP Route
1. Define route in `internal/web/routes.go` using chi router:
```go
r.Get("/api/custom-endpoint", h.customHandler)
```
2. Implement handler method in appropriate file (e.g., `actions.go`, `logs.go`)
3. Use `hostService` to find container/host via `FindContainer()` or `FindHost()`
4. Return JSON response or establish SSE/WebSocket stream
### Adding a New Log View Type
1. Create route file in `assets/pages/` (e.g., `custom/[id].vue`)
2. Create composable in `assets/composable/eventStreams.ts` (e.g., `useCustomStream()`)
3. Composable should:
- Build API URL with appropriate filters
- Create EventSource connection
- Handle buffering and message batching
- Return reactive `messages` array and control methods
4. Use `LogViewer.vue` component to render messages
5. Add backend API endpoint if needed (see above)
### Adding a New GraphQL Query/Mutation
1. Define in `graph/schema.graphqls`
2. Run `pnpm codegen` to regenerate types
3. Implement resolver in `graph/schema.resolvers.go`
4. Use `hostService` from resolver context to access backend services
5. Frontend calls via urql client (auto-imported via `@urql/vue`)
### Adding Container Stats/Metrics
1. Add field to `Stat` type in `internal/container/types.go`
2. Update `stats_collector.go` to extract metric from Docker API response
3. Add calculation logic in `docker/calculation.go` if needed
4. Ensure protobuf definition includes field in `protos/rpc.proto`
5. Frontend automatically receives updates via existing SSE stream
6. Update `Container` model in `assets/models/Container.ts` if UI needs access
### Working with Notifications/Alerts
**Backend** (`internal/notification/`):
- `manager.go`: Rule evaluation engine, manages alert state
- `log_listener.go`: Subscribes to container log streams, evaluates rules against incoming logs
- `types.go`: Alert rule definitions (log pattern matching, thresholds)
- `dispatcher/`: Notification channel implementations
**Frontend** (`assets/pages/notifications.vue`, `assets/components/Notification/`):
- `AlertForm.vue`, `DestinationForm.vue`: UI for creating rules
- Rules stored via GraphQL mutations
- Alert state displayed in notification cards
**Adding a new notification channel:**
1. Implement dispatcher interface in `internal/notification/dispatcher/`
2. Register in `manager.go` dispatcher factory
3. Add UI form in `assets/components/Notification/DestinationForm.vue`
4. Add GraphQL schema fields if needed
## Common Development Patterns
### Testing
- Always run Go tests with race detector: `go test -race`
- Frontend tests require `TZ=UTC` for timestamp consistency
- Integration tests use Playwright with `make int` (runs docker-compose setup)
- Use `testify/assert` for Go test assertions
### Hot Reload Development
- `make dev` runs both backend (air) and frontend (vite) with hot reload
- `DEV=true` disables embedded asset serving
- `LIVE_FS=true` serves assets from filesystem instead of embedded
- Backend changes trigger air restart automatically
- Frontend changes trigger vite HMR
### Debugging
- Backend logs: Set `--level debug` flag or `DOZZLE_LEVEL=debug` env var
- Frontend: Vue DevTools browser extension
- GraphQL: Use GraphQL Playground at `/api/graphql` (when enabled)
- SSE streams: Browser DevTools Network tab shows EventSource connections
+8 -7
View File
@@ -1,7 +1,7 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:23.10.0-alpine AS node
FROM --platform=$BUILDPLATFORM node:25.6.1-alpine AS node
RUN corepack enable
RUN npm install -g --force corepack && corepack enable
WORKDIR /build
@@ -22,10 +22,10 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
# install gRPC dependencies
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
RUN apk add --no-cache ca-certificates protoc protobuf-dev \
&& mkdir /dozzle \
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
@@ -50,17 +50,18 @@ COPY --from=node /build/dist ./dist
ARG TAG=dev
ARG TARGETOS TARGETARCH
# Generate protos
RUN go generate
# Generate protos and graphql
RUN go generate ./...
# Build binary
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
RUN GOEXPERIMENT=jsonv2 GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
RUN mkdir /data
FROM scratch
COPY --from=builder /data /data
COPY --from=builder /tmp /tmp
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /dozzle/dozzle /dozzle
+18 -16
View File
@@ -1,7 +1,4 @@
PROTO_DIR := protos
GEN_DIR := internal/agent/pb
PROTO_FILES := $(wildcard $(PROTO_DIR)/*.proto)
GEN_FILES := $(patsubst $(PROTO_DIR)/%.proto,$(GEN_DIR)/%.pb.go,$(PROTO_FILES))
.PHONY: clean
clean:
@@ -30,9 +27,11 @@ build: dist generate
.PHONY: docker
docker: shared_key.pem shared_cert.pem
@docker build --build-arg TAG=local -t amir20/dozzle .
@docker build --build-arg TAG=local -t amir20/dozzle:local .
generate: shared_key.pem shared_cert.pem $(GEN_FILES)
.PHONY: generate
generate: shared_key.pem shared_cert.pem
@go generate ./...
.PHONY: dev
dev: generate fake_assets
@@ -43,28 +42,31 @@ int:
docker compose up --build --force-recreate --exit-code-from playwright
shared_key.pem:
@openssl genpkey -algorithm RSA -out shared_key.pem -pkeyopt rsa_keygen_bits:2048
@openssl genpkey -algorithm Ed25519 -out shared_key.pem
shared_cert.pem: shared_key.pem
@openssl req -new -key shared_key.pem -out shared_request.csr -subj "/C=US/ST=California/L=San Francisco/O=Dozzle"
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 365
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 1825
@rm shared_request.csr
$(GEN_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
@go generate
.PHONY: push
push: docker
@docker tag amir20/dozzle:latest amir20/dozzle:local-test
@docker push amir20/dozzle:local-test
tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/air-verse/air@latest
run: docker
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:local
preview: build
pnpm preview
.PHONY: agent-reload
agent-reload: docker
@VM_NAME=$${VM_NAME:-dozzle-agent}; \
echo "📦 Loading image into VM $$VM_NAME..."; \
docker save amir20/dozzle:local | orb exec -m $$VM_NAME docker load; \
echo "🔄 Recreating agent..."; \
orb exec -m $$VM_NAME docker stop dozzle-agent || true; \
orb exec -m $$VM_NAME docker rm dozzle-agent || true; \
orb exec -m $$VM_NAME docker run -d --name dozzle-agent -p 7007:7007 -v /var/run/docker.sock:/var/run/docker.sock -v ~/dozzle-certs:/certs -v ~/dozzle-data:/data -e DOZZLE_LEVEL=debug amir20/dozzle:local agent --cert /certs/shared_cert.pem --key /certs/shared_key.pem; \
echo "✅ Agent reloaded"
+45 -56
View File
@@ -1,6 +1,10 @@
<p align="center">
<img src="assets/logo.svg" alt="Dozzle Logo" width="200"/>
</p>
# Dozzle - [dozzle.dev](https://dozzle.dev/)
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesnt store any log files. It is for live monitoring of your container logs only.
Dozzle is a lightweight, web-based application for monitoring Docker logs in real time. It doesn't store any log files—it's designed purely for live log viewing.
https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
@@ -9,20 +13,23 @@ https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
[![Docker Version](https://img.shields.io/docker/v/amir20/dozzle?sort=semver)](https://hub.docker.com/r/amir20/dozzle/)
![Test](https://github.com/amir20/dozzle/workflows/Test/badge.svg)
> [!NOTE]
> If you like Dozzle, check out [`dtop`](https://github.com/amir20/dtop), a top-like application for monitoring Docker containers. It integrates with Dozzle to link directly to container logs.
## Features
- Intelligent fuzzy search for container names 🤖
- Search logs using regex 🔦
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine) 📊
- Small memory footprint 🏎
- Intelligent fuzzy search for container names
- Search logs using regex
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine)
- Small memory footprint
- Split screen for viewing multiple logs
- Live stats with memory and CPU usage
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for proxy forward authorization 🚨
- [Swarm](https://dozzle.dev/guide/swarm-mode) mode support 🐳
- [Agent](https://dozzle.dev/guide/agent) mode for monitoring multiple Docker hosts 🕵️‍♂️
- Dark mode 🌙
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for forward proxy authorization
- [Swarm mode](https://dozzle.dev/guide/swarm-mode) support
- [Agent mode](https://dozzle.dev/guide/agent) for monitoring multiple Docker hosts
- Dark mode
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com), or [Kibana](https://www.elastic.co/products/kibana) are better suited for full search capabilities.
## Getting Started
@@ -32,13 +39,13 @@ Dozzle is a small container (7 MB compressed). Pull the latest release with:
### Running Dozzle
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
The simplest way to use Dozzle is to run the Docker container. Mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8080/](http://localhost:8080/).
Here is the Docker Compose file:
Here is a Docker Compose example:
services:
dozzle:
@@ -49,11 +56,11 @@ Here is the Docker Compose file:
ports:
- 8080:8080
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts) or common [questions](https://dozzle.dev/guide/faq) see documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts), or common [questions](https://dozzle.dev/guide/faq), see the documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
## Swarm Mode
Dozzle works with Docker Swarm mode. You can run Dozzle as a global service with:
Dozzle works with Docker Swarm. You can run Dozzle as a global service:
$ docker service create --name dozzle --env DOZZLE_MODE=swarm --mode global --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
@@ -61,7 +68,7 @@ See the [Swarm Mode](https://dozzle.dev/guide/swarm-mode) documentation for more
## Agent Mode
Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent mode with:
Dozzle can monitor multiple Docker hosts. Run Dozzle in agent mode with:
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent
@@ -69,19 +76,19 @@ See the [Agent Mode](https://dozzle.dev/guide/agent) documentation for more deta
## Technical Details
Dozzle users automatic API negotiation which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
Dozzle uses automatic API negotiation, which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
### Installation on podman
### Installation on Podman
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
By default, Podman doesn't have a background process, but you can enable the remote socket for Dozzle to work.
Verify first if your podman installation has enabled remote socket:
First, verify if your Podman installation has the remote socket enabled:
```
podman info
```
When you get under the key remote socket output like this, its already enabled:
If you see output like this under the remote socket key, it's already enabled:
```
remoteSocket:
@@ -89,59 +96,40 @@ When you get under the key remote socket output like this, its already enabled:
path: /run/user/1000/podman/podman.sock
```
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
If it's not enabled, follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
Once you have the podman remote socket you can run Dozzle on podman.
Once the Podman remote socket is enabled, you can run Dozzle:
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
```
Additionally you have to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
Additionally, you need to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker does, due to its daemonless architecture.
Under `/var/lib/docker` create a file named `engine-id`. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using `uuidgen > engine-id`. After that the file should have an identifier that looks like this: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
Create a file named `engine-id` under `/var/lib/docker`. On a system with Podman, you'll need to create the folder path as well. Place a UUID inside the file, for example using `uuidgen > engine-id`. The file should contain an identifier like: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
For more details check [Podman Infos](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it)
For more details, see [Podman Info](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it).
## Security
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
Dozzle supports file-based authentication and forward proxy authentication with tools like [Authelia](https://www.authelia.com/). See the documentation at https://dozzle.dev/guide/authentication.
## Analytics collected
## Analytics
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding. As a result, there is no time to do user studies of Dozzle. Analytics is collected to prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live using [ Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding, so there's no time for formal user studies. Analytics help prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live on the [Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
If you do not want to be tracked at all, see the `--no-analytics` flag below.
To disable analytics, use the `--no-analytics` flag.
## Environment variables and configuration
## Environment Variables and Configuration
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default |
| --------------------- | -------------------------- | -------------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | `false` |
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
Dozzle follows the [12-factor](https://12factor.net/) model. Configuration can be done via CLI flags or environment variables. See the documentation at [dozzle.dev/guide/supported-env-vars](https://dozzle.dev/guide/supported-env-vars) for more details.
## Support
There are many ways you can support Dozzle:
There are many ways to support Dozzle:
- Use it! Write about it! Star it! If you love Dozzle, drop me a line and tell me what you love.
- Blog about Dozzle to spread the word. If you are good at writing send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/)
- Blog about Dozzle to spread the word. If you're good at writing, send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/).
- Sponsor my work at https://www.buymeacoffee.com/amirraminfar
<a href="https://www.buymeacoffee.com/amirraminfar" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
@@ -154,8 +142,9 @@ There are many ways you can support Dozzle:
To build and test locally:
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
1. Install [Node.js](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
2. Install [Go](https://go.dev/doc/install).
3. Install tools with `make tools`.
4. Install node modules `pnpm install`.
5. Run `make dev` to start a development server with hot reload.
3. Install [protoc](https://grpc.io/docs/protoc-installation/).
4. Install Go tools with `go install tool`.
5. Install Node modules with `pnpm install`.
6. Run `make dev` to start a development server with hot reload.
+1 -1
View File
@@ -15,7 +15,7 @@ watchEffect(() => {
if (theme === "auto") {
theme = mode.value;
}
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", theme == "dark" ? "#121212" : "#3FA68F");
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", theme == "dark" ? "#121212" : "#F5F5F5");
document.documentElement.setAttribute("data-theme", theme);
});
</script>
+423 -367
View File
@@ -6,388 +6,422 @@
// biome-ignore lint: disable
export {}
declare global {
const DEFAULT_SETTINGS: typeof import('./stores/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const allLevels: typeof import('./composable/logContext')['allLevels']
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
const collapseNav: typeof import('./stores/settings')['collapseNav']
const compact: typeof import('./stores/settings')['compact']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const config: typeof import('./stores/config')['default']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createDrawer: typeof import('./composable/drawer')['createDrawer']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const dateLocale: typeof import('./stores/settings')['dateLocale']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const drawerContext: typeof import('./composable/drawer')['drawerContext']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const flattenJSON: typeof import('./utils/index')['flattenJSON']
const flattenJSONToMap: typeof import('./utils/index')['flattenJSONToMap']
const formatBytes: typeof import('./utils/index')['formatBytes']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDeep: typeof import('./utils/index')['getDeep']
const globalShowPopup: typeof import('./composable/popup')['globalShowPopup']
const h: typeof import('vue')['h']
const hashCode: typeof import('./utils/index')['hashCode']
const hourStyle: typeof import('./stores/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isMobile: typeof import('./composable/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./stores/settings')['lightTheme']
const locale: typeof import('./stores/settings')['locale']
const loggingContextKey: typeof import('./composable/logContext')['loggingContextKey']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const menuWidth: typeof import('./stores/settings')['menuWidth']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
const provideScrollContext: typeof import('./composable/scrollContext')['provideScrollContext']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const scrollContextKey: typeof import('./composable/scrollContext')['scrollContextKey']
const search: typeof import('./stores/settings')['search']
const sessionHost: typeof import('./composable/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composable/title')['setTitle']
const settings: typeof import('./stores/settings')['settings']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
const showStd: typeof import('./stores/settings')['showStd']
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
const size: typeof import('./stores/settings')['size']
const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars']
const softWrap: typeof import('./stores/settings')['softWrap']
const storeToRefs: typeof import('pinia')['storeToRefs']
const stripVersion: typeof import('./utils/index')['stripVersion']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAnnouncements: typeof import('./stores/announcements')['useAnnouncements']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useContainerActions: typeof import('./composable/containerActions')['useContainerActions']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useContainerStream: typeof import('./composable/eventStreams')['useContainerStream']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDrawer: typeof import('./composable/drawer')['useDrawer']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useDuckDB: typeof import('./composable/duckdb')['useDuckDB']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useExponentialMovingAverage: typeof import('./utils/index')['useExponentialMovingAverage']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGroupedStream: typeof import('./composable/eventStreams')['useGroupedStream']
const useHead: typeof import('@vueuse/head')['useHead']
const useHostStream: typeof import('./composable/eventStreams')['useHostStream']
const useHosts: typeof import('./stores/hosts')['useHosts']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLoggingContext: typeof import('./composable/logContext')['useLoggingContext']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMergedStream: typeof import('./composable/eventStreams')['useMergedStream']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePinnedLogsStore: typeof import('./stores/pinned')['usePinnedLogsStore']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProfileStorage: typeof import('./composable/profileStorage')['useProfileStorage']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollContext: typeof import('./composable/scrollContext')['useScrollContext']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
const useServiceStream: typeof import('./composable/eventStreams')['useServiceStream']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStackStream: typeof import('./composable/eventStreams')['useStackStream']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwarmStore: typeof import('./stores/swarm')['useSwarmStore']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToast: typeof import('./composable/toast')['useToast']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVisibleFilter: typeof import('./composable/visible')['useVisibleFilter']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
const withBase: typeof import('./stores/config')['withBase']
const DEFAULT_SETTINGS: typeof import('./stores/settings').DEFAULT_SETTINGS
const EffectScope: typeof import('vue').EffectScope
const K8sNamespace: typeof import('./stores/k8s').K8sNamespace
const K8sOwner: typeof import('./stores/k8s').K8sOwner
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const allLevels: typeof import('./composable/logContext').allLevels
const arrayEquals: typeof import('./utils/index').arrayEquals
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const automaticRedirect: typeof import('./stores/settings').automaticRedirect
const collapseNav: typeof import('./stores/settings').collapseNav
const compact: typeof import('./stores/settings').compact
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
const computedInject: typeof import('@vueuse/core').computedInject
const computedWithControl: typeof import('@vueuse/core').computedWithControl
const config: typeof import('./stores/config').default
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createContainerHints: typeof import('./composable/exprEditor').createContainerHints
const createDrawer: typeof import('./composable/drawer').createDrawer
const createEventHook: typeof import('@vueuse/core').createEventHook
const createExprEditor: typeof import('./composable/exprEditor').createExprEditor
const createGlobalState: typeof import('@vueuse/core').createGlobalState
const createInjectionState: typeof import('@vueuse/core').createInjectionState
const createLogHints: typeof import('./composable/exprEditor').createLogHints
const createMetricHints: typeof import('./composable/exprEditor').createMetricHints
const createPinia: typeof import('pinia').createPinia
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
const createRef: typeof import('@vueuse/core').createRef
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const createTemplateEditor: typeof import('./composable/templateEditor').createTemplateEditor
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const dateLocale: typeof import('./stores/settings').dateLocale
const debouncedRef: typeof import('@vueuse/core').debouncedRef
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const drawerContext: typeof import('./composable/drawer').drawerContext
const eagerComputed: typeof import('@vueuse/core').eagerComputed
const effectScope: typeof import('vue').effectScope
const extendRef: typeof import('@vueuse/core').extendRef
const flattenJSON: typeof import('./utils/index').flattenJSON
const flattenJSONToMap: typeof import('./utils/index').flattenJSONToMap
const formatBytes: typeof import('./utils/index').formatBytes
const formatDuration: typeof import('./utils/index').formatDuration
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const getDeep: typeof import('./utils/index').getDeep
const globalShowPopup: typeof import('./composable/popup').globalShowPopup
const groupContainers: typeof import('./stores/settings').groupContainers
const h: typeof import('vue').h
const hashCode: typeof import('./utils/index').hashCode
const hourStyle: typeof import('./stores/settings').hourStyle
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDefined: typeof import('@vueuse/core').isDefined
const isMobile: typeof import('./composable/media').isMobile
const isObject: typeof import('./utils/index').isObject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const lightTheme: typeof import('./stores/settings').lightTheme
const loadBetween: typeof import('./composable/eventStreams').loadBetween
const locale: typeof import('./stores/settings').locale
const loggingContextKey: typeof import('./composable/logContext').loggingContextKey
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const menuWidth: typeof import('./stores/settings').menuWidth
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onClickOutside: typeof import('@vueuse/core').onClickOutside
const onDeactivated: typeof import('vue').onDeactivated
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
const onLongPress: typeof import('@vueuse/core').onLongPress
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onStartTyping: typeof import('@vueuse/core').onStartTyping
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const persistentVisibleKeysForContainer: typeof import('./composable/storage').persistentVisibleKeysForContainer
const pinnedContainers: typeof import('./composable/storage').pinnedContainers
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const provideLoggingContext: typeof import('./composable/logContext').provideLoggingContext
const provideScrollContext: typeof import('./composable/scrollContext').provideScrollContext
const reactify: typeof import('@vueuse/core').reactify
const reactifyObject: typeof import('@vueuse/core').reactifyObject
const reactive: typeof import('vue').reactive
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
const reactivePick: typeof import('@vueuse/core').reactivePick
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refAutoReset: typeof import('@vueuse/core').refAutoReset
const refDebounced: typeof import('@vueuse/core').refDebounced
const refDefault: typeof import('@vueuse/core').refDefault
const refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const scrollContextKey: typeof import('./composable/scrollContext').scrollContextKey
const search: typeof import('./stores/settings').search
const sessionHost: typeof import('./composable/storage').sessionHost
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const setTitle: typeof import('./composable/title').setTitle
const settings: typeof import('./stores/settings').settings
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const showAllContainers: typeof import('./stores/settings').showAllContainers
const showStd: typeof import('./stores/settings').showStd
const showTimestamp: typeof import('./stores/settings').showTimestamp
const size: typeof import('./stores/settings').size
const smallerScrollbars: typeof import('./stores/settings').smallerScrollbars
const softWrap: typeof import('./stores/settings').softWrap
const storeToRefs: typeof import('pinia').storeToRefs
const stripVersion: typeof import('./utils/index').stripVersion
const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs
const templateRef: typeof import('@vueuse/core').templateRef
const throttledRef: typeof import('@vueuse/core').throttledRef
const throttledWatch: typeof import('@vueuse/core').throttledWatch
const toRaw: typeof import('vue').toRaw
const toReactive: typeof import('@vueuse/core').toReactive
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toRelativeTime: typeof import('./utils/index').toRelativeTime
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
const unref: typeof import('vue').unref
const unrefElement: typeof import('@vueuse/core').unrefElement
const until: typeof import('@vueuse/core').until
const useActiveElement: typeof import('@vueuse/core').useActiveElement
const useAlertForm: typeof import('./composable/alertForm').useAlertForm
const useAnimate: typeof import('@vueuse/core').useAnimate
const useAnnouncements: typeof import('./stores/announcements').useAnnouncements
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
const useArrayFind: typeof import('@vueuse/core').useArrayFind
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
const useArrayMap: typeof import('@vueuse/core').useArrayMap
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
const useArraySome: typeof import('@vueuse/core').useArraySome
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
const useAsyncState: typeof import('@vueuse/core').useAsyncState
const useAttrs: typeof import('vue').useAttrs
const useBase64: typeof import('@vueuse/core').useBase64
const useBattery: typeof import('@vueuse/core').useBattery
const useBluetooth: typeof import('@vueuse/core').useBluetooth
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
const useCached: typeof import('@vueuse/core').useCached
const useClipboard: typeof import('@vueuse/core').useClipboard
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
const useCloned: typeof import('@vueuse/core').useCloned
const useColorMode: typeof import('@vueuse/core').useColorMode
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
const useContainerActions: typeof import('./composable/containerActions').useContainerActions
const useContainerStore: typeof import('./stores/container').useContainerStore
const useContainerStream: typeof import('./composable/eventStreams').useContainerStream
const useCountdown: typeof import('@vueuse/core').useCountdown
const useCounter: typeof import('@vueuse/core').useCounter
const useCssModule: typeof import('vue').useCssModule
const useCssSupports: typeof import('@vueuse/core').useCssSupports
const useCssVar: typeof import('@vueuse/core').useCssVar
const useCssVars: typeof import('vue').useCssVars
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
const useCycleList: typeof import('@vueuse/core').useCycleList
const useDark: typeof import('@vueuse/core').useDark
const useDateFormat: typeof import('@vueuse/core').useDateFormat
const useDebounce: typeof import('@vueuse/core').useDebounce
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
const useDevicesList: typeof import('@vueuse/core').useDevicesList
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
const useDownloadUrl: typeof import('./composable/downloadUrl').useDownloadUrl
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDrawer: typeof import('./composable/drawer').useDrawer
const useDropZone: typeof import('@vueuse/core').useDropZone
const useDuckDB: typeof import('./composable/duckdb').useDuckDB
const useElementBounding: typeof import('@vueuse/core').useElementBounding
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
const useElementHover: typeof import('@vueuse/core').useElementHover
const useElementSize: typeof import('@vueuse/core').useElementSize
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
const useEventBus: typeof import('@vueuse/core').useEventBus
const useEventListener: typeof import('@vueuse/core').useEventListener
const useEventSource: typeof import('@vueuse/core').useEventSource
const useExponentialMovingAverage: typeof import('./utils/index').useExponentialMovingAverage
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
const useFavicon: typeof import('@vueuse/core').useFavicon
const useFetch: typeof import('@vueuse/core').useFetch
const useFileDialog: typeof import('@vueuse/core').useFileDialog
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
const useFocus: typeof import('@vueuse/core').useFocus
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
const useFps: typeof import('@vueuse/core').useFps
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useGroupedStream: typeof import('./composable/eventStreams').useGroupedStream
const useHead: typeof import('@vueuse/head').useHead
const useHistoricalContainerLog: typeof import('./composable/historicalLogs').useHistoricalContainerLog
const useHostStream: typeof import('./composable/eventStreams').useHostStream
const useHosts: typeof import('./stores/hosts').useHosts
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useK8sStore: typeof import('./stores/k8s').useK8sStore
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLink: typeof import('vue-router/auto').useLink
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useLoggingContext: typeof import('./composable/logContext').useLoggingContext
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
const useMediaControls: typeof import('@vueuse/core').useMediaControls
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
const useMemoize: typeof import('@vueuse/core').useMemoize
const useMemory: typeof import('@vueuse/core').useMemory
const useMergedStream: typeof import('./composable/eventStreams').useMergedStream
const useModel: typeof import('vue').useModel
const useMounted: typeof import('@vueuse/core').useMounted
const useMouse: typeof import('@vueuse/core').useMouse
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNamespaceStream: typeof import('./composable/eventStreams').useNamespaceStream
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork
const useNow: typeof import('@vueuse/core').useNow
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
const useOnline: typeof import('@vueuse/core').useOnline
const useOwnerStream: typeof import('./composable/eventStreams').useOwnerStream
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePinnedLogsStore: typeof import('./stores/pinned').usePinnedLogsStore
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
const usePrevious: typeof import('@vueuse/core').usePrevious
const useProfileStorage: typeof import('./composable/profileStorage').useProfileStorage
const useRafFn: typeof import('@vueuse/core').useRafFn
const useRefHistory: typeof import('@vueuse/core').useRefHistory
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
const useRoute: typeof import('vue-router/auto').useRoute
const useRouter: typeof import('vue-router/auto').useRouter
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
const useScriptTag: typeof import('@vueuse/core').useScriptTag
const useScroll: typeof import('@vueuse/core').useScroll
const useScrollContext: typeof import('./composable/scrollContext').useScrollContext
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSearchFilter: typeof import('./composable/search').useSearchFilter
const useSeoMeta: typeof import('@vueuse/head').useSeoMeta
const useServiceStream: typeof import('./composable/eventStreams').useServiceStream
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSimpleRefHistory: typeof import('./utils/index').useSimpleRefHistory
const useSlots: typeof import('vue').useSlots
const useSorted: typeof import('@vueuse/core').useSorted
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
const useStackStream: typeof import('./composable/eventStreams').useStackStream
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSupported: typeof import('@vueuse/core').useSupported
const useSwarmStore: typeof import('./stores/swarm').useSwarmStore
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
const useTextDirection: typeof import('@vueuse/core').useTextDirection
const useTextSelection: typeof import('@vueuse/core').useTextSelection
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
const useThrottle: typeof import('@vueuse/core').useThrottle
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
const useTimeout: typeof import('@vueuse/core').useTimeout
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
const useTimestamp: typeof import('@vueuse/core').useTimestamp
const useTitle: typeof import('@vueuse/core').useTitle
const useToNumber: typeof import('@vueuse/core').useToNumber
const useToString: typeof import('@vueuse/core').useToString
const useToast: typeof import('./composable/toast').useToast
const useToggle: typeof import('@vueuse/core').useToggle
const useTransition: typeof import('@vueuse/core').useTransition
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
const useUserMedia: typeof import('@vueuse/core').useUserMedia
const useVModel: typeof import('@vueuse/core').useVModel
const useVModels: typeof import('@vueuse/core').useVModels
const useVibrate: typeof import('@vueuse/core').useVibrate
const useVirtualList: typeof import('@vueuse/core').useVirtualList
const useVisibleFilter: typeof import('./composable/visible').useVisibleFilter
const useWakeLock: typeof import('@vueuse/core').useWakeLock
const useWebNotification: typeof import('@vueuse/core').useWebNotification
const useWebSocket: typeof import('@vueuse/core').useWebSocket
const useWebWorker: typeof import('@vueuse/core').useWebWorker
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
const useWindowSize: typeof import('@vueuse/core').useWindowSize
const watch: typeof import('vue').watch
const watchArray: typeof import('@vueuse/core').watchArray
const watchAtMost: typeof import('@vueuse/core').watchAtMost
const watchDebounced: typeof import('@vueuse/core').watchDebounced
const watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
const watchOnce: typeof import('@vueuse/core').watchOnce
const watchPausable: typeof import('@vueuse/core').watchPausable
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const watchThrottled: typeof import('@vueuse/core').watchThrottled
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
const withBase: typeof import('./stores/config').withBase
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { AlertFormOptions, ContainerResult } from './composable/alertForm'
import('./composable/alertForm')
// @ts-ignore
export type { DrawerWidth } from './composable/drawer'
import('./composable/drawer')
// @ts-ignore
export type { LogStreamSource } from './composable/eventStreams'
import('./composable/eventStreams')
// @ts-ignore
export type { ExprEditorOptions } from './composable/exprEditor'
import('./composable/exprEditor')
// @ts-ignore
export type { TemplateEditorOptions } from './composable/templateEditor'
import('./composable/templateEditor')
// @ts-ignore
export type { Config, Profile } from './stores/config'
import('./stores/config')
// @ts-ignore
export type { Host } from './stores/hosts'
import('./stores/hosts')
// @ts-ignore
export type { K8sNamespace, K8sOwner } from './stores/k8s'
import('./stores/k8s')
// @ts-ignore
export type { Settings } from './stores/settings'
import('./stores/settings')
}
@@ -399,6 +433,8 @@ declare module 'vue' {
interface ComponentCustomProperties {
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly K8sNamespace: UnwrapRef<typeof import('./stores/k8s')['K8sNamespace']>
readonly K8sOwner: UnwrapRef<typeof import('./stores/k8s')['K8sOwner']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly allLevels: UnwrapRef<typeof import('./composable/logContext')['allLevels']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
@@ -416,15 +452,20 @@ declare module 'vue' {
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createContainerHints: UnwrapRef<typeof import('./composable/exprEditor')['createContainerHints']>
readonly createDrawer: UnwrapRef<typeof import('./composable/drawer')['createDrawer']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createExprEditor: UnwrapRef<typeof import('./composable/exprEditor')['createExprEditor']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createLogHints: UnwrapRef<typeof import('./composable/exprEditor')['createLogHints']>
readonly createMetricHints: UnwrapRef<typeof import('./composable/exprEditor')['createMetricHints']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplateEditor: UnwrapRef<typeof import('./composable/templateEditor')['createTemplateEditor']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
@@ -441,11 +482,14 @@ declare module 'vue' {
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly formatDuration: UnwrapRef<typeof import('./utils/index')['formatDuration']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly groupContainers: UnwrapRef<typeof import('./stores/settings')['groupContainers']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hashCode: UnwrapRef<typeof import('./utils/index')['hashCode']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
@@ -459,7 +503,9 @@ declare module 'vue' {
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly loadBetween: UnwrapRef<typeof import('./composable/eventStreams')['loadBetween']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly loggingContextKey: UnwrapRef<typeof import('./composable/logContext')['loggingContextKey']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
@@ -510,11 +556,11 @@ declare module 'vue' {
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
@@ -542,6 +588,7 @@ declare module 'vue' {
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toRelativeTime: UnwrapRef<typeof import('./utils/index')['toRelativeTime']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
@@ -553,6 +600,7 @@ declare module 'vue' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAlertForm: UnwrapRef<typeof import('./composable/alertForm')['useAlertForm']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAnnouncements: UnwrapRef<typeof import('./stores/announcements')['useAnnouncements']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
@@ -588,6 +636,7 @@ declare module 'vue' {
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssSupports: UnwrapRef<typeof import('@vueuse/core')['useCssSupports']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
@@ -603,6 +652,7 @@ declare module 'vue' {
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDownloadUrl: UnwrapRef<typeof import('./composable/downloadUrl')['useDownloadUrl']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
@@ -629,6 +679,7 @@ declare module 'vue' {
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGroupedStream: UnwrapRef<typeof import('./composable/eventStreams')['useGroupedStream']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHistoricalContainerLog: UnwrapRef<typeof import('./composable/historicalLogs')['useHistoricalContainerLog']>
readonly useHostStream: UnwrapRef<typeof import('./composable/eventStreams')['useHostStream']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
@@ -639,8 +690,10 @@ declare module 'vue' {
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useK8sStore: UnwrapRef<typeof import('./stores/k8s')['useK8sStore']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
@@ -656,12 +709,14 @@ declare module 'vue' {
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNamespaceStream: UnwrapRef<typeof import('./composable/eventStreams')['useNamespaceStream']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly useOwnerStream: UnwrapRef<typeof import('./composable/eventStreams')['useOwnerStream']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
@@ -682,8 +737,8 @@ declare module 'vue' {
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
@@ -718,6 +773,7 @@ declare module 'vue' {
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
+53 -8
View File
@@ -1,17 +1,22 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AlertCard: typeof import('./components/Notification/AlertCard.vue')['default']
AlertForm: typeof import('./components/Notification/AlertForm.vue')['default']
Announcements: typeof import('./components/Announcements.vue')['default']
BarChart: typeof import('./components/BarChart.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:copyFile': typeof import('~icons/carbon/copy-file')['default']
'Carbon:information': typeof import('~icons/carbon/information')['default']
'Carbon:logoKubernetes': typeof import('~icons/carbon/logo-kubernetes')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
@@ -27,67 +32,106 @@ declare module 'vue' {
'Cil:circle': typeof import('~icons/cil/circle')['default']
'Cil:columns': typeof import('~icons/cil/columns')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
CloudDestinationForm: typeof import('./components/Notification/CloudDestinationForm.vue')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
ContainerDropdown: typeof import('./components/ContainerDropdown.vue')['default']
ContainerEventLogItem: typeof import('./components/LogViewer/ContainerEventLogItem.vue')['default']
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
ContainerPopup: typeof import('./components/ContainerPopup.vue')['default']
ContainerStatCell: typeof import('./components/ContainerStatCell.vue')['default']
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
DateTime: typeof import('./components/common/DateTime.vue')['default']
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
DestinationCard: typeof import('./components/Notification/DestinationCard.vue')['default']
DestinationForm: typeof import('./components/Notification/DestinationForm.vue')['default']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
GroupedLogItem: typeof import('./components/LogViewer/GroupedLogItem.vue')['default']
GroupMenu: typeof import('./components/GroupMenu.vue')['default']
HistoricalContainerLog: typeof import('./components/ContainerViewer/HistoricalContainerLog.vue')['default']
HostCard: typeof import('./components/HostCard.vue')['default']
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
HostList: typeof import('./components/HostList.vue')['default']
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
HostMenu: typeof import('./components/HostMenu.vue')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
'Ion:ellipsisVertical': typeof import('~icons/ion/ellipsis-vertical')['default']
K8sMenu: typeof import('./components/K8sMenu.vue')['default']
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
Links: typeof import('./components/Links.vue')['default']
LoadMoreLogItem: typeof import('./components/LogViewer/LoadMoreLogItem.vue')['default']
LogActions: typeof import('./components/LogViewer/LogActions.vue')['default']
LogAlertFields: typeof import('./components/Notification/LogAlertFields.vue')['default']
LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
LogItem: typeof import('./components/LogViewer/LogItem.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogList: typeof import('./components/LogViewer/LogList.vue')['default']
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
'MaterialSymbols:codeBlocksRounded': typeof import('~icons/material-symbols/code-blocks-rounded')['default']
'MaterialSymbols:contentCopy': typeof import('~icons/material-symbols/content-copy')['default']
'MaterialSymbols:eyeTracking': typeof import('~icons/material-symbols/eye-tracking')['default']
'MaterialSymbols:link': typeof import('~icons/material-symbols/link')['default']
'MaterialSymbols:logout': typeof import('~icons/material-symbols/logout')['default']
'MaterialSymbols:person': typeof import('~icons/material-symbols/person')['default']
'MaterialSymbols:terminal': typeof import('~icons/material-symbols/terminal')['default']
'MaterialSymbolsLight:collapseAll': typeof import('~icons/material-symbols-light/collapse-all')['default']
'Mdi:account': typeof import('~icons/mdi/account')['default']
'Mdi:alert': typeof import('~icons/mdi/alert')['default']
'Mdi:alertCircle': typeof import('~icons/mdi/alert-circle')['default']
'Mdi:alertOutline': typeof import('~icons/mdi/alert-outline')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
'Mdi:bell': typeof import('~icons/mdi/bell')['default']
'Mdi:chartLine': typeof import('~icons/mdi/chart-line')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
'Mdi:checkCircle': typeof import('~icons/mdi/check-circle')['default']
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-down')['default']
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:cloud': typeof import('~icons/mdi/cloud')['default']
'Mdi:cloudOutline': typeof import('~icons/mdi/cloud-outline')['default']
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
'Mdi:github': typeof import('~icons/mdi/github')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
'Mdi:key': typeof import('~icons/mdi/key')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
'Mdi:linkVariant': typeof import('~icons/mdi/link-variant')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:pencilOutline': typeof import('~icons/mdi/pencil-outline')['default']
'Mdi:plus': typeof import('~icons/mdi/plus')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
'Mdi:textBoxOutline': typeof import('~icons/mdi/text-box-outline')['default']
'Mdi:trashCanOutline': typeof import('~icons/mdi/trash-can-outline')['default']
'Mdi:webhook': typeof import('~icons/mdi/webhook')['default']
MetricAlertFields: typeof import('./components/Notification/MetricAlertFields.vue')['default']
MetricCard: typeof import('./components/MetricCard.vue')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default']
MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default']
NamespaceLog: typeof import('./components/K8sViewer/NamespaceLog.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
OwnerLog: typeof import('./components/K8sViewer/OwnerLog.vue')['default']
PageWithLinks: typeof import('./components/PageWithLinks.vue')['default']
'Ph:arrowsMerge': typeof import('~icons/ph/arrows-merge')['default']
'Ph:boundingBoxFill': typeof import('~icons/ph/bounding-box-fill')['default']
@@ -95,15 +139,16 @@ declare module 'vue' {
'Ph:command': typeof import('~icons/ph/command')['default']
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
'Ph:cpu': typeof import('~icons/ph/cpu')['default']
'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default']
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:memory': typeof import('~icons/ph/memory')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
PhArrowDown: typeof import('~icons/ph/arrow-down')['default']
PhArrowUp: typeof import('~icons/ph/arrow-up')['default']
Popup: typeof import('./components/Popup.vue')['default']
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
@@ -120,7 +165,6 @@ declare module 'vue' {
SQLTable: typeof import('./components/LogViewer/SQLTable.vue')['default']
StackLog: typeof import('./components/StackViewer/StackLog.vue')['default']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
SwarmMenu: typeof import('./components/SwarmMenu.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
Terminal: typeof import('./components/Terminal.vue')['default']
@@ -128,6 +172,7 @@ declare module 'vue' {
ToastModal: typeof import('./components/common/ToastModal.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default']
WebhookDestinationForm: typeof import('./components/Notification/WebhookDestinationForm.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
+8 -4
View File
@@ -1,5 +1,9 @@
<template>
<Dropdown class="dropdown-end" @closed="releaseSeen = mostRecent?.tag ?? config.version">
<Dropdown
class="dropdown-end"
@click="config.releaseCheckMode === 'manual' && fetchReleases()"
@closed="releaseSeen = mostRecent?.tag ?? config.version"
>
<template #trigger>
<mdi:announcement class="size-6 -rotate-12" />
<template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag">
@@ -22,7 +26,7 @@
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
</div>
<div class="text-base-content/80 text-sm">
{{ release.body }}
@@ -39,7 +43,7 @@
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.latest">
{{ $t("releases.latest") }}
</Tag>
@@ -63,7 +67,7 @@
<script setup lang="ts">
import { useAnnouncements } from "@/stores/announcements";
const { announcements, mostRecent } = useAnnouncements();
const { announcements, mostRecent, fetchReleases } = useAnnouncements();
const { t } = useI18n();
const releaseSeen = useProfileStorage("releaseSeen", config.version);
+108
View File
@@ -0,0 +1,108 @@
<template>
<div ref="chartContainer" class="flex items-end gap-[2px]" @mousemove="onContainerHover">
<div
v-for="(dataPoint, i) in downsampledData"
:key="i"
class="bar min-h-px flex-1 rounded-t-sm"
:class="barClass"
:style="{ '--height': `${Math.min(dataPoint, 100)}%` }"
></div>
</div>
</template>
<style scoped>
.bar {
height: var(--height);
will-change: height;
contain: layout;
}
</style>
<script setup lang="ts">
const { chartData, barClass = "" } = defineProps<{
chartData: number[];
barClass?: string;
}>();
const hoverIndex = defineEmit<[startIndex: number, endIndex: number]>();
const chartContainer = ref<HTMLElement | null>(null);
const { width } = useElementSize(chartContainer);
const BAR_WIDTH = 3;
const GAP = 2;
const availableBars = computed(() => Math.floor(width.value / (BAR_WIDTH + GAP)));
const bucketSize = computed(() => Math.ceil(chartData.length / availableBars.value));
const downsampledData = ref<number[]>([]);
const changeCounter = ref(-1);
// Watch chartData changes
watch(
() => chartData,
() => {
// If changeCounter is -1, it means this is the first time the data is loaded
if (changeCounter.value === -1) {
recalculate();
}
changeCounter.value++;
if (changeCounter.value >= bucketSize.value) {
// Recalculate when counter reaches bucket size
recalculate();
changeCounter.value = 0;
}
},
);
// Recalculate when width changes
watch([availableBars, bucketSize], () => {
recalculate();
changeCounter.value = -1;
});
function recalculate() {
if (chartData.length <= availableBars.value || availableBars.value === 0) {
downsampledData.value = [...chartData];
return;
}
const size = bucketSize.value;
const result = [];
// Create complete buckets
const numCompleteBuckets = Math.floor(chartData.length / size);
for (let i = 0; i < numCompleteBuckets; i++) {
const start = i * size;
const end = start + size;
const bucket = chartData.slice(start, end);
const avg = bucket.reduce((sum, val) => sum + val, 0) / bucket.length;
result.push(avg);
}
// Show only the last N bars that fit on screen
downsampledData.value = result.slice(-availableBars.value);
}
function onContainerHover(event: MouseEvent) {
if (!chartContainer.value) return;
const rect = chartContainer.value.getBoundingClientRect();
const x = event.clientX - rect.left;
// Calculate which bar the mouse is over based on position
const barWidth = width.value / downsampledData.value.length;
const index = Math.floor(x / barWidth);
// Ensure index is within bounds
if (index < 0 || index >= downsampledData.value.length) return;
// Map downsampled index back to original data index range
const numCompleteBuckets = Math.floor(chartData.length / bucketSize.value);
const offset = Math.max(0, numCompleteBuckets - availableBars.value);
const startIndex = (offset + index) * bucketSize.value;
const endIndex = Math.min(startIndex + bucketSize.value - 1, chartData.length - 1);
hoverIndex(startIndex, endIndex);
}
</script>
+24
View File
@@ -0,0 +1,24 @@
<template>
<div class="dropdown">
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm"><slot /> <carbon:caret-down /></button>
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 border-base-content/20 border shadow-sm">
<li v-for="other in containers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }" class="text-nowrap">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
{{ other.name }}
<div v-if="other.state === 'running'">running</div>
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { type Container } from "@/models/Container";
const { containers } = defineProps<{
containers: Container[];
}>();
</script>
+28 -24
View File
@@ -1,28 +1,32 @@
<template>
<div>
<span class="font-light capitalize"> STATE </span>
<span class="font-semibold uppercase"> {{ container.state }} </span>
</div>
<div v-if="container.startedAt.getFullYear() > 0">
<span class="font-light capitalize"> STARTED </span>
<span class="font-semibold">
<DistanceTime :date="container.startedAt" strict />
</span>
</div>
<div v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
<span class="font-light capitalize"> FINISHED </span>
<span class="font-semibold">
<DistanceTime :date="container.finishedAt" strict />
</span>
</div>
<div v-if="container.state == 'running'">
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
</div>
<div v-if="container.state == 'running'">
<span class="font-light capitalize"> MEM </span>
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
<table class="w-full border-separate border-spacing-x-1">
<tbody>
<tr>
<th class="text-right font-light capitalize">STATE</th>
<td class="font-semibold uppercase">{{ container.state }}</td>
</tr>
<tr v-if="container.startedAt.getFullYear() > 0">
<th class="text-right font-light capitalize">STARTED</th>
<td class="font-semibold">
<RelativeTime :date="container.startedAt" />
</td>
</tr>
<tr v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
<th class="text-right font-light capitalize">FINISHED</th>
<td class="font-semibold">
<RelativeTime :date="container.finishedAt" />
</td>
</tr>
<tr v-if="container.state == 'running'">
<th class="text-right font-light capitalize">Load</th>
<td class="font-semibold">{{ container.stat.cpu.toFixed(2) }}%</td>
</tr>
<tr v-if="container.state == 'running'">
<th class="text-right font-light capitalize">MEM</th>
<td class="font-semibold">{{ formatBytes(container.stat.memoryUsage) }}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
+55
View File
@@ -0,0 +1,55 @@
<template>
<div class="flex flex-row items-center gap-2">
<BarChart class="h-4 flex-1" :chart-data="chartData" :bar-class="barClass" />
<span class="w-fit text-right text-sm">{{ displayValue }}</span>
</div>
</template>
<script setup lang="ts">
import type { Container } from "@/models/Container";
import type { Host } from "@/stores/hosts";
const { container, type, host } = defineProps<{
container: Container;
type: "cpu" | "mem";
host: Host;
}>();
function totalCores(): number {
if (container.cpuLimit && container.cpuLimit > 0) {
return 1;
}
return host.nCPU ?? 1;
}
const chartData = computed(() => {
if (type === "cpu") {
const cores = totalCores();
return container.statsHistory.map((stat) => Math.min(stat.cpu / cores, 100));
}
return container.statsHistory.map((stat) => Math.min(stat.memory, 100));
});
const averageValue = computed(() => {
if (type === "cpu") {
const cores = totalCores();
return Math.min(container.movingAverage.cpu / cores, 100);
}
return container.movingAverage.memory;
});
const displayValue = computed(() => {
if (type === "cpu") {
return `${averageValue.value.toFixed(0)}%`;
}
return formatBytes(container.movingAverage.memoryUsage);
});
const barClass = computed(() => {
const value = averageValue.value;
if (value <= 50) return "bg-success";
if (value <= 70) return "bg-secondary";
if (value <= 90) return "bg-warning";
return "bg-error";
});
</script>
+119 -110
View File
@@ -1,112 +1,110 @@
!
<template>
<div class="flex flex-row">
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
aria-label="Show All"
v-model="selectedHost"
:value="null"
/>
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
:aria-label="host.name"
v-for="host in hosts"
:value="host.id"
:key="host.id"
<div class="flex flex-col gap-4">
<div class="flex flex-row">
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
aria-label="Show All"
v-model="selectedHost"
:value="null"
/>
<input
type="radio"
name="host"
role="tab"
class="tab rounded-sm!"
:aria-label="host.name"
v-for="host in hosts"
:value="host.id"
:key="host.id"
v-model="selectedHost"
/>
</div>
<DropdownMenu
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<DropdownMenu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<div class="rounded-box border-base-content/10 overflow-x-auto border">
<table class="table-md md:table-lg table-zebra table">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="[value.customClass, { 'selected-sort': key === sortField }]"
v-show="isVisible(key)"
>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
</tr>
</thead>
<tbody class="bg-base-300/30">
<tr v-for="container in paginated" :key="container.id" v-memo="[container.id]" class="hover:bg-base-100/80!">
<td v-if="isVisible('name')" class="max-w-80 truncate">
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<RelativeTime :date="container.created" />
</td>
<td v-if="isVisible('cpu')">
<ContainerStatCell :container="container" type="cpu" :host="hosts[container.host]" />
</td>
<td v-if="isVisible('mem')">
<ContainerStatCell :container="container" type="mem" :host="hosts[container.host]" />
</td>
</tr>
</tbody>
</table>
</div>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated && totalPages <= 15">
<input
class="btn btn-square join-item"
type="radio"
v-model="currentPage"
:aria-label="`${i}`"
:value="i"
v-for="i in totalPages"
/>
</nav>
<DropdownMenu
v-else-if="isPaginated"
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<DropdownMenu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<table class="table-lg bg-base-200 table">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="container in paginated" :key="container.id">
<td v-if="isVisible('name')">
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</td>
<td v-if="isVisible('cpu')">
<div class="flex flex-row items-center gap-1">
<progress
class="progress progress-primary"
:value="Math.min(container.movingAverage.cpu, 100)"
:max="100"
></progress>
<span class="text-sm">{{ container.movingAverage.cpu.toFixed(0) }}%</span>
</div>
</td>
<td v-if="isVisible('mem')">
<div class="flex flex-row items-center gap-1">
<progress class="progress progress-primary" :value="container.movingAverage.memory" max="100"></progress>
<span class="text-sm">{{ container.movingAverage.memory.toFixed(0) }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<input
class="btn btn-square join-item"
type="radio"
v-model="currentPage"
:aria-label="`${i}`"
:value="i"
v-for="i in totalPages"
:options="Array.from({ length: totalPages }, (_, i) => ({ label: `${i + 1}`, value: i + 1 }))"
/>
</nav>
</div>
</div>
</template>
@@ -117,7 +115,15 @@ import { toRefs } from "@vueuse/core";
const { hosts } = useHosts();
const selectedHost = ref(null);
const fields = {
const fields: Record<
string,
{
label: string;
sortFunc: (a: Container, b: Container) => number;
mobileVisible: boolean;
customClass?: string;
}
> = {
name: {
label: "label.container-name",
sortFunc: (a: Container, b: Container) => a.name.localeCompare(b.name) * direction.value,
@@ -127,26 +133,32 @@ const fields = {
label: "label.host",
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
mobileVisible: false,
customClass: "w-1",
},
state: {
label: "label.status",
sortFunc: (a: Container, b: Container) => a.state.localeCompare(b.state) * direction.value,
mobileVisible: false,
customClass: "w-1",
},
created: {
label: "label.created",
sortFunc: (a: Container, b: Container) => (a.created.getTime() - b.created.getTime()) * direction.value,
mobileVisible: true,
customClass: "w-1",
},
cpu: {
label: "label.avg-cpu",
sortFunc: (a: Container, b: Container) => (a.movingAverage.cpu - b.movingAverage.cpu) * direction.value,
mobileVisible: false,
customClass: "min-w-48",
},
mem: {
label: "label.avg-mem",
sortFunc: (a: Container, b: Container) => (a.movingAverage.memory - b.movingAverage.memory) * direction.value,
sortFunc: (a: Container, b: Container) =>
(a.movingAverage.memoryUsage - b.movingAverage.memoryUsage) * direction.value,
mobileVisible: false,
customClass: "min-w-48",
},
};
@@ -159,10 +171,10 @@ const perPage = useStorage("DOZZLE_TABLE_PAGE_SIZE", 15);
const pageSizes = [15, 30, 50, 100];
const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CONTAINERS_SORT", {
column: "created",
direction: -1,
column: "created" as keys,
direction: -1 as 1 | -1,
});
const { column: sortField, direction } = toRefs(storage);
const { column: sortField, direction } = toRefs(storage.value);
const counter = useInterval(10000);
const filteredContainers = computed(() =>
containers.filter((c) => selectedHost.value === null || c.host === selectedHost.value),
@@ -197,7 +209,7 @@ function isVisible(field: keys) {
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
[data-icon] {
display: none;
@@ -208,7 +220,7 @@ function isVisible(field: keys) {
}
th {
@apply border-base-100 border-b-2;
@apply border-base-200 border-b-2;
&.selected-sort {
font-weight: bold;
@apply border-primary;
@@ -219,9 +231,6 @@ th {
}
tbody td {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -1,27 +1,28 @@
<template>
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm w-10 gap-0.5 px-2">
<carbon:circle-solid class="text-red w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2.5" v-if="streamConfig.stdout" />
<div class="dropdown dropdown-end dropdown-hover z-20">
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
</label>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 z-50 w-52 p-1 shadow-sm">
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
</li>
<li>
<a @click.prevent="showSearch = true">
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
@click="hideMenu"
>
<li v-if="!historical">
<a @click="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
</li>
<li v-if="!historical">
<a @click="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li v-if="hasComplexLogs">
<a @click.prevent="showDrawer(LogAnalytics, { container }, 'lg')">
<a @click="showDrawer(LogAnalytics, { container }, 'lg')">
<ph:file-sql /> SQL Analytics
<KeyShortcut char="f" :modifiers="['shift', 'meta']" />
</a>
@@ -102,8 +103,28 @@
</details>
</li>
<li class="line"></li>
<li v-if="enableDownload">
<a :href="downloadUrl" download>
<octicon:download-24 />
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
</a>
</li>
<li v-if="isSupported">
<a @click="copyLogs()">
<mdi:content-copy />
{{ isFiltered ? $t("toolbar.copy-filtered-logs") : $t("toolbar.copy-logs") }}
</a>
</li>
<li>
<a @click="copyPermalink()">
<material-symbols:link />
{{ $t("toolbar.copy-permalink") }}
</a>
</li>
<!-- Container Actions (Enabled via config) -->
<template v-if="enableActions">
<template v-if="enableActions && !historical">
<li class="line"></li>
<li>
<button
@@ -135,19 +156,23 @@
</li>
</template>
<li class="line"></li>
<li>
<a @click.prevent="showDrawer(Terminal, { container, action: 'attach' }, 'lg')">
<ri:terminal-window-fill /> Attach
<KeyShortcut char="a" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a @click.prevent="showDrawer(Terminal, { container, action: 'exec' }, 'lg')">
<material-symbols:terminal /> Shell
<KeyShortcut char="e" :modifiers="['shift', 'meta']" />
</a>
</li>
<template v-if="enableShell && !historical">
<li class="line"></li>
<li>
<a @click="showDrawer(Terminal, { container, action: 'attach' }, 'lg')">
<ri:terminal-window-fill />
{{ $t("toolbar.attach") }}
<KeyShortcut char="a" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a @click="showDrawer(Terminal, { container, action: 'exec' }, 'lg')">
<material-symbols:terminal />
{{ $t("toolbar.shell") }}
<KeyShortcut char="e" :modifiers="['shift', 'meta']" />
</a>
</li>
</template>
</ul>
</div>
</template>
@@ -159,14 +184,115 @@ import LogAnalytics from "../LogViewer/LogAnalytics.vue";
import Terminal from "@/components/Terminal.vue";
const { showSearch } = useSearchFilter();
const { enableActions } = config;
const { enableActions, enableShell, enableDownload } = config;
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
const showDrawer = useDrawer();
const { container } = defineProps<{ container: Container }>();
const { container, historical = false } = defineProps<{ container: Container; historical?: boolean }>();
const clear = defineEmit();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
const router = useRouter();
const { copy, copied, isSupported } = useClipboard();
const { t } = useI18n();
const { showToast, removeToast } = useToast();
async function copyPermalink() {
const url = router.resolve({
name: "/show",
query: { name: container.name, host: container.host },
}).href;
const resolved = new URL(url, window.location.origin);
if (!isSupported.value) {
showToast(
{
title: t("error.copy-not-supported-hint"),
message: resolved.href,
type: "info",
},
{ expire: 10000 },
);
return;
}
await copy(resolved.href);
if (copied.value) {
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
}
}
async function copyLogs() {
const params = new URLSearchParams();
if (streamConfig.value.stdout) params.append("stdout", "1");
if (streamConfig.value.stderr) params.append("stderr", "1");
params.append("everything", "1");
const { debouncedSearchFilter } = useSearchFilter();
if (debouncedSearchFilter.value) {
params.append("filter", debouncedSearchFilter.value);
}
const selectedLevels = Array.from(levels.value);
if (selectedLevels.length > 0 && selectedLevels.length < allLevels.length) {
selectedLevels.forEach((level) => params.append("levels", level));
}
const url = withBase(`/api/hosts/${container.host}/containers/${container.id}/logs?${params.toString()}`);
const toastId = "copy-logs";
showToast(
{
id: toastId,
title: t("toolbar.copying-logs"),
message: "",
type: "info",
},
{ once: true },
);
const blobPromise = fetch(url, { headers: { Accept: "text/plain" } })
.then((response) => {
if (!response.ok) throw new Error(response.statusText);
return response.blob();
})
.then((blob) => {
removeToast(toastId);
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
return blob;
})
.catch((err) => {
removeToast(toastId);
showToast(
{
title: "Error",
message: err.message,
type: "error",
},
{ expire: 5000 },
);
throw err;
});
await navigator.clipboard.write([new ClipboardItem({ "text/plain": blobPromise })]);
}
onKeyStroke("f", (e) => {
if (hasComplexLogs.value) {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
@@ -175,31 +301,28 @@ onKeyStroke("f", (e) => {
}
}
});
if (enableShell) {
onKeyStroke("a", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(Terminal, { container, action: "attach" }, "lg");
e.preventDefault();
}
});
onKeyStroke("a", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(Terminal, { container, action: "attach" }, "lg");
e.preventDefault();
}
});
onKeyStroke("e", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(Terminal, { container, action: "exec" }, "lg");
e.preventDefault();
}
});
}
onKeyStroke("e", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(Terminal, { container, action: "exec" }, "lg");
e.preventDefault();
}
});
const downloadParams = computed(() =>
Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
);
const downloadUrl = computed(() =>
withBase(
`/api/containers/${container.host}~${container.id}/download?${new URLSearchParams(downloadParams.value).toString()}`,
),
const containerRef = computed(() => [container]);
const { downloadUrl, isFiltered } = useDownloadUrl(
containerRef,
streamConfig,
levels,
toRef(() => container.name),
);
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
@@ -214,10 +337,20 @@ const toggleAllLevels = computed({
}
},
});
const hideMenu = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, 50);
}
};
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
li.line {
@apply bg-base-content/20 h-px;
@@ -15,7 +15,7 @@ defineProps<{
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
[health="unhealthy"] {
@apply text-red;
}
@@ -1,7 +1,7 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="@container mx-2 flex items-center gap-2 md:ml-4">
<div class="@container mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
<ContainerTitle :container="container" />
<MultiContainerStat
class="ml-auto lg:hidden lg:@3xl:flex"
@@ -9,7 +9,7 @@
v-if="container.state === 'running'"
/>
<ContainerActionsToolbar @clear="viewer?.clear()" class="max-md:hidden" :container="container" />
<ContainerActionsToolbar @clear="viewer?.clear()" :container="container" />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
@@ -1,44 +1,48 @@
<template>
<div class="@container flex flex-1 items-center gap-1.5 truncate md:gap-2">
<div class="@container flex min-w-0 flex-1 items-center gap-1.5 md:gap-2">
<label class="swap swap-rotate size-4">
<input type="checkbox" v-model="pinned" />
<carbon:star-filled class="swap-on text-secondary" />
<carbon:star class="swap-off" />
</label>
<div class="inline-flex items-center text-sm">
<div class="breadcrumbs p-0">
<div class="inline-flex min-w-0 items-center text-sm">
<div class="breadcrumbs min-w-0 overflow-x-visible p-0 font-mono">
<ul>
<li v-if="config.hosts.length > 1" class="font-thin max-md:hidden">
{{ container.hostLabel }}
</li>
<li>
<div v-if="otherContainers.length === 0" class="font-mono">{{ container.name }}</div>
<div class="wrapper" ref="wrapper" v-else>
<button popovertarget="popover-container-list" class="btn btn-xs md:btn-sm anchor font-mono">
{{ container.name }} <carbon:caret-down />
</button>
<ul popover id="popover-container-list" class="dropdown menu rounded-box bg-base-100 tethered shadow-sm">
<li v-for="other in otherContainers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
<div class="font-mono" v-if="other.isSwarm">{{ other.swarmId }}</div>
<div class="font-mono" v-else>{{ other.name }}</div>
<div v-if="other.state === 'running'">running</div>
<DistanceTime :date="other.created" strict class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
<template v-if="otherContainers.length === 0">{{ container.name }}</template>
<div v-else>
<div class="dropdown">
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm">
{{ container.name }} <carbon:caret-down />
</button>
<ul
tabindex="0"
class="dropdown-content menu rounded-box bg-base-100 border-base-content/20 border shadow-sm"
>
<li v-for="other in otherContainers">
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }">
<div
class="status data-[state=exited]:status-error data-[state=running]:status-success"
:data-state="other.state"
></div>
<div v-if="other.isSwarm">{{ other.swarmId }}</div>
<div v-else>{{ other.name }}</div>
<div v-if="other.state === 'running'">running</div>
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
</router-link>
</li>
</ul>
</div>
</div>
</li>
</ul>
</div>
</div>
<ContainerHealth :health="container.health" v-if="container.health" />
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
<Tag class="hidden! font-mono @xl:block!" size="small">
{{ container.image.replace(/@sha.*/, "") }}
</Tag>
</div>
@@ -66,34 +70,6 @@ const otherContainers = computed(() =>
.filter((c) => c.name === container.name && c.id !== container.id)
.sort((a, b) => +b.created - +a.created),
);
const wrapper = useTemplateRef("wrapper");
onMounted(async () => {
if (!("anchorName" in document.documentElement.style)) {
// @ts-ignore
const module = await import("@oddbird/css-anchor-positioning/fn");
// @ts-ignore
await module.default([wrapper.value]);
}
});
</script>
<style scoped>
/* https://github.com/oddbird/css-anchor-positioning/issues/282 */
.wrapper {
anchor-scope: --anchor;
}
.anchor {
anchor-name: --anchor;
}
.tethered {
margin: 0;
padding: 0;
position-anchor: --anchor;
position: absolute;
top: anchor(bottom);
left: anchor(left);
}
</style>
<style scoped></style>
@@ -0,0 +1,63 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="@container mx-2 flex items-center gap-2 md:ml-4">
<ContainerTitle :container="container" />
<router-link
:to="{ name: '/container/[id]', params: { id: container.id } }"
class="btn btn-secondary btn-sm"
v-if="container.state === 'running'"
>
<mdi:lightning-bolt />
Live Logs
</router-link>
<ContainerActionsToolbar class="max-md:hidden" :container="container" historical />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useHistoricalContainerLog"
:entity="historicalContainer"
:visible-keys="visibleKeys"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { HistoricalContainer } from "@/models/Container";
import { ComponentExposed } from "vue-component-type-helpers";
const {
id,
showTitle = false,
scrollable = false,
closable = false,
date,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
date: Date;
}>();
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer(toRef(() => id));
const historicalContainer = toRef(() => new HistoricalContainer(container.value, date));
const visibleKeys = persistentVisibleKeysForContainer(container);
useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(
toRef(() => [container.value]),
{ showContainerName: false, showHostname: false, historical: true },
);
</script>
@@ -42,6 +42,8 @@ function createFuzzySearchModal() {
"host",
{},
"running",
0,
0,
[],
),
new Container(
@@ -55,6 +57,8 @@ function createFuzzySearchModal() {
"host",
{},
"running",
0,
0,
[],
),
new Container(
@@ -68,6 +72,8 @@ function createFuzzySearchModal() {
"host",
{},
"running",
0,
0,
[],
),
],
+9 -6
View File
@@ -1,5 +1,5 @@
<template>
<div class="dropdown dropdown-open w-full">
<div class="dropdown dropdown-open w-full shadow-md">
<div class="input input-xl input-primary flex w-full items-center">
<mdi:magnify class="flex size-8" />
<input
@@ -13,8 +13,11 @@
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<form method="dialog">
<button class="swap hover:swap-active">
<form method="dialog" class="flex">
<button v-if="isMobile">
<mdi:close />
</button>
<button v-else class="swap hover:swap-active outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
@@ -30,7 +33,7 @@
<a
class="grid auto-cols-max grid-cols-[min-content_auto] gap-2 py-4"
@click.prevent="selected(result.item)"
:class="index === selectedIndex ? 'menu-focus' : ''"
:class="{ 'menu-focus': index === selectedIndex }"
>
<div :class="{ 'text-primary': result.item.state === 'running' }">
<template v-if="result.item.type === 'container'">
@@ -50,7 +53,7 @@
<span data-name v-html="matchedName(result)"></span>
</div>
<DistanceTime :date="result.item.created" class="text-xs font-light" />
<RelativeTime :date="result.item.created" class="text-xs font-light" />
<span
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
@@ -214,7 +217,7 @@ function matchedName({ item, matches = [] }: FuseResult<Item>) {
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
:deep(mark) {
@apply bg-transparent text-inherit underline underline-offset-2;
}
@@ -2,13 +2,11 @@
<ScrollableView :scrollable="scrollable" v-if="group.containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 gap-1.5 truncate md:gap-2">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ $t("label.container", group.containers.length) }}</div>
</div>
</div>
<ContainerDropdown :containers="group.containers">
{{ $t("label.container", group.containers.length) }}
</ContainerDropdown>
<MultiContainerStat class="ml-auto" :containers="group.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
<MultiContainerActionToolbar class="max-md:hidden" :name="group.name" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
+154
View File
@@ -0,0 +1,154 @@
<template>
<div class="card bg-base-100">
<div class="card-body flex gap-2 max-md:p-4">
<div class="flex flex-row gap-2 overflow-hidden">
<div class="flex items-center gap-1 truncate text-xl font-semibold">
<HostIcon :type="host.type" class="flex-none" />
<div class="truncate">
{{ host.name }}
</div>
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
<carbon:warning />
offline
</span>
<span
class="badge badge-success badge-xs gap-2 p-2"
:class="{ 'badge-warning': config.version != host.agentVersion }"
v-else-if="host.type == 'agent'"
title="Dozzle Agent"
>
{{ host.agentVersion }}
</span>
</div>
<ul class="ml-auto flex flex-row flex-wrap gap-x-2 text-sm max-md:text-xs md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" />
{{ $t("label.container", hostContainers.length) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
</div>
<div class="grid grid-cols-2 gap-2 md:gap-3" v-if="stats">
<MetricCard
:icon="PhCpu"
:value="stats.weighted.movingAverage.totalCPU"
:chartData="cpuHistory"
container-class="border-primary/40 bg-primary/20"
text-class="text-primary"
bar-class="bg-primary"
:formatValue="(value) => `${value.toFixed(1)}%`"
:label="`${host.nCPU} CPU`"
/>
<MetricCard
:icon="PhMemory"
:value="stats.weighted.movingAverage.totalMemUsage"
:chartData="memHistory"
container-class="border-secondary/40 bg-secondary/20"
text-class="text-secondary"
bar-class="bg-secondary"
:formatValue="(value) => formatBytes(value, { decimals: 1 })"
:label="formatBytes(host.memTotal, { decimals: 1 })"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Host } from "@/stores/hosts";
import { Container } from "@/models/Container";
// @ts-ignore
import PhCpu from "~icons/ph/cpu";
// @ts-ignore
import PhMemory from "~icons/ph/memory";
const props = defineProps<{
host: Host;
}>();
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore) as unknown as {
containers: Ref<Container[]>;
};
const hostContainers = computed(() =>
containers.value.filter((container) => container.host === props.host.id && container.state === "running"),
);
function toContainerCores(container: Container): number {
if (container.cpuLimit && container.cpuLimit > 0) {
return 1;
}
return props.host.nCPU ?? 1;
}
type TotalStat = {
totalCPU: number;
totalMem: number;
totalMemUsage: number;
};
const totalStat = ref<TotalStat>({ totalCPU: 0, totalMem: 0, totalMemUsage: 0 });
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
const cpuHistory = computed(() =>
history.value.map((stat) => ({
percent: stat.totalCPU,
value: stat.totalCPU,
})),
);
const memHistory = computed(() =>
history.value.map((stat) => ({
percent: stat.totalMem,
value: stat.totalMemUsage,
})),
);
const stats = reactive({ mostRecent: totalStat, weighted: useExponentialMovingAverage(totalStat) });
watch(
() => hostContainers.value,
() => {
const initial: TotalStat[] = [];
for (let i = 1; i <= 300; i++) {
const stat = hostContainers.value.reduce(
(acc, container) => {
const item = container.statsHistory.at(-i);
if (!item) {
return acc;
}
const cores = toContainerCores(container);
return {
totalCPU: acc.totalCPU + item.cpu / cores,
totalMem: acc.totalMem + item.memory,
totalMemUsage: acc.totalMemUsage + item.memoryUsage,
};
},
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
);
initial.push(stat);
}
reset({ initial: initial.reverse() });
stats.weighted.reset(initial.at(-1)!);
},
{ immediate: true },
);
useIntervalFn(() => {
totalStat.value = hostContainers.value.reduce(
(acc, container) => {
const cores = toContainerCores(container);
return {
totalCPU: acc.totalCPU + container.stat.cpu / cores,
totalMem: acc.totalMem + container.stat.memory,
totalMemUsage: acc.totalMemUsage + container.stat.memoryUsage,
};
},
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
);
}, 1000);
</script>
+2 -111
View File
@@ -1,120 +1,11 @@
<template>
<ul class="grid gap-4 md:grid-cols-[repeat(auto-fill,minmax(480px,1fr))]">
<li v-for="host in hosts" class="card bg-base-100">
<div class="card-body grid auto-cols-auto grid-flow-col justify-between gap-4">
<div class="flex flex-col gap-2 overflow-hidden">
<div class="flex items-center gap-1 truncate text-xl font-semibold">
<HostIcon :type="host.type" class="flex-none" />
<div class="truncate">
{{ host.name }}
</div>
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
<carbon:warning />
offline
</span>
<span
class="badge badge-success badge-xs gap-2 p-2"
:class="{ 'badge-warning': config.version != host.agentVersion }"
v-else-if="host.type == 'agent'"
title="Dozzle Agent"
>
{{ host.agentVersion }}
</span>
</div>
<ul class="flex flex-row gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1"><ph:cpu /> {{ host.nCPU }} <span class="max-md:hidden">CPUs</span></li>
<li class="flex items-center gap-1">
<ph:memory /> {{ formatBytes(host.memTotal) }}
<span class="max-md:hidden">total</span>
</li>
</ul>
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" />
{{ $t("label.container", hostContainers[host.id]?.length ?? 0) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
</div>
<div class="flex flex-row gap-4 md:gap-8" v-if="weightedStats[host.id]">
<div
class="radial-progress text-primary text-[0.85rem] transition-none [--size:4rem] [--thickness:0.25em] md:text-[0.9rem] md:[--size:5rem]"
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalCPU / (host.nCPU * 100)) * 100)};`"
role="progressbar"
>
{{ weightedStats[host.id].weighted.totalCPU.toFixed(0) }}%
</div>
<div
class="radial-progress text-primary text-[0.85rem] transition-none [--size:4rem] [--thickness:0.25em] md:text-[0.9rem] md:[--size:5rem]"
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalMem / host.memTotal) * 100)};`"
role="progressbar"
>
{{ formatBytes(weightedStats[host.id].weighted.totalMem, 1) }}
</div>
</div>
</div>
<li v-for="host in hosts" :key="host.id">
<HostCard :host="host" />
</li>
</ul>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore) as unknown as {
containers: Ref<Container[]>;
};
const runningContainers = computed(() => containers.value.filter((container) => container.state === "running"));
const { hosts } = useHosts();
const hostContainers = computed(() => {
const results: Record<string, Container[]> = {};
for (const container of runningContainers.value) {
if (!results[container.host]) {
results[container.host] = [];
}
results[container.host].push(container);
}
return results;
});
type TotalStat = {
totalCPU: number;
totalMem: number;
};
const weightedStats: Record<string, { mostRecent: TotalStat; weighted: TotalStat }> = {};
const initWeightedStats = () => {
for (const [host, containers] of Object.entries(hostContainers.value)) {
const mostRecent = ref<TotalStat>({ totalCPU: 0, totalMem: 0 });
for (const container of containers) {
mostRecent.value.totalCPU += container.stat.cpu;
mostRecent.value.totalMem += container.stat.memoryUsage;
}
weightedStats[host] = reactive({ mostRecent, weighted: useExponentialMovingAverage(mostRecent) });
}
};
watchOnce(hostContainers, initWeightedStats);
initWeightedStats();
useIntervalFn(
() => {
for (const [host, containers] of Object.entries(hostContainers.value)) {
const stat = { totalCPU: 0, totalMem: 0 };
for (const container of containers) {
stat.totalCPU += container.stat.cpu;
stat.totalMem += container.stat.memoryUsage;
}
if (weightedStats[host]) {
// TODO fix this init
weightedStats[host].mostRecent = stat;
}
}
},
1000,
{ immediate: true },
);
</script>
+34 -11
View File
@@ -13,7 +13,7 @@
}"
class="btn btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-hosts')"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
{{ hosts[sessionHost].name }}
@@ -24,15 +24,21 @@
<div class="flex-none">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
<ph:dots-three-vertical-bold />
<ion:ellipsis-vertical />
</label>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 z-50 w-52 p-1 shadow-sm">
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
>
<li>
<a class="text-sm capitalize" @click="toggleShowAllContainers()">
<mdi:check class="w-4" v-if="showAllContainers" />
<div v-else class="w-4"></div>
Show all containers
{{ $t("label.show-all-containers") }}
</a>
<a class="text-sm capitalize" @click="collapseAll()">
<material-symbols-light:collapse-all class="w-4" />
{{ $t("label.collapse-all") }}
</a>
</li>
</ul>
@@ -58,7 +64,7 @@
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
<summary class="text-base-content/80 font-light">
<component :is="icon" />
{{ label.startsWith("label.") ? $t(label) : label }}
{{ label.startsWith("label.") ? $t(label) : label }} ({{ containers.length }})
<router-link
:to="{
@@ -67,7 +73,7 @@
}"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-containers')"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
</router-link>
@@ -115,7 +121,7 @@
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage";
import { showAllContainers } from "@/stores/settings";
import { showAllContainers, groupContainers } from "@/stores/settings";
// @ts-ignore
import Pin from "~icons/ph/map-pin-simple";
@@ -141,6 +147,18 @@ const updateCollapsedGroups = (event: Event, label: string) => {
} else {
collapsedGroups.value.add(label);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
const collapseAll = () => {
menuItems.value.forEach(({ label }) => {
collapsedGroups.value.add(label);
});
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
const debouncedPinnedContainers = debouncedRef(pinnedContainers, 200);
@@ -165,7 +183,7 @@ const menuItems = computed(() => {
for (const item of sortedContainers.value) {
const namespace = item.namespace;
if (debouncedPinnedContainers.value.has(item.name)) {
if (debouncedPinnedContainers.value?.has(item.name)) {
pinned.push(item);
} else if (namespace) {
namespaced[namespace] ||= [];
@@ -180,10 +198,15 @@ const menuItems = computed(() => {
items.push({ label: "label.pinned", containers: pinned, icon: Pin });
}
for (const [label, containers] of Object.entries(namespaced).sort(([a], [b]) => a.localeCompare(b))) {
if (containers.length > 1) {
const shouldGroup =
groupContainers.value === "always" || (groupContainers.value === "at-least-2" && containers.length > 1);
if (shouldGroup) {
items.push({ label, containers, icon: Stack });
} else {
singular.push(containers[0]);
for (const container of containers) {
singular.push(container);
}
}
}
+2 -2
View File
@@ -2,7 +2,7 @@
<ScrollableView :scrollable="scrollable" v-if="host">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate md:gap-2">
<div class="flex flex-1 items-center gap-1.5 truncate md:gap-2">
<ph:computer-tower />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ host.name }}</div>
@@ -12,7 +12,7 @@
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
<MultiContainerActionToolbar class="max-md:hidden" :name="host.name" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
-31
View File
@@ -1,31 +0,0 @@
<template>
<div ref="root" class="flex min-h-[1px] justify-center">
<span class="loading loading-bars loading-md text-primary mt-4" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
const { onLoadMore = () => Promise.resolve(), enabled } = defineProps<{
onLoadMore: () => Promise<void>;
enabled: boolean;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (onLoadMore && enabled) {
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await onLoadMore();
isLoading.value = false;
await nextTick();
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
}
});
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>
+142
View File
@@ -0,0 +1,142 @@
<template>
<div class="flex items-center">
<div class="breadcrumbs flex-1">
<ul>
<li>
<a @click.prevent="setNamespace(null)" class="link-primary">{{ $t("label.namespaces") }}</a>
</li>
<li v-if="selectedNamespace === 'all'">
{{ $t("label.all-namespaces") }}
</li>
<li v-else-if="selectedNamespace" class="cursor-default">
<router-link
:to="{
name: '/namespace/[name]',
params: { name: selectedNamespace },
}"
class="btn btn-outline btn-primary btn-xs"
active-class="btn-active"
>
<ph:arrows-merge />
{{ selectedNamespace }}
</router-link>
</li>
</ul>
</div>
<div class="flex-none">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
<ph:dots-three-vertical-bold />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
>
<li>
<a class="text-sm capitalize" @click="collapseAll()">
<material-symbols-light:collapse-all class="w-4" />
{{ $t("label.collapse-all") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<SlideTransition :slide-right="selectedNamespace !== null">
<template #left>
<ul class="menu p-0">
<li>
<a @click.prevent="setNamespace('all')">
<ph:circles-four />
{{ $t("label.all-namespaces") }}
</a>
</li>
<li v-for="ns in namespaces" :key="ns.name">
<a @click.prevent="setNamespace(ns.name)">
<ph:circles-four />
{{ ns.name }}
</a>
</li>
</ul>
</template>
<template #right>
<ul class="menu w-full p-0 text-[0.95rem]" ref="menu">
<li v-for="{ name, owners } in filteredNamespaces" :key="name">
<details open>
<summary class="text-base-content/80 font-light">
<ph:stack />
{{ name }} ({{ owners.length }})
<router-link
:to="{ name: '/namespace/[name]', params: { name } }"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
</router-link>
</summary>
<ul>
<li v-for="owner in owners" :key="`${owner.kind}-${owner.name}`">
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
</router-link>
</li>
</ul>
</details>
</li>
<li v-if="ownersWithoutNamespace.length > 0">
<details open>
<summary class="text-base-content/80 font-light">
<ph:circles-four />
{{ $t("label.owners") }} ({{ ownersWithoutNamespace.length }})
</summary>
<ul>
<li v-for="owner in ownersWithoutNamespace" :key="`${owner.kind}-${owner.name}`">
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
</router-link>
</li>
</ul>
</details>
</li>
</ul>
</template>
</SlideTransition>
</template>
<script lang="ts" setup>
const store = useK8sStore();
const { namespaces, owners } = storeToRefs(store);
const selectedNamespace = ref<string | null>("all");
const setNamespace = (namespace: string | null) => (selectedNamespace.value = namespace);
const filteredNamespaces = computed(() => {
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
return namespaces.value;
}
return namespaces.value.filter((ns) => ns.name === selectedNamespace.value);
});
const ownersWithoutNamespace = computed(() => {
const filtered = owners.value.filter((owner) => !owner.namespace);
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
return filtered;
}
return [];
});
const menu = useTemplateRef("menu");
const collapseAll = () => {
const details = menu.value?.querySelectorAll("details");
details?.forEach((detail) => (detail.open = false));
};
</script>
@@ -0,0 +1,43 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="namespace.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<ph:stack />
<div class="font-mono text-sm font-semibold">{{ namespace.name }}</div>
<ContainerDropdown :containers="namespace.containers">
{{ $t("label.container", namespace.containers.length) }}
</ContainerDropdown>
</div>
<MultiContainerStat class="ml-auto" :containers="namespace.containers" />
<MultiContainerActionToolbar class="max-md:hidden" :name="namespace.name" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useNamespaceStream"
:entity="namespace"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import { K8sNamespace } from "@/stores/k8s";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { namespace, scrollable = false } = defineProps<{
scrollable?: boolean;
namespace: K8sNamespace;
}>();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
provideLoggingContext(
toRef(() => namespace.containers),
{ showContainerName: true, showHostname: false },
);
</script>
+43
View File
@@ -0,0 +1,43 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="owner.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<ph:stack-simple />
<div class="font-mono text-sm font-semibold">{{ owner.kind }}/{{ owner.name }}</div>
<ContainerDropdown :containers="owner.containers">
{{ $t("label.container", owner.containers.length) }}
</ContainerDropdown>
</div>
<MultiContainerStat class="ml-auto" :containers="owner.containers" />
<MultiContainerActionToolbar class="max-md:hidden" :name="owner.name" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useOwnerStream"
:entity="owner"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import { K8sOwner } from "@/stores/k8s";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { owner, scrollable = false } = defineProps<{
scrollable?: boolean;
owner: K8sOwner;
}>();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
provideLoggingContext(
toRef(() => owner.containers),
{ showContainerName: true, showHostname: false },
);
</script>
+36 -13
View File
@@ -3,6 +3,15 @@
<slot name="more-items"></slot>
<Announcements />
<router-link
:to="{ name: '/notifications' }"
:aria-label="$t('title.notifications')"
data-testid="notifications"
class="btn btn-circle btn-sm"
>
<mdi:bell class="size-6" />
</router-link>
<router-link
:to="{ name: '/settings' }"
:aria-label="$t('title.settings')"
@@ -14,23 +23,31 @@
<dropdown class="dropdown-end" v-if="config.user">
<template #trigger>
<img
class="ring-base-content/60 size-6 max-w-none rounded-full p-px ring-1"
:src="withBase('/api/profile/avatar')"
/>
<template v-if="config.disableAvatars || !config.user.email">
<material-symbols:person class="size-6" />
</template>
<template v-else>
<img
class="ring-base-content/60 size-6 max-w-none rounded-full p-px ring-1"
:src="withBase('/api/profile/avatar')"
/>
</template>
</template>
<template #content>
<div class="p-2">
<div class="font-bold">
{{ config.user.name }}
</div>
<div class="text-sm font-light">
<div v-if="config.user.email" class="text-sm font-light">
{{ config.user.email }}
</div>
</div>
<ul class="menu mt-4 p-0">
<li v-if="config.authProvider === 'simple'">
<button @click.prevent="logout()" class="text-primary p-2">{{ $t("button.logout") }}</button>
<ul v-if="config.authProvider === 'simple' || config.logoutUrl" class="menu mt-4 p-0">
<li>
<button @click.prevent="logout()" class="text-primary p-2">
<material-symbols:logout />
{{ $t("button.logout") }}
</button>
</li>
</ul>
</template>
@@ -38,11 +55,17 @@
</div>
</template>
<script lang="ts" setup>
async function logout() {
await fetch(withBase("/api/token"), {
method: "DELETE",
});
const { logoutUrl } = config;
location.reload();
async function logout() {
if (logoutUrl) {
location.href = logoutUrl;
} else {
await fetch(withBase("/api/token"), {
method: "DELETE",
});
location.reload();
}
}
</script>
+50 -12
View File
@@ -1,15 +1,32 @@
<template>
<LogItem :logEntry @click="showDrawer(LogDetails, { entry: logEntry })" class="clickable">
<ul class="space-x-4">
<li v-for="(value, name) in validValues" :key="name" class="inline-flex">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="font-bold" v-html="JSON.stringify(value)"> </span>
</template>
<span class="font-bold" v-html="stripAnsi(value.toString())" v-else></span>
<DefineTemplate v-slot="{ data }">
<ul class="inline-flex flex-wrap space-x-4" @click="preventDefaultOnLinks">
<li v-for="(value, name) in data" :key="name" v-if="isObject(data)">
<span class="key">{{ name }}=</span>
<span class="value" v-if="value === null">&lt;null&gt;</span>
<ReuseTemplate :data="value" v-else-if="isObject(value) || Array.isArray(value)" />
<span v-else class="value" :class="typeof value" v-html="stripAnsi(value.toString())"></span>
</li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
<li v-else-if="Array.isArray(data)">
<ul class="array inline-flex flex-wrap space-x-1">
<li
v-for="(item, index) in data"
:key="index"
class="after:text-base-content/70 not-last:after:content-[',']"
>
<ReuseTemplate :data="item" v-if="isObject(item) || Array.isArray(item)" />
<span v-else class="value" :class="typeof item" v-html="stripAnsi(item.toString())"></span>
</li>
</ul>
</li>
<li class="key" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
</DefineTemplate>
<LogItem :logEntry>
<LogLevel class="flex select-none" :level="logEntry.level" />
<div @click="containers.length > 0 && showDrawer(LogDetails, { entry: logEntry })" class="cursor-pointer">
<ReuseTemplate :data="validValues" />
</div>
</LogItem>
</template>
<script lang="ts" setup>
@@ -22,16 +39,37 @@ const { logEntry } = defineProps<{
showContainerName?: boolean;
}>();
const { containers } = useLoggingContext();
const [DefineTemplate, ReuseTemplate] = createReusableTemplate();
const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
});
const showDrawer = useDrawer();
function preventDefaultOnLinks(event: MouseEvent) {
if (event.target instanceof HTMLAnchorElement && event.target.rel?.includes("external")) {
event.stopImmediatePropagation();
}
}
</script>
<style scoped>
@import "@/main.css" reference;
.text-light {
@apply text-base-content/70;
@reference "@/main.css";
.key {
@apply text-base-content/70 font-light;
}
.value {
@apply text-base-content font-bold;
}
.array {
@apply before:text-base-content/80 after:text-base-content/80 before:content-['['] after:content-[']'];
}
.string {
@apply before:content-['"'] after:content-['"'];
}
</style>
@@ -13,7 +13,7 @@ const { logEntry } = defineProps<{
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
[data-event="container-stopped"] {
@apply text-red;
}
+37 -12
View File
@@ -29,10 +29,15 @@ describe("<ContainerEventSource />", () => {
global.EventSource = EventSource;
// @ts-ignore
window.scrollTo = vi.fn();
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
}));
global.IntersectionObserver = class IntersectionObserver {
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
takeRecords = vi.fn();
root = null;
rootMargin = "";
thresholds = [];
} as any;
vi.useFakeTimers();
vi.setSystemTime(1560336942459);
});
@@ -65,12 +70,29 @@ describe("<ContainerEventSource />", () => {
template: "Test from createLogEventSource",
},
},
{
name: "/container/[id].time.[datetime]",
path: "/container/:id/time/:datetime",
component: {
template: "Test from createLogEventSource",
},
},
],
});
return mount(Component, {
global: {
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
plugins: [
router,
createTestingPinia({
createSpy: vi.fn,
stubActions: false,
initialState: {
container: { containers: [{ id: "abc", image: "test:v123", host: "localhost" }] },
},
}),
createI18n({}),
],
components: {
LogViewer,
},
@@ -84,6 +106,7 @@ describe("<ContainerEventSource />", () => {
streamConfig: reactive({ stdout: true, stderr: true }),
hasComplexLogs: ref(false),
levels: new Set<Level>(["info"]),
historical: ref(false),
},
},
},
@@ -105,6 +128,8 @@ describe("<ContainerEventSource />", () => {
"localhost",
{},
"created",
0,
0,
[],
),
},
@@ -136,7 +161,7 @@ describe("<ContainerEventSource />", () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message.", "c": "abc"}`,
});
vi.runAllTimers();
@@ -152,39 +177,39 @@ describe("<ContainerEventSource />", () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message.", "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
});
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
data: `{"ts":1560336942459, "m":"foo bar", "id":1, "rm": "foo bar", "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
});
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
data: `{"ts":1560336942459, "m":"foo bar", "id":1, "c": "abc"}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
});
});
});
+39 -18
View File
@@ -1,5 +1,4 @@
<template>
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 10" />
<ul class="flex animate-pulse flex-col gap-4 p-4" v-if="loading || (noLogs && waitingForMoreLog)">
<div class="flex flex-row gap-2" v-for="size in sizes">
<div class="bg-base-content/50 h-3 w-40 shrink-0 rounded-full opacity-50"></div>
@@ -7,21 +6,26 @@
</div>
<span class="sr-only">Loading...</span>
</ul>
<div v-else-if="noLogs && !waitingForMoreLog" class="p-4">Container has no logs yet</div>
<div v-else-if="noLogs && !waitingForMoreLog" class="p-4">
{{ $t("label.no-logs") }}
</div>
<slot :messages="messages" v-else></slot>
<IndeterminateBar :color />
<IndeterminateBar :color v-if="!historical" />
</template>
<script lang="ts" setup generic="T">
import { LogStreamSource } from "@/composable/eventStreams";
const route = useRoute();
const { entity, streamSource } = $defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
entity: T;
}>();
const { messages, loadOlderLogs, isLoadingMore, opened, loading, error, eventSourceURL } = streamSource($$(entity));
const { loadingMore } = useLoggingContext();
const { historical } = useLoggingContext();
const { messages, opened, loading, error } = streamSource(toRef(() => entity));
const color = computed(() => {
if (error.value) return "error";
if (loading.value) return "secondary";
@@ -37,19 +41,36 @@ defineExpose({
clear: () => (messages.value = []),
});
const fetchMore = async () => {
if (!isLoadingMore.value) {
loadingMore.value = true;
await loadOlderLogs();
loadingMore.value = false;
}
};
if (historical.value && route.query.logId) {
watchOnce(messages, async () => {
await nextTick();
document.getElementById(route.query.logId as string)?.scrollIntoView({ behavior: "instant", block: "center" });
});
}
const shuffle = (items: any[]) => {
return items.sort(() => Math.random() - 0.5);
};
const sizes = computedWithControl(eventSourceURL, () =>
shuffle(["w-3/5", "w-2/3", "w-9/12", "w-1/2", "w-1/3", "w-3/4"]),
const sizes = ref<string[]>([]);
watch(
opened,
(value) => {
if (value) return;
const sizeOptions = [
"w-2/12",
"w-3/12",
"w-4/12",
"w-5/12",
"w-6/12",
"w-7/12",
"w-8/12",
"w-9/12",
"w-10/12",
"w-11/12",
"w-full",
];
sizes.value = Array.from({ length: 18 }, () => sizeOptions[Math.floor(Math.random() * sizeOptions.length)]);
},
{
flush: "sync",
immediate: true,
},
);
</script>
@@ -0,0 +1,37 @@
<template>
<LogItem :logEntry>
<div class="flex flex-col">
<div v-for="(msg, index) in logEntry.message" :key="index" class="flex items-start gap-x-2">
<LogLevel class="flex select-none" :level="logEntry.level" :position="getPosition(index)" />
<div
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
v-html="colorize(msg)"
:class="{ 'min-h-4': msg === '' }"
></div>
</div>
</div>
</LogItem>
</template>
<script lang="ts" setup>
import { GroupedLogEntry, type Position } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "var(--color-base-content)",
bg: "var(--color-base-100)",
});
const { logEntry } = defineProps<{
logEntry: GroupedLogEntry;
}>();
const getPosition = (index: number): Position => {
const len = logEntry.message.length;
if (index === 0) return "start";
if (index === len - 1) return "end";
return "middle";
};
const colorize = (value: string) => ansiConvertor.toHtml(value);
</script>
@@ -0,0 +1,31 @@
<template>
<div ref="root" class="flex min-h-[1px] flex-1 content-center justify-center">
<span class="loading loading-bars loading-md text-primary m-2" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
import { LoadMoreLogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: LoadMoreLogEntry;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
useIntersectionObserver(root, async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (isLoading.value) return;
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await logEntry.loadMore();
isLoading.value = false;
await nextTick();
if (logEntry.rememberScrollPosition) {
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
}
});
</script>
<style scoped></style>
+192
View File
@@ -0,0 +1,192 @@
<template>
<div
class="dropdown dropdown-hover absolute -left-2 z-10 font-sans"
:class="shouldShowBelow ? 'dropdown-right' : 'dropdown-right dropdown-end'"
v-show="container"
ref="dropdownRef"
@mouseenter="checkDropdownPosition"
>
<router-link
v-if="isSearching"
@click="resetSearch()"
tabindex="0"
class="btn btn-square btn-xs border-base-content/20 bg-base-100 pointer-events-auto! opacity-0 shadow-sm group-hover/entry:opacity-90"
:to="{
name: '/container/[id].time.[datetime]',
params: { id: container.id, datetime: logEntry.date.toISOString() },
query: { logId: logEntry.id },
}"
>
<material-symbols:eye-tracking />
</router-link>
<button
tabindex="0"
class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"
v-else
>
<ion:ellipsis-vertical />
</button>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm"
@click="hideMenu"
>
<li v-if="isSearching">
<router-link
@click="resetSearch()"
:to="{
name: '/container/[id].time.[datetime]',
params: { id: container.id, datetime: logEntry.date.toISOString() },
query: { logId: logEntry.id },
}"
>
<material-symbols:eye-tracking />
{{ $t("action.see-in-context") }}
</router-link>
</li>
<li>
<a
@click="copyLogMessage()"
:disabled="!isSupported"
:title="!isSupported ? $t('error.copy-not-supported') : ''"
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
>
<material-symbols:content-copy />
{{ $t("action.copy-log") }}
</a>
</li>
<li>
<a
@click="copyPermalink()"
:disabled="!isSupported"
:title="!isSupported ? $t('error.copy-not-supported') : ''"
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
>
<material-symbols:link />
{{ $t("action.copy-link") }}
</a>
</li>
<li v-if="logEntry instanceof ComplexLogEntry">
<a @click="showDrawer(LogDetails, { entry: logEntry })">
<material-symbols:code-blocks-rounded />
{{ $t("action.show-details") }}
</a>
</li>
<li>
<a @click="createAlert()">
<mdi:bell />
{{ $t("action.create-alert") }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import stripAnsi from "strip-ansi";
import { Container } from "@/models/Container";
import { LogEntry, SimpleLogEntry, ComplexLogEntry, GroupedLogEntry, JSONObject } from "@/models/LogEntry";
import LogDetails from "./LogDetails.vue";
import AlertForm from "@/components/Notification/AlertForm.vue";
const { logEntry, container } = defineProps<{
logEntry: LogEntry<string | JSONObject>;
container: Container;
}>();
const { showToast } = useToast();
const showDrawer = useDrawer();
const router = useRouter();
const { isSearching, resetSearch } = useSearchFilter();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
async function copyLogMessage() {
if (!isSupported.value) {
return;
}
if (logEntry instanceof ComplexLogEntry) {
await copy(stripAnsi(logEntry.rawMessage));
} else if (logEntry instanceof SimpleLogEntry) {
await copy(stripAnsi(logEntry.rawMessage));
} else if (logEntry instanceof GroupedLogEntry) {
await copy(stripAnsi(logEntry.message.join("\n")));
}
if (copied.value) {
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
}
}
async function copyPermalink() {
if (!isSupported.value) {
return;
}
const url = router.resolve({
name: "/container/[id].time.[datetime]",
params: { id: container.id, datetime: logEntry.date.toISOString() },
query: { logId: logEntry.id },
}).href;
const resolved = new URL(url, window.location.origin);
await copy(resolved.href);
if (copied.value) {
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
}
}
function createAlert() {
const containerExpr = `name contains "${container.name}"`;
let logExpr = "";
if (logEntry.level && logEntry.level !== "unknown") {
logExpr = `level == "${logEntry.level}"`;
}
const nameParts = [container.name];
if (logEntry.level && logEntry.level !== "unknown") {
nameParts.push(logEntry.level);
}
const name = nameParts.join(" ");
showDrawer(AlertForm, { prefill: { name, containerExpression: containerExpr, logExpression: logExpr } }, "lg");
}
function hideMenu(e: MouseEvent) {
if (e.target instanceof HTMLAnchorElement) {
setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, 50);
}
}
const dropdownRef = useTemplateRef<HTMLDivElement>("dropdownRef");
const shouldShowBelow = ref(false);
function checkDropdownPosition() {
if (!dropdownRef.value) return;
const rect = dropdownRef.value.getBoundingClientRect();
shouldShowBelow.value = rect.top < 150;
}
</script>
+60 -24
View File
@@ -2,7 +2,7 @@
<aside>
<header class="flex items-center gap-4">
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
<h2 class="text-sm"><DistanceTime :date="container.created" /></h2>
<h2 class="text-sm"><RelativeTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
@@ -11,18 +11,26 @@
<textarea
v-model="query"
class="textarea textarea-primary w-full font-mono text-lg"
:class="{ 'textarea-error': error }"
:class="{ 'textarea-error!': error }"
:disabled="state === 'downloading'"
></textarea>
<div class="label max-h-48 overflow-y-auto pr-2">
<span class="label-text-alt text-error" v-if="error">{{ error }}</span>
<span class="label-text-alt" v-else>
Total {{ results.numRows }} records
<template v-if="results.numRows > pageLimit"> . Showing first {{ page.numRows }}. </template>
<div class="mt-2">
<span class="text-error" v-if="error">{{ error }}</span>
<span v-else-if="state === 'initializing'">{{ $t("analytics.creating_table") }}</span>
<span v-else-if="state === 'downloading'">{{
$t("analytics.downloading", { size: formatBytes(bytes, { decimals: 1 }) })
}}</span>
<span v-else-if="evaluating">{{ $t("analytics.evaluating_query") }}</span>
<span v-else>
{{ $t("analytics.total_records", { count: results.numRows.toLocaleString() }) }}
<template v-if="results.numRows > pageLimit">{{
$t("analytics.showing_first", { count: page.numRows.toLocaleString() })
}}</template>
</span>
</div>
</label>
</section>
<SQLTable :table="page" :loading="evaluating || !isReady" />
<SQLTable :table="page" :loading="evaluating || state !== 'ready'" />
</div>
</aside>
</template>
@@ -30,12 +38,15 @@
<script setup lang="ts">
import { Container } from "@/models/Container";
import { type Table } from "@apache-arrow/esnext-esm";
const { container } = defineProps<{ container: Container }>();
const query = ref("SELECT * FROM logs LIMIT 100");
const error = ref<string | null>(null);
const debouncedQuery = debouncedRef(query, 500);
const evaluating = ref(false);
const pageLimit = 1000;
const state = ref<"downloading" | "ready" | "initializing">("downloading");
const bytes = ref(0);
const url = withBase(
`/api/hosts/${container.host}/containers/${container.id}/logs?stdout=1&stderr=1&everything&jsonOnly`,
@@ -51,27 +62,49 @@ if (!response.ok) {
const { db, conn } = await useDuckDB();
const empty = await conn.query<Record<string, any>>(`SELECT 1 LIMIT 0`);
const { isReady } = useAsyncState(
async () => {
await db.registerFileBuffer("logs.json", new Uint8Array(await response.arrayBuffer()));
onMounted(async () => {
try {
state.value = "downloading";
const reader = response.body?.getReader();
if (!reader) throw new Error("No reader available from stream");
const chunks: Uint8Array[] = [];
bytes.value = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
bytes.value += value.length;
}
const arrayBuffer = new Uint8Array(bytes.value);
let position = 0;
for (const chunk of chunks) {
arrayBuffer.set(chunk, position);
position += chunk.length;
}
await db.registerFileBuffer("logs.json", arrayBuffer);
state.value = "initializing";
await conn.query(
`CREATE TABLE logs AS SELECT unnest(m) FROM read_json('logs.json', ignore_errors = true, format = 'newline_delimited')`,
);
},
undefined,
{
onError: (e) => {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
},
},
);
state.value = "ready";
} catch (e) {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
}
});
const results = computedAsync(
async () => {
if (isReady.value) {
if (state.value === "ready") {
return await conn.query<Record<string, any>>(debouncedQuery.value);
} else {
return empty;
@@ -89,7 +122,10 @@ const results = computedAsync(
},
);
whenever(evaluating, () => (error.value = null));
whenever(evaluating, () => {
error.value = null;
state.value = "ready";
});
const page = computed(() =>
results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value,
) as unknown as ComputedRef<Table<Record<string, any>>>;
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<Tag size="small" class="items-start!">
<Tag class="items-start!">
<DateTime :date="date" class="text-blue whitespace-nowrap" />
</Tag>
</template>
+12 -11
View File
@@ -1,10 +1,10 @@
<template>
<header class="flex items-center gap-4">
<Tag :data-level="entry.level" class="text-white uppercase" v-if="entry.level">{{ entry.level }}</Tag>
<Tag :data-level="entry.level" class="show-unknown text-white uppercase" v-if="entry.level">{{ entry.level }}</Tag>
<h1 class="text-lg max-md:hidden">
<DateTime :date="entry.date" />
</h1>
<h2 class="text-sm"><DistanceTime :date="entry.date" /> on {{ entry.std }}</h2>
<h2 class="text-sm"><RelativeTime :date="entry.date" /> on {{ entry.std }}</h2>
</header>
<div class="mt-8 flex flex-col gap-10">
@@ -29,15 +29,15 @@
<div class="flex gap-2">
Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="JSON.stringify(entry.unfilteredMessage)">
<UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage">
<button class="swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" />
<mdi:content-copy class="swap-off" />
<material-symbols:content-copy class="swap-off" />
</button>
</UseClipboard>
</div>
<div class="bg-base-200 max-h-48 overflow-scroll rounded-sm border border-white/20 p-2">
<pre v-html="syntaxHighlight(entry.unfilteredMessage)"></pre>
<pre v-html="syntaxHighlight(entry.rawMessage)"></pre>
</div>
</section>
<table class="table-pin-rows table table-fixed" v-if="entry instanceof ComplexLogEntry">
@@ -59,7 +59,7 @@
{{ key.join(".") }}
</td>
<td class="truncate max-md:hidden">
<code v-html="JSON.stringify(value)"></code>
<code>{{ JSON.stringify(value) }}</code>
</td>
<td>
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
@@ -96,14 +96,15 @@ function toggleField(key: string[]) {
const fields = computed({
get() {
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
const allFields = flattenJSONToMap(entry.unfilteredMessage);
const rawFields = JSON.parse(entry.rawMessage);
const allFields = flattenJSONToMap(rawFields);
if (visibleKeys.value.size === 0) {
for (const [key, value] of allFields) {
fieldsWithValue.push({ key, value, enabled: true });
}
} else {
for (const [key, enabled] of visibleKeys.value) {
const value = getDeep(entry.unfilteredMessage, key);
const value = getDeep(rawFields, key);
fieldsWithValue.push({ key, value, enabled });
}
@@ -141,8 +142,8 @@ const toggleAllFields = computed({
},
});
function syntaxHighlight(json: any) {
json = JSON.stringify(json, null, 2);
function syntaxHighlight(json: string) {
json = JSON.stringify(JSON.parse(json.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")), null, 2);
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
function (match: string) {
@@ -166,7 +167,7 @@ function syntaxHighlight(json: any) {
useSortable(list, fields);
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
.font-mono {
font-family:
ui-monospace,
+21 -9
View File
@@ -1,19 +1,29 @@
<template>
<div class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<LogActions :logEntry :container />
<LogStd :std="logEntry.std" class="shrink-0 select-none" v-if="showStd" />
<RandomColorTag class="shrink-0 select-none" :value="host.name" v-if="showHostname" />
<RandomColorTag class="shrink-0 select-none" :value="container.name" v-if="showContainerName" truncateRight />
<LogDate :date="logEntry.date" v-if="showTimestamp" class="shrink-0 select-none" />
<LogLevel
class="flex select-none"
:level="logEntry.level"
:position="logEntry instanceof SimpleLogEntry ? logEntry.position : undefined"
/>
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<RandomColorTag class="w-30 shrink-0 select-none md:w-40" :value="host.name" v-if="showHostname" />
<RandomColorTag
v-if="showContainerName"
class="w-30 shrink-0 select-none group-[.compact]:flex-1 md:w-40"
:value="container.name"
truncateRight
/>
<LogDate
v-if="showTimestamp"
:date="logEntry.date"
class="shrink-0 select-none"
:class="{ 'bg-secondary': route.query.logId === logEntry.id.toString() }"
/>
</div>
<slot />
</div>
</template>
<script lang="ts" setup>
import { LogEntry, SimpleLogEntry } from "@/models/LogEntry";
import { LogEntry } from "@/models/LogEntry";
const { logEntry } = defineProps<{
logEntry: LogEntry<any>;
@@ -25,4 +35,6 @@ const { hosts } = useHosts();
const container = currentContainer(toRef(() => logEntry.containerID));
const host = computed(() => hosts.value[container.value.host]);
const route = useRoute();
</script>
+31 -18
View File
@@ -1,41 +1,50 @@
<template>
<div :data-level="level" :data-position="position" class="mt-1.5 size-2.5 flex-none rounded-lg"></div>
<div
:data-level="level"
:data-position="position"
class="mt-1.5 size-2.5 flex-none rounded-lg"
:class="{ showUnknown }"
></div>
</template>
<script lang="ts" setup>
import { Position } from "@/models/LogEntry";
import { Position, Level } from "@/models/LogEntry";
defineProps<{
level?: string;
const {
level,
position,
showUnknown = false,
} = defineProps<{
level?: Level;
position?: Position;
showUnknown?: boolean;
}>();
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
[data-position="start"],
[data-position="middle"],
[data-position="end"] {
align-self: stretch;
height: auto;
}
[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.4em;
margin-top: auto;
align-self: flex-end;
border-radius: 0.375rem 0.375rem 0 0;
}
[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.4em 0;
align-self: stretch;
margin-top: 0;
}
[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.4em;
align-self: flex-start;
border-radius: 0 0 0.375rem 0.375rem;
margin-top: 0;
}
</style>
<style>
@import "@/main.css" reference;
@reference "@/main.css";
[data-level="debug"],
[data-level="trace"] {
@apply !bg-purple;
@@ -53,4 +62,8 @@ defineProps<{
[data-level="warn"] {
@apply !bg-orange;
}
[data-level="unknown"].show-unknown {
@apply !bg-base-300;
}
</style>
+17 -13
View File
@@ -1,10 +1,10 @@
<template>
<ul class="events group pt-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }">
<ul class="group pt-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }" data-logs>
<li
v-for="item in messages"
ref="list"
:key="item.id"
:data-key="item.id"
:id="item.id.toString()"
:data-time="item.date.getTime()"
class="group/entry"
>
@@ -14,22 +14,19 @@
</template>
<script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry";
import type { LogEntry, LogMessage } from "@/models/LogEntry";
const { loading, progress, currentDate } = useScrollContext();
const { progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[];
messages: LogEntry<LogMessage>[];
}>();
watchEffect(() => {
loading.value = messages.length === 0;
});
const { containers } = useLoggingContext();
const list = ref<HTMLElement[]>([]);
let previousDate = new Date();
useIntersectionObserver(
list,
(entries) => {
@@ -40,9 +37,12 @@ useIntersectionObserver(
const time = entry.target.getAttribute("data-time");
if (time) {
const date = new Date(parseInt(time));
if (+date === +previousDate) break;
previousDate = date;
const diff = new Date().getTime() - container.created.getTime();
progress.value = (date.getTime() - container.created.getTime()) / diff;
currentDate.value = date;
break;
}
}
}
@@ -54,8 +54,8 @@ useIntersectionObserver(
);
</script>
<style scoped>
@import "@/main.css" reference;
.events {
@reference "@/main.css";
ul {
font-family:
ui-monospace,
SFMono-Regular,
@@ -67,7 +67,7 @@ useIntersectionObserver(
monospace;
> li {
@apply has-[.clickable]:hover:bg-primary/10 flex px-2 py-1 break-words last:snap-end odd:bg-gray-400/[0.07] has-[.clickable]:cursor-pointer md:px-4;
@apply flex px-2 py-1 break-words last:snap-end odd:bg-gray-400/[0.07] md:px-4;
&:last-child {
scroll-margin-block-end: 5rem;
}
@@ -82,7 +82,7 @@ useIntersectionObserver(
}
&.large {
@apply text-lg;
@apply text-[1em];
}
&.compact {
@@ -99,6 +99,10 @@ useIntersectionObserver(
@apply bg-secondary inline-block rounded-xs;
animation: pops 200ms ease-out;
}
:deep(a[rel~="external"]) {
@apply text-primary underline-offset-4 hover:underline;
}
}
@keyframes pops {
@@ -1,44 +0,0 @@
<template>
<div class="flex gap-2">
<div
class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer"
v-if="isSupported"
:title="t('log_actions.copy_log')"
>
<span
class="text-primary rounded-sm bg-slate-800/60 px-1.5 py-1 hover:bg-slate-700"
@click.prevent="copyLogMessageToClipBoard()"
>
<carbon:copy-file />
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { LogEntry, JSONObject } from "@/models/LogEntry";
const { message } = defineProps<{
message: () => string;
logEntry: LogEntry<string | JSONObject>;
}>();
const { showToast } = useToast();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
async function copyLogMessageToClipBoard() {
await copy(message());
if (copied.value) {
showToast(
{
title: t("toasts.copied.title"),
message: t("toasts.copied.message"),
type: "info",
},
{ expire: 2000 },
);
}
}
</script>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<Tag size="small" :std="std" class="items-start!">
<Tag :std="std" class="items-start!">
{{ std }}
</Tag>
</template>
@@ -13,7 +13,7 @@ defineProps<{
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
[std="stdout"] {
@apply text-blue;
}
+1 -25
View File
@@ -6,37 +6,13 @@
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
messages: LogEntry<string | string[] | JSONObject>[];
visibleKeys: Map<string[], boolean>;
}>();
const { messages, visibleKeys } = toRefs(props);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { debouncedSearchFilter } = useSearchFilter();
const { streamConfig } = useLoggingContext();
const visibleMessages = filteredPayload(messages);
const router = useRouter();
watchEffect(() => {
const query = {} as Record<string, string>;
if (debouncedSearchFilter.value !== "") {
query.search = debouncedSearchFilter.value;
}
if (!streamConfig.value.stderr) {
query.stderr = streamConfig.value.stderr.toString();
}
if (!streamConfig.value.stdout) {
query.stdout = streamConfig.value.stdout.toString();
}
router.push({
query,
replace: true,
});
});
</script>
<style scoped></style>
@@ -1,21 +1,28 @@
<template>
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm gap-0.5 px-2">
<carbon:circle-solid class="text-red w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2.5" v-if="streamConfig.stdout" />
<div class="dropdown dropdown-end dropdown-hover z-20">
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
</label>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 z-50 w-52 p-1 shadow-sm">
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
@click="hideMenu"
>
<li>
<a @click.prevent="clear()">
<a @click="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
<li v-if="enableDownload">
<a :href="downloadUrl" download>
<octicon:download-24 />
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
</a>
</li>
<li>
<a @click.prevent="showSearch = true">
<a @click="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
@@ -84,26 +91,28 @@
<script lang="ts" setup>
const { showSearch } = useSearchFilter();
const { enableDownload } = config;
const clear = defineEmit();
const { streamConfig, showHostname, showContainerName, containers } = useLoggingContext();
const { name } = defineProps<{ name?: string }>();
const downloadParams = computed(() =>
Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
);
const { streamConfig, showHostname, showContainerName, containers, levels } = useLoggingContext();
const downloadUrl = computed(() =>
withBase(
`/api/containers/${containers.value.map((c) => c.host + "~" + c.id).join(",")}/download?${new URLSearchParams(downloadParams.value).toString()}`,
),
);
const { downloadUrl, isFiltered } = useDownloadUrl(containers, streamConfig, levels, name);
const hideMenu = (e: MouseEvent) => {
if (e.target instanceof HTMLAnchorElement) {
setTimeout(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, 50);
}
};
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
li.line {
@apply bg-base-content/20 h-px;
}
@@ -1,20 +1,62 @@
<template>
<div class="flex gap-4">
<StatMonitor :data="memoryData" label="mem" :stat-value="formatBytes(totalStat.memoryUsage)" />
<StatMonitor :data="cpuData" label="load" :stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'" />
<div class="flex gap-1 md:gap-4">
<div
class="grid hidden min-w-15 grid-cols-[auto_1fr_auto_1fr] items-center gap-0.5 text-xs leading-none sm:grid md:grid-cols-[auto_1fr]"
>
<PhArrowUp class="text-primary" />
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
<PhArrowDown class="text-secondary" />
<span class="tabular-nums">{{ formatBytes(networkRate.rx, { short: true, decimals: 1 }) }}/s</span>
</div>
<StatMonitor
:data="cpuData"
:icon="PhCpu"
:stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'"
:limit="roundCPU(limits.cpu) + ' CPU'"
container-class="border-primary/40 bg-primary/20"
text-class="hover:text-primary"
bar-class="bg-primary"
:formatter="(value: number) => value.toFixed(2) + '%'"
/>
<StatMonitor
:data="memoryData"
:icon="PhMemory"
:stat-value="formatBytes(totalStat.memoryUsage)"
:limit="formatBytes(limits.memory, { short: true, decimals: 1 })"
container-class="border-secondary/40 bg-secondary/20"
text-class="hover:text-secondary"
bar-class="bg-secondary"
:formatter="(value: number) => formatBytes(value)"
/>
</div>
</template>
<script lang="ts" setup>
import { Stat } from "@/models/Container";
import { Container } from "@/models/Container";
// @ts-ignore
import PhCpu from "~icons/ph/cpu";
// @ts-ignore
import PhMemory from "~icons/ph/memory";
const { containers } = defineProps<{
containers: Container[];
}>();
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 });
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
const { hosts } = useHosts();
const networkRate = ref({ rx: 0, tx: 0 });
const roundCPU = (num: number) => (Number.isInteger(num) ? num.toFixed(0) : num.toFixed(1));
function toContainerCores(container: Container): number {
if (container.cpuLimit && container.cpuLimit > 0) {
return 1;
}
const hostInfo = hosts.value[container.host];
return hostInfo?.nCPU ?? 1;
}
watch(
() => containers,
@@ -22,52 +64,115 @@ watch(
const initial: Stat[] = [];
for (let i = 1; i <= 300; i++) {
const stat = containers.reduce(
(acc, { statsHistory }) => {
const item = statsHistory.at(-i);
(acc, container) => {
const item = container.statsHistory.at(-i);
if (!item) {
return acc;
}
const cores = toContainerCores(container);
return {
cpu: acc.cpu + item.cpu,
cpu: acc.cpu + item.cpu / cores,
memory: acc.memory + item.memory,
memoryUsage: acc.memoryUsage + item.memoryUsage,
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0 },
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
);
initial.push(stat);
}
totalStat.value = initial[0];
reset({ initial: initial.reverse() });
},
{ immediate: true },
);
const limits = computed(() => {
// Group containers by host
const containersByHost = new Map<string, Container[]>();
containers.forEach((container) => {
if (!containersByHost.has(container.host)) {
containersByHost.set(container.host, []);
}
containersByHost.get(container.host)!.push(container);
});
let totalCpu = 0;
let totalMemory = 0;
// Process each host independently
containersByHost.forEach((hostContainers, hostId) => {
const hostInfo = hosts.value[hostId];
const hostTotalMemory = hostInfo?.memTotal || 0;
const hostTotalCpu = hostInfo?.nCPU || 0;
// Check if any container lacks limits
const hasUnlimitedCpu = hostContainers.some((c) => !c.cpuLimit || c.cpuLimit <= 0);
const hasUnlimitedMemory = hostContainers.some((c) => !c.memoryLimit);
// Calculate CPU for this host
if (hasUnlimitedCpu) {
// At least one container has no limit, use host total
totalCpu += hostTotalCpu;
} else {
// All containers have limits, sum them up (capped at host total)
const sumCpu = hostContainers.reduce((sum, c) => sum + (c.cpuLimit || 0), 0);
totalCpu += Math.min(sumCpu, hostTotalCpu);
}
// Calculate Memory for this host
if (hasUnlimitedMemory) {
// At least one container has no limit, use host total
totalMemory += hostTotalMemory;
} else {
// All containers have limits, sum them up (capped at host total)
const sumMemory = hostContainers.reduce((sum, c) => sum + (c.memoryLimit || 0), 0);
totalMemory += Math.min(sumMemory, hostTotalMemory);
}
});
return {
cpu: totalCpu,
memory: totalMemory,
};
});
useIntervalFn(() => {
const previousStat = totalStat.value;
totalStat.value = containers.reduce(
(acc, { stat }) => {
(acc, container) => {
const cores = toContainerCores(container);
return {
cpu: acc.cpu + stat.cpu,
memory: acc.memory + stat.memory,
memoryUsage: acc.memoryUsage + stat.memoryUsage,
cpu: acc.cpu + container.stat.cpu / cores,
memory: acc.memory + container.stat.memory,
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0 },
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
);
networkRate.value = {
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
tx: Math.max(0, totalStat.value.networkTxTotal - previousStat.networkTxTotal),
};
}, 1000);
const cpuData = computed(() =>
history.value.map((stat, i) => ({
x: i,
y: Math.max(0, stat.cpu),
value: Math.max(0, stat.cpu).toFixed(2) + "%",
value: Math.max(0, stat.cpu),
})),
);
const memoryData = computed(() =>
history.value.map((stat, i) => ({
x: i,
y: stat.memoryUsage,
value: formatBytes(stat.memoryUsage),
y: stat.memory,
value: stat.memoryUsage,
})),
);
</script>
@@ -1,5 +1,5 @@
<template>
<div class="tag grid w-40 overflow-hidden rounded-sm text-center text-sm text-white">
<div class="tag grid overflow-hidden rounded-sm text-center text-sm text-white">
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
<div class="col-start-1 row-start-1 truncate px-2 brightness-100" :class="truncateRight ? '[direction:rtl]' : ''">
{{ value }}
+3 -20
View File
@@ -1,21 +1,15 @@
<template>
<LogItem :logEntry>
<LogLevel class="flex select-none" :level="logEntry.level" />
<div
class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap"
v-html="linkify(colorize(logEntry.message))"
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
v-html="colorize(logEntry.message)"
></div>
<LogMessageActions
class="absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100"
:message="() => decodeXML(stripAnsi(logEntry.message))"
:log-entry="logEntry"
/>
</LogItem>
</template>
<script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry";
import { decodeXML } from "entities";
import AnsiConvertor from "ansi-to-html";
import stripAnsi from "strip-ansi";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
@@ -28,15 +22,4 @@ defineProps<{
}>();
const colorize = (value: string) => ansiConvertor.toHtml(value);
const urlPattern =
/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_+.~#?&/=]*/g;
const linkify = (text: string) =>
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
</script>
<style scoped>
@import "@/main.css" reference;
.log-wrapper :deep(a) {
@apply text-primary underline-offset-4 hover:underline;
}
</style>
@@ -2,16 +2,16 @@
<div class="my-4 flex-1 text-center">
<div class="relative">
<ZigZag class="absolute inset-0 mt-2" />
<span class="bg-base-200 relative px-4 py-2 font-bold whitespace-pre-wrap">
<button class="btn btn-primary btn-xs relative whitespace-pre-wrap" @click="logEntry.loadSkippedEntries()">
{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}
</span>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { SkippedLogsEntry } from "@/models/LogEntry";
defineProps<{
const { logEntry } = defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
+53 -12
View File
@@ -1,25 +1,66 @@
<template>
<div class="hover:text-secondary relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="border-primary hidden overflow-hidden rounded-xs border px-px pt-1 pb-px md:flex">
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
<div class="relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false" :class="textClass">
<div class="overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden" :class="containerClass">
<BarChart
:chart-data="chartData"
:bar-class="`${barClass} opacity-70 hover:opacity-100`"
class="h-8 w-44"
@hover-index="(startIndex: number, endIndex: number) => onHoverIndexChange(startIndex, endIndex)"
/>
</div>
<div class="bg-base-200 inline-flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
<div class="font-light uppercase">{{ label }}</div>
<div class="font-bold select-none">
{{ mouseOver ? (selectedPoint?.value ?? selectedPoint?.y ?? statValue) : statValue }}
<div class="bg-base-200 flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
<component :is="icon" class="text-sm" />
<div class="font-bold tabular-nums select-none">
{{ displayValue }}
<span v-if="limit !== -1 && !mouseOver" class="max-md:hidden"> / {{ limit }} </span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
import type { Component } from "vue";
let selectedPoint: Point<unknown> | undefined = $ref();
const {
data,
icon,
statValue,
limit = -1,
containerClass = "border-primary",
textClass = "",
barClass = "bg-primary",
formatter,
} = defineProps<{
data: Point<unknown>[];
icon: Component;
statValue: string | number;
limit?: string | number;
containerClass?: string;
textClass?: string;
barClass?: string;
formatter?: (value: number) => string;
}>();
function onSelectedPoint(point: Point<unknown>) {
selectedPoint = point;
const chartData = computed(() => data.map((point) => (point.y as number) ?? 0));
const mouseOver = ref(false);
const hoveredRange = ref<{ start: number; end: number } | null>(null);
function onHoverIndexChange(startIndex: number, endIndex: number) {
hoveredRange.value = { start: startIndex, end: endIndex };
}
let mouseOver = $ref(false);
const displayValue = computed(() => {
if (mouseOver.value && hoveredRange.value !== null) {
const { start, end } = hoveredRange.value;
const points = data.slice(start, end + 1);
const sum = points.reduce((acc, point) => acc + ((point.value as number) ?? (point.y as number) ?? 0), 0);
const avg = sum / points.length;
if (formatter) {
return formatter(avg);
}
return avg.toFixed(2);
}
return statValue;
});
</script>
@@ -1,44 +0,0 @@
<template>
<svg :width="width" :height="height" @mousemove="onMove" class="group">
<path :d="path" class="fill-primary" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="stroke-secondary invisible stroke-2 group-hover:visible" />
</svg>
</template>
<script lang="ts" setup>
import { extent } from "d3-array";
import { scaleLinear } from "d3-scale";
import { area, curveStep } from "d3-shape";
const d3 = { extent, scaleLinear, area, curveStep };
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const selectedPoint = defineEmit<[value: Point<unknown>]>();
const shape = d3
.area<Point<unknown>>()
.curve(d3.curveStep)
.x((d) => x(d.x))
.y0(height)
.y1((d) => y(d.y));
const path = computed(() => {
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
y.domain(d3.extent([...data, { y: 1 }], (d) => d.y) as [number, number]);
return shape(data) ?? "";
});
let lineX = $ref(0);
function onMove(e: MouseEvent) {
const { offsetX } = e;
const xValue = x.invert(offsetX);
const index = Math.round(xValue);
lineX = x(index);
const point = data[index];
selectedPoint(point);
}
</script>
@@ -1,73 +1,138 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium" show-container-name="false">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
</svg></button>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
<!--v-if-->
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
<li><a><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M21 19v1H3v-1l2-2v-6c0-3.1 2.03-5.83 5-6.71V4a2 2 0 0 1 2-2a2 2 0 0 1 2 2v.29c2.97.88 5 3.61 5 6.71v6zm-7 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"></path>
</svg> action.create-alert</a></li>
</ul>
</div>
<!--v-if-->
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
<div data-v-a49e52d4="" class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
<!--v-if-->
</div>
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">foo bar</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium" show-container-name="false">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
</svg></button>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
<!--v-if-->
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
<li><a><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M21 19v1H3v-1l2-2v-6c0-3.1 2.03-5.83 5-6.71V4a2 2 0 0 1 2-2a2 2 0 0 1 2 2v.29c2.97.88 5 3.61 5 6.71v6zm-7 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"></path>
</svg> action.create-alert</a></li>
</ul>
</div>
<!--v-if-->
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
</div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
<div data-v-a49e52d4="" class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
<!--v-if-->
</div>
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">foo bar</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium" show-container-name="false">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
</li>
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
</svg></button>
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
<!--v-if-->
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
</svg> action.copy-log</a></li>
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
</svg> action.copy-link</a></li>
<!--v-if-->
<li><a><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
<path fill="currentColor" d="M21 19v1H3v-1l2-2v-6c0-3.1 2.03-5.83 5-6.71V4a2 2 0 0 1 2-2a2 2 0 0 1 2 2v.29c2.97.88 5 3.61 5 6.71v6zm-7 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2"></path>
</svg> action.create-alert</a></li>
</ul>
</div>
<!--v-if-->
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none" size="small">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
<!--v-if-->
<!--v-if-->
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
</div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
<div data-v-a49e52d4="" class="log-wrapper [word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
<div data-v-a49e52d4="" class="flex gap-2 absolute -right-1 opacity-0 transition-opacity delay-150 duration-250 group-hover/entry:opacity-100">
<!--v-if-->
</div>
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > should parse messages 1`] = `
SimpleLogEntry {
"_message": "This is a message.",
"containerID": undefined,
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
LoadMoreLogEntry {
"_message": "",
"containerID": "",
"date": 2019-06-12T10:55:42.709Z,
"id": 1560336942709,
"level": undefined,
"position": undefined,
"loader": [Function],
"rawMessage": "info",
"rememberScrollPosition": true,
"std": "stderr",
}
`;
+56
View File
@@ -0,0 +1,56 @@
<template>
<div class="rounded-lg border p-2 md:p-3" :class="containerClass">
<div class="mb-2 flex items-center gap-1.5 text-sm font-medium" :class="textClass">
<component :is="icon" class="text-lg" />
<span>{{ label }}</span>
</div>
<div class="mb-1.5 text-lg font-semibold tabular-nums">{{ formattedValue }}</div>
<div class="text-base-content/60 mb-1 text-xs tabular-nums max-md:hidden">
avg {{ formatValue(average) }} pk {{ formatValue(peak) }}
</div>
<BarChart class="h-8" :chartData="percentData" :barClass="barClass" />
</div>
</template>
<script setup lang="ts">
import type { Component } from "vue";
export interface MetricDataPoint {
percent: number; // value 0 - 100
value: number;
}
const {
label,
icon,
value,
chartData,
containerClass = "",
textClass = "",
barClass = "",
formatValue = (v: number) => v.toString(),
} = defineProps<{
label: string;
icon: Component;
value: string | number;
chartData: MetricDataPoint[];
containerClass?: string;
textClass?: string;
barClass?: string;
formatValue?: (value: number) => string;
}>();
const percentData = computed(() => chartData.map((d) => d.percent));
const peak = computed(() => (chartData.length > 0 ? Math.max(...chartData.map((d) => d.value)) : 0));
const average = computed(() => {
if (chartData.length === 0) return 0;
return chartData.reduce((sum, d) => sum + d.value, 0) / chartData.length;
});
const formattedValue = computed(() => {
if (typeof value === "string") return value;
return formatValue(value);
});
</script>
@@ -1,15 +1,11 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 gap-1.5 truncate md:gap-2">
<octicon:container-24 />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ containers.length }} containers</div>
</div>
</div>
<div class="mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
<octicon:container-24 />
<ContainerDropdown :containers="containers">{{ $t("label.container", containers.length) }}</ContainerDropdown>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
<MultiContainerActionToolbar @clear="viewer?.clear()" />
</div>
</template>
<template #default>
@@ -0,0 +1,147 @@
<template>
<div class="card bg-base-100 shadow-sm" :class="{ 'opacity-60': !alert.enabled }">
<div class="card-body gap-4 p-5">
<!-- Header -->
<div class="flex items-start justify-between">
<div class="flex items-center gap-2">
<h4 class="flex items-center gap-2 text-lg font-semibold">
<mdi:chart-line v-if="alert.metricExpression" class="text-info" />
<mdi:text-box-outline v-else class="text-info" />
<span>{{ alert.name }}</span> <span class="text-sm font-light"></span>
<div class="group/dispatch dropdown dropdown-hover">
<div
tabindex="0"
role="button"
class="border-base-content/0 hover:border-base-content/20 flex cursor-pointer items-center gap-1 rounded border px-1.5 py-0.5 text-xs font-light transition-colors"
:class="{ 'text-warning': !alert.dispatcher }"
>
<template v-if="alert.dispatcher">
<mdi:webhook v-if="alert.dispatcher.type === 'webhook'" />
<mdi:cloud v-else />
{{ alert.dispatcher.name }}
</template>
<template v-else>
<mdi:alert-outline />
{{ $t("notifications.alert.dispatcher-deleted") }}
</template>
<mdi:chevron-down class="text-[0.6rem] opacity-0 transition-opacity group-hover/dispatch:opacity-100" />
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-50 w-48 p-2 shadow-lg">
<li v-for="dest in dispatchers" :key="dest.id">
<a
class="flex items-center gap-2"
:class="{ active: dest.id === alert.dispatcher?.id }"
@click="changeDispatcher(dest.id)"
>
<mdi:webhook v-if="dest.type === 'webhook'" />
<mdi:cloud v-else />
{{ dest.name }}
</a>
</li>
</ul>
</div>
</h4>
<span v-if="!alert.enabled" class="badge badge-warning badge-sm">{{ $t("notifications.alert.paused") }}</span>
</div>
<input type="checkbox" class="toggle toggle-primary" :checked="alert.enabled" @change="toggleEnabled" />
</div>
<!-- Expressions -->
<div class="text-base-content/80 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
<span>{{ $t("notifications.alert.containers") }}</span>
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.containerExpression }}</code>
<template v-if="alert.metricExpression">
<span>{{ $t("notifications.alert.metric-filter") }}</span>
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.metricExpression }}</code>
<span>{{ $t("notifications.alert.cooldown") }}</span>
<span>{{ formatDuration(alert.cooldown || 300, locale || undefined) }}</span>
</template>
<template v-else>
<span>{{ $t("notifications.alert.log-filter") }}</span>
<code class="bg-base-200 text-base-content rounded px-2 py-0.5 font-mono">{{ alert.logExpression }}</code>
</template>
</div>
<!-- Footer -->
<div class="border-base-content/10 text-base-content/80 flex items-center justify-between border-t pt-3 text-xs">
<div class="flex items-center gap-4">
<span>
{{ $t("notifications.alert.containers-count", { count: alert.triggeredContainers }) }}
</span>
<span>
{{ $t("notifications.alert.triggered-count", { count: alert.triggerCount }) }}
</span>
<span v-if="alert.lastTriggeredAt">
{{ $t("notifications.alert.last-triggered", { time: formatTimeAgo(alert.lastTriggeredAt) }) }}
</span>
</div>
<div class="flex items-center gap-1">
<button class="btn btn-ghost btn-square" @click="editAlert">
<mdi:pencil-outline />
</button>
<button class="btn btn-ghost btn-square" @click="deleteAlert" :disabled="isDeleting">
<span v-if="isDeleting" class="loading loading-spinner loading-xs"></span>
<mdi:trash-can-outline v-else />
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Dispatcher, NotificationRule } from "@/types/notifications";
import AlertForm from "./AlertForm.vue";
const { alert, onUpdated } = defineProps<{
alert: NotificationRule;
onUpdated?: () => void;
}>();
const showDrawer = useDrawer();
const isDeleting = ref(false);
const dispatchers = ref<Dispatcher[]>([]);
onMounted(async () => {
const res = await fetch(withBase("/api/notifications/dispatchers"));
dispatchers.value = await res.json();
});
async function changeDispatcher(id: number) {
await fetch(withBase(`/api/notifications/rules/${alert.id}`), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dispatcherId: id }),
});
onUpdated?.();
}
function formatTimeAgo(dateStr: string): string {
const date = new Date(dateStr);
if (date.getFullYear() === 0) return "-";
return toRelativeTime(date, undefined);
}
async function toggleEnabled() {
await fetch(withBase(`/api/notifications/rules/${alert.id}`), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: !alert.enabled }),
});
onUpdated?.();
}
function editAlert() {
showDrawer(AlertForm, { alert, onCreated: onUpdated }, "lg");
}
async function deleteAlert() {
isDeleting.value = true;
try {
await fetch(withBase(`/api/notifications/rules/${alert.id}`), { method: "DELETE" });
onUpdated?.();
} finally {
isDeleting.value = false;
}
}
</script>
@@ -0,0 +1,214 @@
<template>
<div class="space-y-4 p-4">
<div class="mb-6">
<h2 class="text-2xl font-bold">
{{ isEditing ? $t("notifications.alert-form.edit-title") : $t("notifications.alert-form.create-title") }}
</h2>
<p class="text-base-content/60">{{ $t("notifications.alert-form.description") }}</p>
</div>
<!-- Alert Name -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.alert-name") }}</legend>
<input
ref="alertNameInput"
v-model="alertName"
type="text"
class="input focus:input-primary w-full text-base"
:class="alertName.trim() ? 'input-primary' : ''"
required
:placeholder="$t('notifications.alert-form.alert-name-placeholder')"
/>
</fieldset>
<!-- Alert Type Toggle -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.alert-type") }}</legend>
<div class="flex gap-2">
<button
class="btn btn-sm"
:class="alertType === 'log' ? 'btn-primary' : 'btn-outline'"
@click="alertType = 'log'"
>
<mdi:text-box-outline class="mr-1" />
{{ $t("notifications.alert-form.log-alert") }}
</button>
<button
class="btn btn-sm"
:class="alertType === 'metric' ? 'btn-primary' : 'btn-outline'"
@click="alertType = 'metric'"
>
<mdi:chart-line class="mr-1" />
{{ $t("notifications.alert-form.metric-alert") }}
</button>
</div>
</fieldset>
<!-- Container Filter -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.container-filter") }}</legend>
<div
class="input focus-within:input-primary w-full focus-within:z-50"
:class="
containerExpression.trim() && !containerResult?.error
? 'input-primary'
: { 'input-error!': containerResult?.error }
"
>
<div ref="containerEditorRef" class="w-full"></div>
</div>
<div v-if="containerResult" class="fieldset-label">
<span v-if="containerResult.error" class="text-error">{{ containerResult.error }}</span>
<span v-else-if="containerResult.containers?.length" class="text-success">
<mdi:check class="inline" />
{{
$t("notifications.alert-form.containers-match", {
count: containerResult.containers.length,
names: containerResult.containers.map((c) => c.name).join(", "),
})
}}
</span>
<span v-else class="text-warning">
<mdi:alert class="inline" />
{{ $t("notifications.alert-form.no-containers-match") }}
</span>
</div>
</fieldset>
<!-- Type-specific fields -->
<KeepAlive>
<LogAlertFields
v-if="alertType === 'log'"
ref="fieldsRef"
:alert="alert"
:prefill="prefill"
:container-expression="containerExpression"
:is-loading="isLoading"
:validate-preview="validatePreview"
/>
<MetricAlertFields
v-else
ref="fieldsRef"
:alert="alert"
:prefill="prefill"
:container-expression="containerExpression"
:is-loading="isLoading"
:validate-preview="validatePreview"
/>
</KeepAlive>
<!-- Destination -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.destination") }}</legend>
<details class="dropdown w-full" ref="destinationDropdown">
<summary class="btn btn-outline w-full justify-between" :class="{ 'btn-primary': selectedDestination }">
<span class="flex items-center gap-2">
<template v-if="selectedDestination">
<mdi:webhook v-if="selectedDestination.type === 'webhook'" />
<mdi:cloud v-else />
{{ selectedDestination.name }}
</template>
<span v-else class="text-base-content/60">{{ $t("notifications.alert-form.select-destination") }}</span>
</span>
<carbon:caret-down />
</summary>
<ul class="dropdown-content menu bg-base-200 rounded-box z-50 mt-1 w-full border p-2 shadow-sm">
<li v-for="dest in destinations" :key="dest.id">
<a
@click="
dispatcherId = dest.id;
destinationDropdown?.removeAttribute('open');
"
:class="{ active: dispatcherId === dest.id }"
>
<mdi:webhook v-if="dest.type === 'webhook'" />
<mdi:cloud v-else />
{{ dest.name }}
</a>
</li>
</ul>
</details>
<div v-if="!destinations.length" class="fieldset-label">
<span class="text-warning">
<mdi:alert class="inline" />
{{ $t("notifications.alert-form.no-destinations") }}
</span>
</div>
</fieldset>
<!-- Error -->
<div v-if="saveError" class="alert alert-error">
<span>{{ saveError }}</span>
</div>
<!-- Actions -->
<div class="flex justify-end gap-2 pt-4">
<button class="btn" @click="close?.()">{{ $t("notifications.alert-form.cancel") }}</button>
<button class="btn btn-primary" :disabled="!canSave" @click="save">
<span v-if="isSaving" class="loading loading-spinner loading-sm"></span>
{{ isEditing ? $t("notifications.alert-form.save") : $t("notifications.alert-form.create") }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAlertForm } from "@/composable/alertForm";
import LogAlertFields from "./LogAlertFields.vue";
import MetricAlertFields from "./MetricAlertFields.vue";
import type { NotificationRule } from "@/types/notifications";
const props = defineProps<{
close?: () => void;
onCreated?: () => void;
alert?: NotificationRule;
prefill?: { name?: string; containerExpression?: string; logExpression?: string; metricExpression?: string };
}>();
const {
isEditing,
alertName,
containerExpression,
dispatcherId,
destinations,
selectedDestination,
containerResult,
isLoading,
isSaving,
saveError,
baseCanSave,
initContainerEditor,
saveAlert,
validatePreview,
} = useAlertForm(props);
// Template refs
const alertNameInput = ref<HTMLInputElement>();
const containerEditorRef = ref<HTMLElement>();
const destinationDropdown = ref<HTMLDetailsElement>();
const fieldsRef = ref<InstanceType<typeof LogAlertFields> | InstanceType<typeof MetricAlertFields>>();
useFocus(alertNameInput, { initialValue: true });
// Alert type
const alertType = ref<"log" | "metric">(props.alert?.metricExpression ? "metric" : "log");
const canSave = computed(() => baseCanSave.value && (fieldsRef.value?.canSave ?? false));
async function save() {
if (!canSave.value || !fieldsRef.value) return;
await saveAlert(fieldsRef.value.typeFields);
}
// Container editor
let containerEditorView: Awaited<ReturnType<typeof initContainerEditor>> | undefined;
onMounted(async () => {
if (containerEditorRef.value) {
containerEditorView = await initContainerEditor(containerEditorRef.value);
}
});
onScopeDispose(() => {
containerEditorView?.destroy();
});
</script>
@@ -0,0 +1,133 @@
<template>
<div class="space-y-4">
<!-- Cloud linked (when editing with prefix) -->
<fieldset v-if="destination?.prefix" class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.api-key") }}</legend>
<div class="join w-full">
<input
type="text"
:value="destination.prefix + '**************************************'"
readonly
disabled
class="input join-item w-full font-mono"
:class="cloudStatusError ? 'input-error' : 'input-success'"
/>
<span class="join-item btn pointer-events-none" :class="cloudStatusError ? 'btn-error' : 'btn-success'">
<mdi:alert-circle v-if="cloudStatusError" class="text-lg" />
<mdi:check v-else class="text-lg" />
</span>
</div>
<!-- Cloud Status -->
<div v-if="isLoadingCloudStatus" class="mt-3 flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span class="text-base-content/60 text-sm">{{ $t("notifications.destination-form.cloud-checking") }}</span>
</div>
<div v-else-if="cloudStatusError" class="mt-3">
<div class="alert alert-error">
<mdi:alert-circle class="text-lg" />
<span>{{ $t("notifications.destination-form.cloud-relink") }}</span>
</div>
</div>
<div v-else-if="cloudStatus" class="mt-3 space-y-3">
<div class="flex items-center justify-between text-sm">
<span class="text-base-content/60">{{ $t("notifications.destination-form.cloud-plan") }}</span>
<span class="badge badge-primary badge-sm capitalize">{{ cloudStatus.plan.name }}</span>
</div>
<div>
<div class="mb-1 flex items-center justify-between text-sm">
<span class="text-base-content/60">{{ $t("notifications.destination-form.cloud-usage") }}</span>
<span
>{{ cloudStatus.usage.events_used.toLocaleString() }} /
{{ cloudStatus.usage.events_limit.toLocaleString() }}</span
>
</div>
<progress
class="progress w-full"
:class="usagePercent > 90 ? 'progress-error' : usagePercent > 70 ? 'progress-warning' : 'progress-primary'"
:value="cloudStatus.usage.events_used"
:max="cloudStatus.usage.events_limit"
></progress>
</div>
</div>
<p class="text-base-content/60 mt-2 text-sm">
{{ $t("notifications.destination-form.cloud-settings-hint") }}
<a :href="cloudSettingsUrl" target="_blank" class="link link-primary">
{{ $t("notifications.destination-form.cloud-settings-link") }}
</a>
</p>
</fieldset>
<!-- Link Dozzle Cloud (when creating or not linked) -->
<div v-else class="card card-border border-primary/30 bg-primary/5">
<div class="card-body items-center text-center">
<mdi:cloud-outline class="text-primary text-4xl" />
<h3 class="card-title">{{ $t("notifications.destination-form.link-cloud") }}</h3>
<p class="text-base-content/60 text-sm">{{ $t("notifications.destination-form.cloud-description") }}</p>
<a :href="cloudLinkUrl" class="btn btn-primary btn-lg mt-2">
<mdi:link-variant class="text-lg" />
{{ $t("notifications.destination-form.link-cloud-button") }}
</a>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 pt-4">
<div class="flex-1"></div>
<button class="btn btn-primary" @click="close?.()">
{{ $t("notifications.destination-form.close") }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Dispatcher } from "@/types/notifications";
const { destination, close } = defineProps<{
destination?: Dispatcher;
close?: () => void;
}>();
const callbackUrl = `${window.location.origin}${withBase("/")}`;
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
const cloudSettingsUrl = `${__CLOUD_URL__}/settings`;
// Cloud status
interface CloudStatus {
user: { email: string; name: string };
plan: { name: string; events_per_month: number; retention_days: number };
usage: { events_used: number; events_limit: number; period: string };
}
const cloudStatus = ref<CloudStatus | null>(null);
const cloudStatusError = ref(false);
const isLoadingCloudStatus = ref(false);
const usagePercent = computed(() => {
if (!cloudStatus.value) return 0;
return (cloudStatus.value.usage.events_used / cloudStatus.value.usage.events_limit) * 100;
});
async function fetchCloudStatus() {
isLoadingCloudStatus.value = true;
cloudStatusError.value = false;
try {
const res = await fetch(withBase("/api/cloud/status"));
if (!res.ok) {
cloudStatusError.value = true;
return;
}
cloudStatus.value = await res.json();
} catch {
cloudStatusError.value = true;
} finally {
isLoadingCloudStatus.value = false;
}
}
if (destination?.prefix) {
fetchCloudStatus();
}
</script>
@@ -0,0 +1,68 @@
<template>
<div class="card bg-base-100 hover:border-primary cursor-pointer border border-transparent" @click="editDestination">
<div class="card-body gap-2 p-4">
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-lg">
<mdi:webhook v-if="destination.type === 'webhook'" class="text-lg" />
<mdi:cloud v-else class="text-primary-content text-lg" />
</div>
<div class="flex-1">
<h4 class="font-semibold">{{ destination.name }}</h4>
<p class="text-base-content/60 text-sm">
{{
destination.type === "webhook"
? $t("notifications.destination.http-webhook")
: $t("notifications.destination.dozzle-cloud")
}}
</p>
</div>
<div class="dropdown dropdown-end" @click.stop>
<label tabindex="0" class="btn btn-ghost btn-sm btn-square">
<ion:ellipsis-vertical />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-100 border-base-content/20 z-50 w-40 border p-1 shadow-sm"
>
<li>
<a @click="editDestination">{{ $t("notifications.destination.edit") }}</a>
</li>
<li>
<a class="text-error" @click="deleteDestination">{{ $t("notifications.destination.delete") }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Dispatcher } from "@/types/notifications";
import DestinationForm from "./DestinationForm.vue";
const { destination, onUpdated, existingDispatchers } = defineProps<{
destination: Dispatcher;
onUpdated?: () => void;
existingDispatchers: Dispatcher[];
}>();
const showDrawer = useDrawer();
function editDestination() {
showDrawer(
DestinationForm,
{
destination,
onCreated: onUpdated,
existingDispatchers,
},
"md",
);
}
async function deleteDestination() {
await fetch(withBase(`/api/notifications/dispatchers/${destination.id}`), { method: "DELETE" });
onUpdated?.();
}
</script>
@@ -0,0 +1,108 @@
<template>
<div class="space-y-4 p-4">
<div class="mb-6">
<h2 class="text-2xl font-bold">
{{
isEditing
? $t("notifications.destination-form.edit-title")
: $t("notifications.destination-form.create-title")
}}
</h2>
<p class="text-base-content/60">{{ $t("notifications.destination-form.description") }}</p>
</div>
<!-- Link Success Alert -->
<div v-if="showLinkSuccess" class="alert alert-success">
<mdi:check-circle class="text-lg" />
<div>
<div class="font-semibold">{{ $t("notifications.cloud-link-success.title") }}</div>
<div class="text-sm">{{ $t("notifications.cloud-link-success.message") }}</div>
</div>
</div>
<!-- Type Selection -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.type") }}</legend>
<div class="space-y-3">
<label
class="card card-border 20 cursor-pointer transition-colors"
:class="type === 'webhook' ? 'border-primary bg-primary/10' : ''"
>
<div class="card-body flex-row items-center gap-3 p-4">
<input type="radio" v-model="type" value="webhook" class="radio radio-primary" />
<div>
<div class="font-semibold">{{ $t("notifications.destination-form.webhook-title") }}</div>
<div class="text-base-content/60 text-sm">
{{ $t("notifications.destination-form.webhook-description") }}
</div>
</div>
</div>
</label>
<label
class="card card-border border-base-content/20 transition-colors"
:class="[
type === 'cloud' ? 'border-primary bg-primary/10' : '',
hasExistingCloudDestination && type !== 'cloud' ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
]"
>
<div class="card-body flex-row items-center gap-3 p-4">
<input
type="radio"
v-model="type"
value="cloud"
class="radio radio-primary"
:disabled="hasExistingCloudDestination && type !== 'cloud'"
/>
<div>
<div class="font-semibold">{{ $t("notifications.destination-form.cloud-title") }}</div>
<div class="text-base-content/60 text-sm">
{{ $t("notifications.destination-form.cloud-description") }}
</div>
<div v-if="hasExistingCloudDestination && type !== 'cloud'" class="text-warning mt-1 text-xs">
{{ $t("notifications.destination-form.cloud-exists") }}
</div>
</div>
</div>
</label>
</div>
</fieldset>
<!-- Type-specific form -->
<WebhookDestinationForm
v-if="type === 'webhook'"
:destination="destination"
:close="close"
:on-created="onCreated"
:is-editing="isEditing"
/>
<CloudDestinationForm v-else :destination="destination" :close="close" />
</div>
</template>
<script lang="ts" setup>
import type { Dispatcher } from "@/types/notifications";
import WebhookDestinationForm from "./WebhookDestinationForm.vue";
import CloudDestinationForm from "./CloudDestinationForm.vue";
const {
close,
onCreated,
destination,
existingDispatchers = [],
showLinkSuccess = false,
} = defineProps<{
close?: () => void;
onCreated?: () => void;
destination?: Dispatcher;
existingDispatchers?: Dispatcher[];
showLinkSuccess?: boolean;
}>();
const isEditing = !!destination;
const type = ref<"webhook" | "cloud">((destination?.type as "webhook" | "cloud") ?? "webhook");
const hasExistingCloudDestination = computed(() => {
const others = isEditing ? existingDispatchers.filter((d) => d.id !== destination!.id) : existingDispatchers;
return others.some((d) => d.type === "cloud");
});
</script>
@@ -0,0 +1,114 @@
<template>
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.log-filter") }}</legend>
<div
class="input focus-within:input-primary w-full focus-within:z-50"
:class="logExpression.trim() && !logError ? 'input-primary' : { 'input-error!': logError }"
>
<div ref="logEditorRef" class="w-full"></div>
</div>
<div v-if="logError || logExpression" class="fieldset-label">
<span v-if="logError" class="text-error">{{ logError }}</span>
<span v-else-if="logMessages.length" class="text-success">
<mdi:check class="inline" />
{{ $t("notifications.alert-form.logs-match", { count: logTotalCount }) }}
</span>
<span v-else-if="!props.isLoading" class="text-warning">
<mdi:alert class="inline" />
{{ $t("notifications.alert-form.no-logs-match") }}
</span>
</div>
</fieldset>
<!-- Log Preview -->
<div v-if="logMessages.length" class="mt-4">
<div class="mb-2 text-lg">{{ $t("notifications.alert-form.preview") }}</div>
<LogList
:messages="logMessages"
:last-selected-item="undefined"
class="border-base-content/50 h-64 overflow-hidden rounded-lg border"
/>
</div>
</template>
<script lang="ts" setup>
import { type LogEvent, type LogEntry, type LogMessage, asLogEntry } from "@/models/LogEntry";
import { createExprEditor, createLogHints } from "@/composable/exprEditor";
import type { NotificationRule, PreviewResult } from "@/types/notifications";
const props = defineProps<{
alert?: NotificationRule;
prefill?: { logExpression?: string };
containerExpression: string;
isLoading: boolean;
validatePreview: (extra: Record<string, unknown>) => Promise<{ data: PreviewResult | null }>;
}>();
const logExpression = ref(props.alert?.logExpression ?? props.prefill?.logExpression ?? "");
const logError = ref<string | null>(null);
const logTotalCount = ref(0);
const logMessages = shallowRef<LogEntry<LogMessage>[]>([]);
const messageKeys = ref<string[]>([]);
const canSave = computed(() => !logError.value);
const typeFields = computed(() => ({ logExpression: logExpression.value, metricExpression: "", cooldown: 0 }));
defineExpose({ canSave, typeFields });
// Validation
async function validate() {
if (!props.containerExpression && !logExpression.value) {
logError.value = null;
logTotalCount.value = 0;
logMessages.value = [];
messageKeys.value = [];
return;
}
const { data } = await props.validatePreview({
logExpression: logExpression.value || undefined,
});
if (data) {
messageKeys.value = data.messageKeys ?? [];
if (logExpression.value && !data.containerError) {
logError.value = data.logError ?? null;
logTotalCount.value = data.totalLogs;
logMessages.value = data.matchedLogs?.map((event) => asLogEntry(event as LogEvent)) ?? [];
} else {
logError.value = null;
logTotalCount.value = 0;
logMessages.value = [];
}
}
}
const debouncedValidate = useDebounceFn(validate, 500);
watch(
[() => props.containerExpression, logExpression],
() => {
debouncedValidate();
},
{ immediate: true },
);
// Editor
const logEditorRef = ref<HTMLElement>();
let logEditorView: Awaited<ReturnType<typeof createExprEditor>> | undefined;
onMounted(async () => {
if (logEditorRef.value) {
logEditorView = await createExprEditor({
parent: logEditorRef.value,
placeholder: 'level == "error" && message contains "timeout"',
initialValue: props.alert?.logExpression ?? props.prefill?.logExpression ?? "",
getHints: () => createLogHints(messageKeys.value),
onChange: (v) => (logExpression.value = v),
});
}
});
onScopeDispose(() => {
logEditorView?.destroy();
});
</script>
@@ -0,0 +1,104 @@
<template>
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.metric-filter") }}</legend>
<div
class="input focus-within:input-primary w-full focus-within:z-50"
:class="metricExpression.trim() && !metricError ? 'input-primary' : { 'input-error!': metricError }"
>
<div ref="metricEditorRef" class="w-full"></div>
</div>
<div v-if="metricError || metricExpression" class="fieldset-label">
<span v-if="metricError" class="text-error">{{ metricError }}</span>
<span v-else class="text-success">
<mdi:check class="inline" />
{{ $t("notifications.alert-form.expression-valid") }}
</span>
</div>
<p class="text-base-content/50 mt-1 text-xs">
{{
$t("notifications.alert-form.metric-fields-hint", {
fields: "cpu (CPU %), memory (memory %), memoryUsage (bytes)",
})
}}
</p>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.alert-form.cooldown-label") }}</legend>
<input v-model.number="cooldown" type="range" min="10" max="3600" step="10" class="range range-primary" />
<p class="text-base-content/50 mt-1 text-xs">
{{ $t("notifications.alert-form.cooldown-hint", { duration: formatDuration(cooldown, locale || undefined) }) }}
</p>
</fieldset>
</template>
<script lang="ts" setup>
import { createExprEditor, createMetricHints } from "@/composable/exprEditor";
import type { NotificationRule, PreviewResult } from "@/types/notifications";
const props = defineProps<{
alert?: NotificationRule;
prefill?: { metricExpression?: string };
containerExpression: string;
isLoading: boolean;
validatePreview: (extra: Record<string, unknown>) => Promise<{ data: PreviewResult | null }>;
}>();
const metricExpression = ref(props.alert?.metricExpression ?? props.prefill?.metricExpression ?? "");
const metricError = ref<string | null>(null);
const cooldown = ref(props.alert?.cooldown ?? 300);
const canSave = computed(() => !!metricExpression.value.trim() && !metricError.value);
const typeFields = computed(() => ({
metricExpression: metricExpression.value,
logExpression: "",
cooldown: cooldown.value,
}));
defineExpose({ canSave, typeFields });
// Validation
async function validate() {
if (!props.containerExpression && !metricExpression.value) {
metricError.value = null;
return;
}
const { data } = await props.validatePreview({
metricExpression: metricExpression.value || undefined,
});
if (data) {
metricError.value = data.metricError ?? null;
}
}
const debouncedValidate = useDebounceFn(validate, 500);
watch(
[() => props.containerExpression, metricExpression],
() => {
debouncedValidate();
},
{ immediate: true },
);
// Editor
const metricEditorRef = ref<HTMLElement>();
let metricEditorView: Awaited<ReturnType<typeof createExprEditor>> | undefined;
onMounted(async () => {
if (metricEditorRef.value) {
metricEditorView = await createExprEditor({
parent: metricEditorRef.value,
placeholder: "cpu > 80 || memory > 90",
initialValue: props.alert?.metricExpression ?? props.prefill?.metricExpression ?? "",
getHints: () => createMetricHints(),
onChange: (v) => (metricExpression.value = v),
});
}
});
onScopeDispose(() => {
metricEditorView?.destroy();
});
</script>
@@ -0,0 +1,226 @@
<template>
<div class="space-y-4">
<!-- Name -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.name") }}</legend>
<input
ref="nameInput"
v-model="name"
type="text"
class="input focus:input-primary w-full text-base"
required
:class="{ 'input-primary': name.trim().length > 0 }"
:placeholder="$t('notifications.destination-form.name-placeholder')"
/>
</fieldset>
<!-- Webhook URL -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.webhook-url") }}</legend>
<input
v-model="webhookUrl"
type="url"
class="input focus:input-primary w-full text-base"
:class="{ 'input-primary': isValidUrl, 'input-error': webhookUrl.trim() && !isValidUrl }"
:placeholder="$t('notifications.destination-form.webhook-url-placeholder')"
/>
</fieldset>
<!-- Payload Format (create mode only) -->
<fieldset v-if="!isEditing" class="fieldset">
<legend class="fieldset-legend text-lg">{{ $t("notifications.destination-form.payload-format") }}</legend>
<div class="flex flex-wrap gap-2">
<button
v-for="format in ['slack', 'discord', 'ntfy', 'custom'] as const"
:key="format"
type="button"
class="btn btn-sm"
:class="payloadFormat === format ? 'btn-primary' : 'btn-ghost'"
@click="selectPayloadFormat(format)"
>
{{ $t(`notifications.destination-form.format-${format}`) }}
</button>
</div>
</fieldset>
<!-- Template -->
<fieldset class="fieldset">
<legend class="fieldset-legend text-lg">
{{ $t("notifications.destination-form.template") }}
<span class="text-base-content/60 ml-2 text-sm font-normal">{{
$t("notifications.destination-form.template-hint")
}}</span>
</legend>
<div
ref="templateEditorRef"
class="border-base-content/20 focus-within:border-primary min-h-48 w-full overflow-auto rounded-lg border"
></div>
</fieldset>
<!-- Error -->
<div v-if="error" class="alert alert-error">
<span>{{ error }}</span>
</div>
<!-- Test Result -->
<div v-if="testResult" class="alert" :class="testResult.success ? 'alert-success' : 'alert-error'">
<span v-if="testResult.success">
{{ $t("notifications.destination-form.test-success") }}
<span v-if="testResult.statusCode" class="opacity-70">({{ testResult.statusCode }})</span>
</span>
<span v-else>
{{ testResult.error }}
</span>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 pt-4">
<button class="btn" @click="testDestination" :disabled="!canTest || !isValidUrl || isTesting">
<span v-if="isTesting" class="loading loading-spinner loading-sm"></span>
{{ $t("notifications.destination-form.test") }}
</button>
<div class="flex-1"></div>
<button class="btn" @click="close?.()">
{{ $t("notifications.destination-form.cancel") }}
</button>
<button class="btn btn-primary" :disabled="!canSave" @click="saveDestination">
<span v-if="isSaving" class="loading loading-spinner loading-sm"></span>
{{ isEditing ? $t("notifications.destination-form.save") : $t("notifications.destination-form.add") }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Dispatcher, TestWebhookResult } from "@/types/notifications";
import { createTemplateEditor } from "@/composable/templateEditor";
import { PAYLOAD_TEMPLATES, type PayloadFormat } from "./payloadTemplates";
const { close, onCreated, destination, isEditing } = defineProps<{
close?: () => void;
onCreated?: () => void;
destination?: Dispatcher;
isEditing: boolean;
}>();
const nameInput = ref<HTMLInputElement>();
const templateEditorRef = ref<HTMLElement>();
const name = ref(destination?.name ?? "");
useFocus(nameInput, { initialValue: true });
const webhookUrl = ref(destination?.url ?? "");
const payloadFormat = ref<PayloadFormat>(isEditing ? "custom" : "slack");
const template = ref(isEditing ? (destination?.template ?? "") : PAYLOAD_TEMPLATES[payloadFormat.value]);
const isTesting = ref(false);
const isSaving = ref(false);
const error = ref<string | null>(null);
const testResult = ref<TestWebhookResult | null>(null);
let templateEditorView: Awaited<ReturnType<typeof createTemplateEditor>> | undefined;
function selectPayloadFormat(format: PayloadFormat) {
payloadFormat.value = format;
template.value = PAYLOAD_TEMPLATES[format];
setEditorContent(template.value);
}
function setEditorContent(value: string) {
if (!templateEditorView) return;
templateEditorView.dispatch({
changes: { from: 0, to: templateEditorView.state.doc.length, insert: value },
});
}
onMounted(async () => {
if (!templateEditorRef.value) return;
templateEditorView = await createTemplateEditor({
parent: templateEditorRef.value,
initialValue: template.value,
onChange: (v) => (template.value = v),
});
});
onScopeDispose(() => {
templateEditorView?.destroy();
});
const canTest = computed(() => webhookUrl.value.trim().length > 0);
const isValidUrl = computed(() => {
try {
new URL(webhookUrl.value.trim());
return true;
} catch {
return false;
}
});
const canSave = computed(() => {
if (isSaving.value) return false;
if (!name.value.trim()) return false;
if (!isValidUrl.value) return false;
return true;
});
async function testDestination() {
if (!canTest.value) return;
isTesting.value = true;
testResult.value = null;
try {
const res = await fetch(withBase("/api/notifications/test-webhook"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: webhookUrl.value.trim(),
template: template.value.trim() || undefined,
}),
});
const data: TestWebhookResult = await res.json();
testResult.value = data;
} catch (e) {
testResult.value = { success: false, error: e instanceof Error ? e.message : "Test failed" };
} finally {
isTesting.value = false;
}
}
async function saveDestination() {
if (!canSave.value) return;
isSaving.value = true;
error.value = null;
try {
const input = {
name: name.value.trim(),
type: "webhook",
url: webhookUrl.value.trim(),
template: template.value.trim() || undefined,
};
const url = isEditing
? withBase(`/api/notifications/dispatchers/${destination!.id}`)
: withBase("/api/notifications/dispatchers");
const res = await fetch(url, {
method: isEditing ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to save destination");
}
onCreated?.();
close?.();
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed to save destination";
} finally {
isSaving.value = false;
}
}
</script>
@@ -0,0 +1,63 @@
export type PayloadFormat = "slack" | "discord" | "ntfy" | "custom";
export const PAYLOAD_TEMPLATES: Record<PayloadFormat, string> = {
slack: JSON.stringify(
{
text: "{{ .Container.Name }}",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "*{{ .Container.Name }}*\n{{ .Detail }}",
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: "Host: {{ .Container.HostName }} | Image: {{ .Container.Image }}",
},
],
},
],
},
null,
2,
),
discord: JSON.stringify(
{
content: "{{ .Container.Name }}",
embeds: [
{
title: "{{ .Container.Name }}",
description: "{{ .Detail }}",
fields: [
{ name: "Host", value: "{{ .Container.HostName }}", inline: true },
{ name: "Image", value: "{{ .Container.Image }}", inline: true },
],
},
],
},
null,
2,
),
ntfy: JSON.stringify(
{
topic: "dozzle-{{ .Container.HostName }}",
title: "{{ .Container.Name }}",
message: "{{ .Detail }}",
},
null,
2,
),
custom: JSON.stringify(
{
container: "{{ .Container.Name }}",
message: "{{ .Detail }}",
},
null,
2,
),
};
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col gap-8 px-4 py-4 md:px-8">
<div class="flex flex-col gap-5 px-4 py-4 md:px-8">
<section>
<Links>
<template #more-items>
+6 -6
View File
@@ -3,8 +3,8 @@
<teleport to="body">
<transition name="fade">
<div
v-show="show && (delayedShow || glopbalShow)"
class="border-base-content/20 bg-base-100 fixed z-50 rounded-sm border p-4 shadow-sm"
v-show="show && (delayedShow || globalShow)"
class="ring-base-content/20 bg-base-100 fixed z-50 rounded-sm p-3 shadow-sm ring"
ref="content"
>
<slot name="content"></slot>
@@ -16,14 +16,14 @@
<script lang="ts" setup>
import { globalShowPopup } from "@/composable/popup";
const glopbalShow = globalShowPopup();
const show = ref(glopbalShow.value);
const globalShow = globalShowPopup();
const show = ref(globalShow.value);
const delayedShow = refDebounced(show, 1000);
const content = ref<HTMLElement>();
const onMouseEnter = (e: Event) => {
show.value = true;
glopbalShow.value = true;
globalShow.value = true;
if (content.value && e.target instanceof HTMLElement) {
const { left, top, width } = e.target.getBoundingClientRect();
@@ -37,7 +37,7 @@ const onMouseEnter = (e: Event) => {
const onMouseLeave = () => {
show.value = false;
glopbalShow.value = false;
globalShow.value = false;
};
const el: Ref<HTMLElement> = useCurrentElement();
+2 -2
View File
@@ -12,7 +12,7 @@
<span> % </span>
</div>
</div>
<DistanceTime :date="date" class="text-sm whitespace-nowrap" />
<RelativeTime :date="date" class="text-sm whitespace-nowrap" />
</div>
</transition>
</template>
@@ -93,7 +93,7 @@ svg {
}
.fadeout-leave-active {
@apply transition-opacity;
@apply transition-opacity duration-400;
}
.fadeout-leave-to {
+25 -24
View File
@@ -2,11 +2,11 @@
<section :class="{ 'h-screen min-h-0': scrollable }" class="flex flex-col">
<header
v-if="$slots.header"
class="border-base-content/10 bg-base-200 sticky top-[55px] z-2 border-b py-2 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0"
class="border-base-content/10 bg-base-200 sticky top-[calc(55px+env(safe-area-inset-top))] z-20 border-b py-0.5 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0 md:py-2"
>
<slot name="header"></slot>
</header>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
<main :data-scrolling="scrollable ? true : undefined" class="min-h-[300px] snap-y overflow-auto">
<div class="invisible relative md:visible" v-show="scrollContext.paused">
<div class="absolute top-4 right-44">
<ScrollProgress
@@ -25,10 +25,10 @@
<div ref="scrollObserver" class="h-px"></div>
</main>
<div class="mr-16 text-right">
<div class="mr-16 text-right" v-if="!historical">
<transition name="fade">
<button
class="transition-colorsblur-xs dark btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm"
class="btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm transition-colors"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="scrollContext.paused"
@@ -49,28 +49,29 @@ const scrollableContent = ref<HTMLElement>();
const scrollContext = provideScrollContext();
const { loadingMore } = useLoggingContext();
const { loadingMore, historical } = useLoggingContext();
if (!historical.value) {
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
threshold: [0, 1],
rootMargin: "40px 0px",
});
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
threshold: [0, 1],
rootMargin: "40px 0px",
});
useMutationObserver(
scrollableContent,
(records) => {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = records[records.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
useMutationObserver(
scrollableContent,
(records) => {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = records[records.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
}
}
}
},
{ childList: true, subtree: true },
);
},
{ childList: true, subtree: true },
);
}
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
scrollObserver.value?.scrollIntoView({ behavior });
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<transition name="slide">
<div
class="fixed z-10 flex w-full justify-end p-2"
class="fixed z-50 flex w-full justify-end p-2"
v-show="showSearch"
v-if="search"
ref="container"
+3 -10
View File
@@ -2,17 +2,10 @@
<ScrollableView :scrollable="scrollable" v-if="service.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 gap-1.5 truncate md:gap-2">
<ph:stack-simple />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ service.name }}</div>
</div>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ $t("label.container", service.containers.length) }}
</Tag>
</div>
<ph:stack-simple />
<ContainerDropdown :containers="service.containers">{{ service.name }}</ContainerDropdown>
<MultiContainerStat class="ml-auto" :containers="service.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
<MultiContainerActionToolbar class="max-md:hidden" :name="service.name" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
+23 -5
View File
@@ -1,13 +1,19 @@
<template>
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 flex-col">
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 w-full flex-col">
<Carousel v-model="selectedCard" class="flex-1">
<CarouselItem v-if="config.mode === 'k8s'" :title="$t('label.k8s-menu')" id="k8s">
<K8sMenu />
</CarouselItem>
<CarouselItem v-if="config.mode === 'swarm' && services.length > 0" :title="$t('label.swarm-menu')" id="swarm">
<SwarmMenu />
</CarouselItem>
<CarouselItem :title="$t('label.host-menu')" id="host">
<HostMenu />
</CarouselItem>
<CarouselItem :title="$t('label.group-menu')" v-if="customGroups.length > 0" id="group">
<GroupMenu />
</CarouselItem>
<CarouselItem :title="$t('label.swarm-menu')" v-if="services.length > 0" id="swarm">
<CarouselItem v-if="config.mode !== 'swarm' && services.length > 0" :title="$t('label.swarm-menu')" id="swarm">
<SwarmMenu />
</CarouselItem>
</Carousel>
@@ -24,13 +30,25 @@ const { ready } = storeToRefs(containerStore);
const route = useRoute();
const swarmStore = useSwarmStore();
const { services, customGroups } = storeToRefs(swarmStore);
const selectedCard = ref<"host" | "swarm" | "group">("host");
let defaultCard: "host" | "swarm" | "group" | "k8s";
switch (config.mode) {
case "k8s":
defaultCard = "k8s";
break;
case "swarm":
defaultCard = "swarm";
break;
default:
defaultCard = "host";
}
const selectedCard = ref<"host" | "swarm" | "group" | "k8s">(defaultCard);
watch(
route,
() => {
if (route.meta.menu && ["host", "swarm", "group"].includes(route.meta.menu as string)) {
selectedCard.value = route.meta.menu as "host" | "swarm" | "group";
if (route.meta.menu && ["host", "swarm", "group", "k8s"].includes(route.meta.menu as string)) {
selectedCard.value = route.meta.menu as "host" | "swarm" | "group" | "k8s";
}
},
{ immediate: true },
+7 -8
View File
@@ -1,17 +1,16 @@
<template>
<aside class="fixed flex h-screen w-[inherit] flex-col gap-4 p-3" data-testid="navigation">
<h1>
<router-link :to="{ name: '/' }">
<LogoWithText class="logo [&_.secondary-fill]:fill-secondary [&_.content-fill]:fill-base-content h-16 w-40" />
<router-link :to="{ name: '/' }" class="flex w-full items-center gap-4 overflow-hidden text-4xl font-thin">
<Logo class="h-14 w-14 shrink-0" />
Dozzle
</router-link>
<small class="mb-4 block text-xs font-light" v-if="hostname">
{{ hostname }}
</small>
<small class="mt-4 block text-sm font-light" v-if="hostname">{{ hostname }}</small>
</h1>
<button
class="input input-sm hover:border-primary inline-flex w-auto cursor-pointer items-center gap-2 self-start font-light"
class="input input-sm hover:border-primary mt-2 inline-flex w-auto cursor-pointer items-center gap-2 self-start font-light"
@click="$emit('search')"
:title="$t('tooltip.search')"
data-testid="search"
@@ -21,12 +20,12 @@
<key-shortcut char="k" class="text-base-content/70"></key-shortcut>
</button>
<SideMenu class="mt-2 flex-1" />
<SideMenu class="flex-1" />
</aside>
</template>
<script lang="ts" setup>
import LogoWithText from "@/logo-text.svg";
import Logo from "@/logo.svg";
const { hostname } = config;
</script>
+5 -10
View File
@@ -2,20 +2,15 @@
<ScrollableView :scrollable="scrollable" v-if="stack.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="@container flex flex-1 gap-1.5 truncate md:gap-2">
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
<ph:stack />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ stack.name }}</div>
</div>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
<div class="font-mono text-sm font-semibold">{{ stack.name }}</div>
<ContainerDropdown :containers="stack.containers">
{{ $t("label.container", stack.containers.length) }}
</Tag>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ $t("label.service", stack.services.length) }}
</Tag>
</ContainerDropdown>
</div>
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
<MultiContainerActionToolbar class="max-md:hidden" :name="stack.name" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
+34 -4
View File
@@ -1,16 +1,39 @@
<template>
<ul class="menu w-full p-0 text-[0.95rem]">
<div class="mb-2 flex items-center">
<div class="flex-1">
{{ $t("label.service", services.length) }}
</div>
<div class="flex-none">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
<ph:dots-three-vertical-bold />
</label>
<ul
tabindex="0"
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
>
<li>
<a class="text-sm capitalize" @click="collapseAll()">
<material-symbols-light:collapse-all class="w-4" />
{{ $t("label.collapse-all") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<ul class="menu w-full p-0 text-[0.95rem]" ref="menu">
<li v-for="{ name, services } in stacks" :key="name">
<details open>
<summary class="text-base-content/80 font-light">
<ph:stack />
{{ name }}
{{ name }} ({{ services.length }})
<router-link
:to="{ name: '/stack/[name]', params: { name } }"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-services')"
:title="$t('tooltip.merge-all')"
>
<ph:arrows-merge />
</router-link>
@@ -32,7 +55,7 @@
<details open>
<summary class="text-base-content/80 font-light">
<ph:circles-four />
{{ $t("label.services") }}
{{ $t("label.services") }} ({{ servicesWithoutStacks.length }})
</summary>
<ul>
<li v-for="service in servicesWithoutStacks" :key="service.name">
@@ -55,4 +78,11 @@ const store = useSwarmStore();
const { stacks, services } = storeToRefs(store);
const servicesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
const menu = useTemplateRef("menu");
const collapseAll = () => {
const details = menu.value?.querySelectorAll("details");
details?.forEach((detail) => (detail.open = false));
};
</script>
+67 -13
View File
@@ -1,16 +1,12 @@
<template>
<aside>
<aside class="flex h-[calc(100svh-50px)] flex-col gap-2">
<header class="flex items-center gap-4">
<material-symbols:terminal class="size-8" />
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
<h2 class="text-sm">Started <DistanceTime :date="container.created" /></h2>
<h2 class="text-sm">Started <RelativeTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
<section>
<div ref="host" class="shell"></div>
</section>
</div>
<div ref="host" class="shell flex-1"></div>
</aside>
</template>
@@ -21,34 +17,72 @@ const { container, action } = defineProps<{ container: Container; action: "attac
const { Terminal } = await import("@xterm/xterm");
const { WebLinksAddon } = await import("@xterm/addon-web-links");
const { FitAddon } = await import("@xterm/addon-fit");
const host = useTemplateRef<HTMLDivElement>("host");
const terminal = new Terminal({
cursorBlink: true,
cursorStyle: "block",
theme: {
background: "rgba(0, 0, 0, 0)",
},
});
terminal.loadAddon(new WebLinksAddon());
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
let ws: WebSocket | null = null;
function sendEvent(type: "userinput" | "resize", data?: string, width?: number, height?: number) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
const event: { type: string; data?: string; width?: number; height?: number } = { type };
if (data !== undefined) event.data = data;
if (width !== undefined) event.width = width;
if (height !== undefined) event.height = height;
ws.send(JSON.stringify(event));
}
onMounted(() => {
terminal.open(host.value!);
terminal.resize(100, 40);
fitAddon.fit();
ws = new WebSocket(withBase(`/api/hosts/${container.host}/containers/${container.id}/${action}`));
ws.onopen = () => {
terminal.writeln(`Attached to ${container.name} 🚀`);
terminal.writeln(`Attaching to ${container.name} 🚀`);
// Send initial resize event
sendEvent("resize", undefined, terminal.cols, terminal.rows);
if (action === "attach") {
ws?.send("\r");
sendEvent("userinput", "\r");
}
terminal.onData((data) => {
ws?.send(data);
sendEvent("userinput", data);
});
// Handle terminal resize
terminal.onResize(({ cols, rows }) => {
sendEvent("resize", undefined, cols, rows);
});
terminal.focus();
};
ws.onmessage = (event) => terminal.write(event.data);
ws.addEventListener("close", () => {
terminal.writeln("⚠️ Connection closed");
});
// Handle window resize
const { width, height } = useWindowSize();
watch([width, height], () => {
requestAnimationFrame(() => {
fitAddon.fit();
});
});
});
onUnmounted(() => {
@@ -58,11 +92,11 @@ onUnmounted(() => {
});
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
.shell {
& :deep(.terminal) {
@apply overflow-hidden rounded border-1 p-2;
@apply overflow-hidden rounded border p-2;
&:is(.focus) {
@apply border-primary;
}
@@ -75,5 +109,25 @@ onUnmounted(() => {
& :deep(.xterm-rows) {
@apply text-base-content;
}
& :deep(.xterm-cursor-block.xterm-cursor-blink) {
animation-name: blink !important;
}
& :deep(.xterm-selection) {
@apply bg-primary/30;
}
}
@keyframes blink {
0% {
background-color: var(--color-base-content);
color: #000000;
}
50% {
background-color: inherit;
color: var(--color-base-content);
}
}
</style>
-30
View File
@@ -1,30 +0,0 @@
<template>
<time :datetime="date.toISOString()" data-ci-skip>{{ text }}</time>
</template>
<script lang="ts" setup>
import { formatDistanceToNow } from "date-fns/formatDistanceToNow";
import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict";
const {
date,
strict = false,
suffix = true,
} = defineProps<{
date: Date;
strict?: boolean;
suffix?: boolean;
}>();
const text = ref<string>();
watch($$(date), updateFromNow, { immediate: true });
function updateFromNow() {
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
text.value = fn(date, {
addSuffix: suffix,
});
}
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
</script>
+2 -2
View File
@@ -1,10 +1,10 @@
<template>
<details class="dropdown" ref="details" v-on-click-outside="close">
<details class="dropdown dropdown-end" ref="details" v-on-click-outside="close">
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
<slot name="trigger"> {{ label }} <carbon:caret-down /></slot>
</summary>
<ul
class="menu dropdown-content rounded-box border-base-content/20 bg-base-200 z-50 mt-1 w-52 border p-2 shadow-sm"
class="menu dropdown-content rounded-box border-base-content/20 bg-base-200 z-50 mt-1 max-h-72 w-48 flex-nowrap overflow-auto border p-2 shadow-sm"
>
<slot>
<li v-for="item in options">
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<label class="label text-base-content cursor-pointer gap-4">
<div class="flex-1"><slot name="label" /></div>
<div class="flex-1 whitespace-normal"><slot name="label" /></div>
<slot name="input" />
</label>
</template>
+23 -21
View File
@@ -1,27 +1,29 @@
<template>
<nav class="border-base-content/20 bg-base-200 fixed top-0 z-10 w-full border-b p-2" data-testid="navigation">
<div class="flex items-center">
<router-link :to="{ name: '/' }">
<Logo class="logo [&_.secondary-fill]:fill-secondary h-8" />
</router-link>
<nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-10 w-full border-b" data-testid="navigation">
<div class="p-2">
<div class="flex items-center">
<router-link :to="{ name: '/' }">
<Logo class="h-10" />
</router-link>
<div class="ml-auto flex items-center gap-2">
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
<mdi:magnify class="size-5" />
</a>
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
<input type="checkbox" v-model="show" />
<mdi:close class="swap-on" />
<mdi:hamburger-menu class="swap-off" />
</label>
<div class="ml-auto flex items-center gap-2">
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
<mdi:magnify class="size-5" />
</a>
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
<input type="checkbox" v-model="show" />
<mdi:close class="swap-on" />
<mdi:hamburger-menu class="swap-off" />
</label>
</div>
</div>
<transition name="fade">
<div v-show="show" class="flex h-[calc(100svh-55px)]">
<SideMenu class="flex-1" />
</div>
</transition>
</div>
<transition name="fade">
<div v-show="show" class="flex h-[calc(100svh-55px)]">
<SideMenu class="flex-1" />
</div>
</transition>
</nav>
</template>
@@ -35,7 +37,7 @@ watch(route, () => {
});
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
+17
View File
@@ -0,0 +1,17 @@
<template>
<time :datetime="date.toISOString()">{{ text }}</time>
</template>
<script lang="ts" setup>
const { date } = defineProps<{
date: Date;
}>();
const text = ref<string>();
const updateFromNow = () => {
text.value = toRelativeTime(date, locale.value === "" ? undefined : locale.value);
};
watch(() => date, updateFromNow, { immediate: true });
useIntervalFn(updateFromNow, 30_000);
</script>
+18 -8
View File
@@ -1,13 +1,18 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-hidden backdrop:bg-none">
<div class="modal-box" :width="width">
<form method="dialog">
<button class="swap hover:swap-active absolute top-4 right-4 outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot v-if="open"></slot>
<div class="pt-safe relative">
<form method="dialog" class="absolute right-0">
<button v-if="isMobile">
<mdi:close />
</button>
<button v-else class="swap hover:swap-active outline-hidden">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot v-if="open" :close="close"></slot>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
@@ -23,17 +28,22 @@ const { width } = defineProps<{
width: DrawerWidth;
}>();
function close() {
panel.value?.close();
}
defineExpose({
open: () => {
open.value = true;
panel.value?.showModal();
},
close,
});
useEventListener(panel, "close", () => (open.value = false));
</script>
<style scoped>
@import "@/main.css" reference;
@reference "@/main.css";
.modal-right :where(.modal-box) {
@apply bg-base-100 fixed right-0 h-lvh max-h-screen translate-x-24 scale-100 rounded-none shadow-none;
+2 -2
View File
@@ -1,7 +1,7 @@
<template>
<div class="toast toast-end whitespace-normal max-md:m-0 max-md:w-full">
<div class="toast toast-end whitespace-normal max-md:end-auto max-md:m-0 max-md:max-w-full">
<div
class="alert max-w-xl max-md:rounded-none"
class="alert max-w-xl shadow-sm max-md:rounded-none"
v-for="{ toast, options: { timed } } in toasts"
:key="toast.id"
:class="{
+150
View File
@@ -0,0 +1,150 @@
import { Container } from "@/models/Container";
import type { ContainerJson } from "@/types/Container";
import type { Dispatcher, NotificationRule, PreviewResult } from "@/types/notifications";
import { createExprEditor, createContainerHints } from "@/composable/exprEditor";
export interface AlertFormOptions {
close?: () => void;
onCreated?: () => void;
alert?: NotificationRule;
prefill?: { name?: string; containerExpression?: string; logExpression?: string; metricExpression?: string };
}
export interface ContainerResult {
error?: string;
containers?: Container[];
}
export function useAlertForm(options: AlertFormOptions) {
const isEditing = computed(() => !!options.alert);
const alertName = ref(options.alert?.name ?? options.prefill?.name ?? "");
const containerExpression = ref(options.alert?.containerExpression ?? options.prefill?.containerExpression ?? "");
const dispatcherId = ref(options.alert?.dispatcher?.id ?? 0);
const isSaving = ref(false);
const saveError = ref<string | null>(null);
// Destinations
const destinations = ref<Dispatcher[]>([]);
onMounted(async () => {
const res = await fetch(withBase("/api/notifications/dispatchers"));
destinations.value = await res.json();
});
const selectedDestination = computed(() => destinations.value.find((d) => d.id === dispatcherId.value));
// Container store for autocomplete
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const containerNames = computed(() => [
...new Set(containers.value.filter((c) => c.state === "running").map((c) => c.name)),
]);
const imageNames = computed(() => [...new Set(containers.value.map((c) => c.image))]);
const hostNames = computed(() => [...new Set(containers.value.map((c) => c.host))]);
// Container validation
const containerResult = ref<ContainerResult | null>(null);
const isLoading = ref(false);
const baseCanSave = computed(
() =>
alertName.value.trim() &&
containerExpression.value.trim() &&
dispatcherId.value > 0 &&
!containerResult.value?.error &&
!isSaving.value,
);
async function initContainerEditor(el: HTMLElement) {
return await createExprEditor({
parent: el,
placeholder: 'name contains "api"',
initialValue: options.alert?.containerExpression ?? options.prefill?.containerExpression ?? "",
getHints: () => createContainerHints(containerNames.value, imageNames.value, hostNames.value),
onChange: (v) => (containerExpression.value = v),
});
}
async function saveAlert(typeSpecificFields: Record<string, unknown>) {
isSaving.value = true;
saveError.value = null;
try {
const input = {
name: alertName.value.trim(),
containerExpression: containerExpression.value,
dispatcherId: dispatcherId.value,
enabled: true,
...typeSpecificFields,
};
const url = isEditing.value
? withBase(`/api/notifications/rules/${options.alert!.id}`)
: withBase("/api/notifications/rules");
const res = await fetch(url, {
method: isEditing.value ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to save alert");
}
options.onCreated?.();
options.close?.();
} catch (e) {
saveError.value = e instanceof Error ? e.message : "Failed to save alert";
} finally {
isSaving.value = false;
}
}
async function validatePreview(extraFields: Record<string, unknown> = {}) {
if (!containerExpression.value && !Object.values(extraFields).some(Boolean)) {
containerResult.value = null;
return { data: null };
}
isLoading.value = true;
try {
const res = await fetch(withBase("/api/notifications/preview"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
containerExpression: containerExpression.value,
...extraFields,
}),
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || "Preview failed");
}
const data: PreviewResult = await res.json();
containerResult.value = containerExpression.value
? {
error: data.containerError ?? undefined,
containers: data.matchedContainers?.map((c) => Container.fromJSON(c as ContainerJson)),
}
: null;
return { data };
} catch (e) {
containerResult.value = { error: e instanceof Error ? e.message : "Unknown error" };
return { data: null };
} finally {
isLoading.value = false;
}
}
return {
isEditing,
alertName,
containerExpression,
dispatcherId,
destinations,
selectedDestination,
containerResult,
isLoading,
isSaving,
saveError,
baseCanSave,
initContainerEditor,
saveAlert,
validatePreview,
};
}

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