detached dc2c50b

refactor: Update build process and enhance CSS handling

- Modify `.air.toml` to change build command and include `serve` in binary arguments.
- Update `Makefile` to ensure CSS is built with TailwindCSS as part of the build process.
- Upgrade `tailwindcss` version in `package.json` and adjust build scripts accordingly.
- Remove unused `@catppuccin/tailwindcss` dependency from `package.json`.
- Refactor HTML templates to improve structure and add refresh buttons for container and image lists.
- Enhance JavaScript functionality for the admin dashboard with improved event handling and notifications.
- Remove deprecated `chart.js` library and clean up related code.
commit
dc2c50b10f8f597c312bf10d62a624de703e62ea
author
bamen · · 2025-03-17 17:05 UTC
parent
ddc5793
signature
unsigned
18 files · +549 -221

this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.

M .air.toml +2 -2
@@ -3,9 +3,9 @@ testdata_dir = "testdata"
33 tmp_dir = "tmp"
44
55 [build]
6- args_bin = []
6+ args_bin = ["serve"]
77 bin = "./dist/tmp/main"
8- cmd = "go build -o ./dist/tmp/main cmd/cli/main.go"
8+ cmd = "go build -o ./dist/tmp/main ./main.go"
99 delay = 0
1010 exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]
1111 exclude_file = []
@@ -3,9 +3,9 @@ testdata_dir = "testdata"
3 tmp_dir = "tmp"3 tmp_dir = "tmp"
4 4
5 [build]5 [build]
6- args_bin = []6+ args_bin = ["serve"]
7 bin = "./dist/tmp/main"7 bin = "./dist/tmp/main"
8- cmd = "go build -o ./dist/tmp/main cmd/cli/main.go"8+ cmd = "go build -o ./dist/tmp/main ./main.go"
9 delay = 09 delay = 0
10 exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]10 exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]
11 exclude_file = []11 exclude_file = []
M .gitignore +1 -0
@@ -15,3 +15,4 @@ setup.sh
1515 .iscontainer
1616 sqlite.db
1717 .vscode/
18+admin-ui/
@@ -15,3 +15,4 @@ setup.sh
15 .iscontainer15 .iscontainer
16 sqlite.db16 sqlite.db
17 .vscode/17 .vscode/
18+admin-ui/
M Makefile +9 -3
@@ -20,13 +20,13 @@ LDFLAGS := -s -w \
2020 ARCHS := amd64 arm64
2121
2222 # Phony targets
23-.PHONY: all build build-push clean
23+.PHONY: all build build-push clean build-css
2424
2525 # Default target
2626 all: build
2727
2828 # Build binaries
29-build:
29+build: build-css
3030 @echo "Building Go binaries..."
3131 @mkdir -p $(DIST_DIR)
3232 @rm -f $(DIST_DIR)/*
@@ -35,8 +35,14 @@ build:
3535 @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-arm64 ./main.go
3636 @echo "Go binaries built successfully"
3737
38+# Build CSS with tailwindcss
39+build-css:
40+ @echo "Building CSS with tailwindcss..."
41+ @bun run build:css
42+ @echo "CSS built successfully"
43+
3844 # Build and push Docker images
39-build-push: build
45+build-push: build-css build
4046 @echo "Cleaning up dangling images..."
4147 @$(ENGINE) image prune -f
4248
@@ -20,13 +20,13 @@ LDFLAGS := -s -w \
20 ARCHS := amd64 arm6420 ARCHS := amd64 arm64
21 21
22 # Phony targets22 # Phony targets
23-.PHONY: all build build-push clean23+.PHONY: all build build-push clean build-css
24 24
25 # Default target25 # Default target
26 all: build26 all: build
27 27
28 # Build binaries28 # Build binaries
29-build:29+build: build-css
30 @echo "Building Go binaries..."30 @echo "Building Go binaries..."
31 @mkdir -p $(DIST_DIR)31 @mkdir -p $(DIST_DIR)
32 @rm -f $(DIST_DIR)/*32 @rm -f $(DIST_DIR)/*
@@ -35,8 +35,14 @@ build:
35 @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-arm64 ./main.go35 @CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/gordon-linux-arm64 ./main.go
36 @echo "Go binaries built successfully"36 @echo "Go binaries built successfully"
37 37
38+# Build CSS with tailwindcss
39+build-css:
40+ @echo "Building CSS with tailwindcss..."
41+ @bun run build:css
42+ @echo "CSS built successfully"
43+
38 # Build and push Docker images44 # Build and push Docker images
39-build-push: build45+build-push: build-css build
40 @echo "Cleaning up dangling images..."46 @echo "Cleaning up dangling images..."
41 @$(ENGINE) image prune -f47 @$(ENGINE) image prune -f
42 48
M internal/templating/models/html/admin/manager.gohtml +0 -2
@@ -16,7 +16,6 @@
1616 hx-trigger="load"
1717 hx-swap="innerHTML"
1818 hx-swap-oob="true"
19- hx-poll="5s"
2019 ></div>
2120 <!-- List all the images -->
2221 <div
@@ -25,7 +24,6 @@
2524 hx-trigger="load"
2625 hx-swap="innerHTML"
2726 hx-swap-oob="true"
28- hx-poll="5s"
2927 ></div>
3028 </div>
3129 </div>
@@ -16,7 +16,6 @@
16 hx-trigger="load"16 hx-trigger="load"
17 hx-swap="innerHTML"17 hx-swap="innerHTML"
18 hx-swap-oob="true"18 hx-swap-oob="true"
19- hx-poll="5s"
20 ></div>19 ></div>
21 <!-- List all the images -->20 <!-- List all the images -->
22 <div21 <div
@@ -25,7 +24,6 @@
25 hx-trigger="load"24 hx-trigger="load"
26 hx-swap="innerHTML"25 hx-swap="innerHTML"
27 hx-swap-oob="true"26 hx-swap-oob="true"
28- hx-poll="5s"
29 ></div>27 ></div>
30 </div>28 </div>
31 </div>29 </div>
M internal/templating/models/html/fragments/containerlist.gohtml +13 -4
@@ -1,7 +1,17 @@
11 <div class="flex flex-col p-4" hx-ext="response-targets">
2- <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
3- {{index .Lang "manager" "content" "h1-containers"}}
4- </h1>
2+ <div class="flex justify-between items-center mb-1">
3+ <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
4+ {{index .Lang "manager" "content" "h1-containers"}}
5+ </h1>
6+ <button
7+ id="refresh-containers-button"
8+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
9+ title="Refresh container list"
10+ >
11+ <span class="iconf text-xl align-middle">󰑐 </span>
12+ Refresh
13+ </button>
14+ </div>
515 <table class="p-4 border-t-2 table-auto">
616 <thead>
717 <tr class="text-center text-gray-400">
@@ -50,7 +60,6 @@
5060 hx-swap="innerHTML"
5161 class="py-2 px-4"
5262 data-active="false"
53- _="on click toggle between .visible and .hidden on #actionResponse-{{.ID}}"
5463 >
5564 <span title="Edit the container" class="iconf text-2xl text-blue-600 hover:text-gordonmascot-hblue">󱩽</span>
5665 </button>
@@ -1,7 +1,17 @@
1 <div class="flex flex-col p-4" hx-ext="response-targets">1 <div class="flex flex-col p-4" hx-ext="response-targets">
2- <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">2+ <div class="flex justify-between items-center mb-1">
3- {{index .Lang "manager" "content" "h1-containers"}}3+ <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
4- </h1>4+ {{index .Lang "manager" "content" "h1-containers"}}
5+ </h1>
6+ <button
7+ id="refresh-containers-button"
8+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
9+ title="Refresh container list"
10+ >
11+ <span class="iconf text-xl align-middle">󰑐 </span>
12+ Refresh
13+ </button>
14+ </div>
5 <table class="p-4 border-t-2 table-auto">15 <table class="p-4 border-t-2 table-auto">
6 <thead>16 <thead>
7 <tr class="text-center text-gray-400">17 <tr class="text-center text-gray-400">
@@ -50,7 +60,6 @@
50 hx-swap="innerHTML"60 hx-swap="innerHTML"
51 class="py-2 px-4"61 class="py-2 px-4"
52 data-active="false"62 data-active="false"
53- _="on click toggle between .visible and .hidden on #actionResponse-{{.ID}}"
54 >63 >
55 <span title="Edit the container" class="iconf text-2xl text-blue-600 hover:text-gordonmascot-hblue">󱩽</span>64 <span title="Edit the container" class="iconf text-2xl text-blue-600 hover:text-gordonmascot-hblue">󱩽</span>
56 </button>65 </button>
M internal/templating/models/html/fragments/createcontainer.gohtml +5 -2
@@ -5,8 +5,11 @@
55 {{if .ErrorMessage}}
66 <div class="text-red-500">{{.ErrorMessage}}</div>
77 {{ end }}
8- <form action="/create-container/{{.ShortID}}" method="post" hx-post="/htmx/create-container/{{.ShortID}}"
9- hx-swap="innerHTML" hx-target-*="#error-message-create-container-{{.ShortID}}">
8+ <form id="create-container-form-{{.ShortID}}" action="/create-container/{{.ShortID}}" method="post"
9+ hx-post="/htmx/create-container/{{.ShortID}}"
10+ hx-swap="innerHTML"
11+ hx-target="#create-container-{{.ShortID}}"
12+ hx-target-*="#error-message-create-container-{{.ShortID}}">
1013 <!-- Image Name -->
1114 <div class="flex space-x-4 pb-4">
1215 <div class="w-1/3">
@@ -5,8 +5,11 @@
5 {{if .ErrorMessage}}5 {{if .ErrorMessage}}
6 <div class="text-red-500">{{.ErrorMessage}}</div>6 <div class="text-red-500">{{.ErrorMessage}}</div>
7 {{ end }}7 {{ end }}
8- <form action="/create-container/{{.ShortID}}" method="post" hx-post="/htmx/create-container/{{.ShortID}}"8+ <form id="create-container-form-{{.ShortID}}" action="/create-container/{{.ShortID}}" method="post"
9- hx-swap="innerHTML" hx-target-*="#error-message-create-container-{{.ShortID}}">9+ hx-post="/htmx/create-container/{{.ShortID}}"
10+ hx-swap="innerHTML"
11+ hx-target="#create-container-{{.ShortID}}"
12+ hx-target-*="#error-message-create-container-{{.ShortID}}">
10 <!-- Image Name -->13 <!-- Image Name -->
11 <div class="flex space-x-4 pb-4">14 <div class="flex space-x-4 pb-4">
12 <div class="w-1/3">15 <div class="w-1/3">
M internal/templating/models/html/fragments/imagelist.gohtml +23 -16
@@ -2,21 +2,29 @@
22 <!-- Upload button aligned right of Images -->
33 <div class="flex justify-between items-center mb-1">
44 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>
5- <button
6- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
7- id="upload-button"
8- hx-get="/htmx/upload-image"
9- hx-target="#upload-image"
10- hx-swap="innerHTML"
11- title="Manually upload a .tar container image"
12- _="on click toggle between .visible and .hidden on #upload-image then add .hidden to .create-container"
13- >
14-
15- <span class="iconf text-xl align-middle">󰭾 </span>
16- Upload
17- </button>
5+ <div class="flex space-x-2">
6+ <button
7+ id="refresh-images-button"
8+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
9+ title="Refresh image list"
10+ >
11+ <span class="iconf text-xl align-middle">󰑐 </span>
12+ Refresh
13+ </button>
14+ <button
15+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
16+ id="upload-button"
17+ hx-get="/htmx/upload-image"
18+ hx-target="#upload-image"
19+ hx-swap="innerHTML"
20+ title="Manually upload a .tar container image"
21+ >
22+ <span class="iconf text-xl align-middle">󰭾 </span>
23+ Upload
24+ </button>
25+ </div>
1826 </div>
19- <div id="upload-image"></div>
27+ <div id="upload-image"></div>
2028 <table class="p-4 border-t-2">
2129 <tbody>
2230 {{
@@ -39,9 +47,8 @@
3947 hx-target-*="#error-message-div-{{.ShortID}}"
4048 hx-swap="innerHTML"
4149 class="py-2 px-4"
42- _="on click toggle between .visible and .hidden on #create-container-{{.ShortID}} then add .hidden to #upload-image"
4350 >
44- <span title="Deploy a container using this image" class="iconf text-xl text-blue-600 hover:text-gordonmascot-hblue"></span>
51+ <span title="Deploy a container using this image" class="iconf text-xl text-blue-600 hover:text-gordonmascot-hblue">󰆍</span>
4552 </button>
4653
4754 <button
@@ -2,21 +2,29 @@
2 <!-- Upload button aligned right of Images -->2 <!-- Upload button aligned right of Images -->
3 <div class="flex justify-between items-center mb-1">3 <div class="flex justify-between items-center mb-1">
4 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>4 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>
5- <button5+ <div class="flex space-x-2">
6- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"6+ <button
7- id="upload-button"7+ id="refresh-images-button"
8- hx-get="/htmx/upload-image"8+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
9- hx-target="#upload-image"9+ title="Refresh image list"
10- hx-swap="innerHTML"10+ >
11- title="Manually upload a .tar container image"11+ <span class="iconf text-xl align-middle">󰑐 </span>
12- _="on click toggle between .visible and .hidden on #upload-image then add .hidden to .create-container"12+ Refresh
13- >13+ </button>
14-14+ <button
15- <span class="iconf text-xl align-middle">󰭾 </span>15+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
16- Upload16+ id="upload-button"
17- </button>17+ hx-get="/htmx/upload-image"
18+ hx-target="#upload-image"
19+ hx-swap="innerHTML"
20+ title="Manually upload a .tar container image"
21+ >
22+ <span class="iconf text-xl align-middle">󰭾 </span>
23+ Upload
24+ </button>
25+ </div>
18 </div>26 </div>
19- <div id="upload-image"></div>27+ <div id="upload-image"></div>
20 <table class="p-4 border-t-2">28 <table class="p-4 border-t-2">
21 <tbody>29 <tbody>
22 {{30 {{
@@ -39,9 +47,8 @@
39 hx-target-*="#error-message-div-{{.ShortID}}"47 hx-target-*="#error-message-div-{{.ShortID}}"
40 hx-swap="innerHTML"48 hx-swap="innerHTML"
41 class="py-2 px-4"49 class="py-2 px-4"
42- _="on click toggle between .visible and .hidden on #create-container-{{.ShortID}} then add .hidden to #upload-image"
43 >50 >
44- <span title="Deploy a container using this image" class="iconf text-xl text-blue-600 hover:text-gordonmascot-hblue"></span>51+ <span title="Deploy a container using this image" class="iconf text-xl text-blue-600 hover:text-gordonmascot-hblue">󰆍</span>
45 </button>52 </button>
46 53
47 <button54 <button
M internal/templating/models/html/fragments/success.gohtml +1 -2
@@ -1,6 +1,5 @@
11 <div
22 id="success"
3- _="on load wait 1s then transition opacity to 0 then remove me then wait 1s then call window.location.reload()"
43 class="flex text-blue-800 content-center justify-center items-center w-full h-full">
5- <p class="text-2xl text-center">Success!</p>
4+ <p class="text-2xl text-center">{{if .Message}}{{.Message}}{{else}}Success!{{end}}</p>
65 </div>
@@ -1,6 +1,5 @@
1 <div1 <div
2 id="success"2 id="success"
3- _="on load wait 1s then transition opacity to 0 then remove me then wait 1s then call window.location.reload()"
4 class="flex text-blue-800 content-center justify-center items-center w-full h-full">3 class="flex text-blue-800 content-center justify-center items-center w-full h-full">
5- <p class="text-2xl text-center">Success!</p>4+ <p class="text-2xl text-center">{{if .Message}}{{.Message}}{{else}}Success!{{end}}</p>
6 </div>5 </div>
M internal/templating/models/html/fragments/uploadimage.gohtml +1 -13
@@ -27,21 +27,9 @@
2727 <input
2828 type="submit"
2929 value="Upload"
30- _="on click toggle @disabled until htmx:afterOnLoad"
30+ id="upload-submit-button"
3131 class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
3232 </form>
3333 <div id="error-message-div" class="text-red-500"></div>
3434 </div>
3535 </div>
36-<script>
37- document.body.addEventListener("htmx:afterOnLoad", function (event) {
38- if (
39- event.target.id === "image-manager" &&
40- event.detail.xhr.responseText === "Success"
41- ) {
42- setTimeout(function () {
43- location.reload();
44- }, 3000);
45- }
46- });
47-</script>
@@ -27,21 +27,9 @@
27 <input27 <input
28 type="submit"28 type="submit"
29 value="Upload"29 value="Upload"
30- _="on click toggle @disabled until htmx:afterOnLoad"30+ id="upload-submit-button"
31 class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />31 class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
32 </form>32 </form>
33 <div id="error-message-div" class="text-red-500"></div>33 <div id="error-message-div" class="text-red-500"></div>
34 </div>34 </div>
35 </div>35 </div>
36-<script>
37- document.body.addEventListener("htmx:afterOnLoad", function (event) {
38- if (
39- event.target.id === "image-manager" &&
40- event.detail.xhr.responseText === "Success"
41- ) {
42- setTimeout(function () {
43- location.reload();
44- }, 3000);
45- }
46- });
47-</script>
M internal/webui/public/assets/css/tailwind.css +37 -41
@@ -114,7 +114,7 @@
114114 --tw-contain-style: ;
115115 }
116116
117-/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
117+/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com
118118 */
119119
120120 /*
@@ -595,10 +595,6 @@ video {
595595 }
596596 }
597597
598-.visible {
599- visibility: visible;
600-}
601-
602598 .fixed {
603599 position: fixed;
604600 }
@@ -841,62 +837,62 @@ video {
841837
842838 .border-gray-300 {
843839 --tw-border-opacity: 1;
844- border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
840+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
845841 }
846842
847843 .border-red-400 {
848844 --tw-border-opacity: 1;
849- border-color: rgb(248 113 113 / var(--tw-border-opacity, 1));
845+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
850846 }
851847
852848 .bg-blue-500 {
853849 --tw-bg-opacity: 1;
854- background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
850+ background-color: rgb(59 130 246 / var(--tw-bg-opacity));
855851 }
856852
857853 .bg-gordonmascot-hblue {
858854 --tw-bg-opacity: 1;
859- background-color: rgb(1 30 92 / var(--tw-bg-opacity, 1));
855+ background-color: rgb(1 30 92 / var(--tw-bg-opacity));
860856 }
861857
862858 .bg-gordonmascot-lblue {
863859 --tw-bg-opacity: 1;
864- background-color: rgb(0 123 192 / var(--tw-bg-opacity, 1));
860+ background-color: rgb(0 123 192 / var(--tw-bg-opacity));
865861 }
866862
867863 .bg-gordonmascot-mblue {
868864 --tw-bg-opacity: 1;
869- background-color: rgb(0 82 147 / var(--tw-bg-opacity, 1));
865+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
870866 }
871867
872868 .bg-gray-100 {
873869 --tw-bg-opacity: 1;
874- background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
870+ background-color: rgb(243 244 246 / var(--tw-bg-opacity));
875871 }
876872
877873 .bg-gray-300 {
878874 --tw-bg-opacity: 1;
879- background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
875+ background-color: rgb(209 213 219 / var(--tw-bg-opacity));
880876 }
881877
882878 .bg-green-500 {
883879 --tw-bg-opacity: 1;
884- background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));
880+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
885881 }
886882
887883 .bg-red-100 {
888884 --tw-bg-opacity: 1;
889- background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
885+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
890886 }
891887
892888 .bg-slate-100 {
893889 --tw-bg-opacity: 1;
894- background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
890+ background-color: rgb(241 245 249 / var(--tw-bg-opacity));
895891 }
896892
897893 .bg-slate-200 {
898894 --tw-bg-opacity: 1;
899- background-color: rgb(226 232 240 / var(--tw-bg-opacity, 1));
895+ background-color: rgb(226 232 240 / var(--tw-bg-opacity));
900896 }
901897
902898 .bg-transparent {
@@ -905,12 +901,12 @@ video {
905901
906902 .bg-white {
907903 --tw-bg-opacity: 1;
908- background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
904+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
909905 }
910906
911907 .bg-zinc-200 {
912908 --tw-bg-opacity: 1;
913- background-color: rgb(228 228 231 / var(--tw-bg-opacity, 1));
909+ background-color: rgb(228 228 231 / var(--tw-bg-opacity));
914910 }
915911
916912 .p-0 {
@@ -1038,72 +1034,72 @@ video {
10381034
10391035 .text-blue-600 {
10401036 --tw-text-opacity: 1;
1041- color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1037+ color: rgb(37 99 235 / var(--tw-text-opacity));
10421038 }
10431039
10441040 .text-blue-800 {
10451041 --tw-text-opacity: 1;
1046- color: rgb(30 64 175 / var(--tw-text-opacity, 1));
1042+ color: rgb(30 64 175 / var(--tw-text-opacity));
10471043 }
10481044
10491045 .text-gordonmascot-hblue {
10501046 --tw-text-opacity: 1;
1051- color: rgb(1 30 92 / var(--tw-text-opacity, 1));
1047+ color: rgb(1 30 92 / var(--tw-text-opacity));
10521048 }
10531049
10541050 .text-gordonmascot-mblue {
10551051 --tw-text-opacity: 1;
1056- color: rgb(0 82 147 / var(--tw-text-opacity, 1));
1052+ color: rgb(0 82 147 / var(--tw-text-opacity));
10571053 }
10581054
10591055 .text-gray-100 {
10601056 --tw-text-opacity: 1;
1061- color: rgb(243 244 246 / var(--tw-text-opacity, 1));
1057+ color: rgb(243 244 246 / var(--tw-text-opacity));
10621058 }
10631059
10641060 .text-gray-400 {
10651061 --tw-text-opacity: 1;
1066- color: rgb(156 163 175 / var(--tw-text-opacity, 1));
1062+ color: rgb(156 163 175 / var(--tw-text-opacity));
10671063 }
10681064
10691065 .text-gray-500 {
10701066 --tw-text-opacity: 1;
1071- color: rgb(107 114 128 / var(--tw-text-opacity, 1));
1067+ color: rgb(107 114 128 / var(--tw-text-opacity));
10721068 }
10731069
10741070 .text-gray-700 {
10751071 --tw-text-opacity: 1;
1076- color: rgb(55 65 81 / var(--tw-text-opacity, 1));
1072+ color: rgb(55 65 81 / var(--tw-text-opacity));
10771073 }
10781074
10791075 .text-gray-800 {
10801076 --tw-text-opacity: 1;
1081- color: rgb(31 41 55 / var(--tw-text-opacity, 1));
1077+ color: rgb(31 41 55 / var(--tw-text-opacity));
10821078 }
10831079
10841080 .text-green-300 {
10851081 --tw-text-opacity: 1;
1086- color: rgb(134 239 172 / var(--tw-text-opacity, 1));
1082+ color: rgb(134 239 172 / var(--tw-text-opacity));
10871083 }
10881084
10891085 .text-red-500 {
10901086 --tw-text-opacity: 1;
1091- color: rgb(239 68 68 / var(--tw-text-opacity, 1));
1087+ color: rgb(239 68 68 / var(--tw-text-opacity));
10921088 }
10931089
10941090 .text-red-600 {
10951091 --tw-text-opacity: 1;
1096- color: rgb(220 38 38 / var(--tw-text-opacity, 1));
1092+ color: rgb(220 38 38 / var(--tw-text-opacity));
10971093 }
10981094
10991095 .text-red-700 {
11001096 --tw-text-opacity: 1;
1101- color: rgb(185 28 28 / var(--tw-text-opacity, 1));
1097+ color: rgb(185 28 28 / var(--tw-text-opacity));
11021098 }
11031099
11041100 .text-white {
11051101 --tw-text-opacity: 1;
1106- color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1102+ color: rgb(255 255 255 / var(--tw-text-opacity));
11071103 }
11081104
11091105 .shadow {
@@ -1277,37 +1273,37 @@ video {
12771273
12781274 .hover\:bg-blue-700:hover {
12791275 --tw-bg-opacity: 1;
1280- background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
1276+ background-color: rgb(29 78 216 / var(--tw-bg-opacity));
12811277 }
12821278
12831279 .hover\:bg-gordonmascot-hblue:hover {
12841280 --tw-bg-opacity: 1;
1285- background-color: rgb(1 30 92 / var(--tw-bg-opacity, 1));
1281+ background-color: rgb(1 30 92 / var(--tw-bg-opacity));
12861282 }
12871283
12881284 .hover\:bg-gordonmascot-mblue:hover {
12891285 --tw-bg-opacity: 1;
1290- background-color: rgb(0 82 147 / var(--tw-bg-opacity, 1));
1286+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
12911287 }
12921288
12931289 .hover\:bg-slate-100:hover {
12941290 --tw-bg-opacity: 1;
1295- background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
1291+ background-color: rgb(241 245 249 / var(--tw-bg-opacity));
12961292 }
12971293
12981294 .hover\:text-gordonmascot-hblue:hover {
12991295 --tw-text-opacity: 1;
1300- color: rgb(1 30 92 / var(--tw-text-opacity, 1));
1296+ color: rgb(1 30 92 / var(--tw-text-opacity));
13011297 }
13021298
13031299 .hover\:text-red-900:hover {
13041300 --tw-text-opacity: 1;
1305- color: rgb(127 29 29 / var(--tw-text-opacity, 1));
1301+ color: rgb(127 29 29 / var(--tw-text-opacity));
13061302 }
13071303
13081304 .focus\:border-gordonmascot-hblue:focus {
13091305 --tw-border-opacity: 1;
1310- border-color: rgb(1 30 92 / var(--tw-border-opacity, 1));
1306+ border-color: rgb(1 30 92 / var(--tw-border-opacity));
13111307 }
13121308
13131309 .focus\:outline-none:focus {
@@ -1317,7 +1313,7 @@ video {
13171313
13181314 .focus\:ring-gordonmascot-hblue:focus {
13191315 --tw-ring-opacity: 1;
1320- --tw-ring-color: rgb(1 30 92 / var(--tw-ring-opacity, 1));
1316+ --tw-ring-color: rgb(1 30 92 / var(--tw-ring-opacity));
13211317 }
13221318
13231319 @media (min-width: 640px) {
@@ -114,7 +114,7 @@
114 --tw-contain-style: ;114 --tw-contain-style: ;
115 }115 }
116 116
117-/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com117+/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com
118 */118 */
119 119
120 /*120 /*
@@ -595,10 +595,6 @@ video {
595 }595 }
596 }596 }
597 597
598-.visible {
599- visibility: visible;
600-}
601-
602 .fixed {598 .fixed {
603 position: fixed;599 position: fixed;
604 }600 }
@@ -841,62 +837,62 @@ video {
841 837
842 .border-gray-300 {838 .border-gray-300 {
843 --tw-border-opacity: 1;839 --tw-border-opacity: 1;
844- border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));840+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
845 }841 }
846 842
847 .border-red-400 {843 .border-red-400 {
848 --tw-border-opacity: 1;844 --tw-border-opacity: 1;
849- border-color: rgb(248 113 113 / var(--tw-border-opacity, 1));845+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
850 }846 }
851 847
852 .bg-blue-500 {848 .bg-blue-500 {
853 --tw-bg-opacity: 1;849 --tw-bg-opacity: 1;
854- background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));850+ background-color: rgb(59 130 246 / var(--tw-bg-opacity));
855 }851 }
856 852
857 .bg-gordonmascot-hblue {853 .bg-gordonmascot-hblue {
858 --tw-bg-opacity: 1;854 --tw-bg-opacity: 1;
859- background-color: rgb(1 30 92 / var(--tw-bg-opacity, 1));855+ background-color: rgb(1 30 92 / var(--tw-bg-opacity));
860 }856 }
861 857
862 .bg-gordonmascot-lblue {858 .bg-gordonmascot-lblue {
863 --tw-bg-opacity: 1;859 --tw-bg-opacity: 1;
864- background-color: rgb(0 123 192 / var(--tw-bg-opacity, 1));860+ background-color: rgb(0 123 192 / var(--tw-bg-opacity));
865 }861 }
866 862
867 .bg-gordonmascot-mblue {863 .bg-gordonmascot-mblue {
868 --tw-bg-opacity: 1;864 --tw-bg-opacity: 1;
869- background-color: rgb(0 82 147 / var(--tw-bg-opacity, 1));865+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
870 }866 }
871 867
872 .bg-gray-100 {868 .bg-gray-100 {
873 --tw-bg-opacity: 1;869 --tw-bg-opacity: 1;
874- background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));870+ background-color: rgb(243 244 246 / var(--tw-bg-opacity));
875 }871 }
876 872
877 .bg-gray-300 {873 .bg-gray-300 {
878 --tw-bg-opacity: 1;874 --tw-bg-opacity: 1;
879- background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));875+ background-color: rgb(209 213 219 / var(--tw-bg-opacity));
880 }876 }
881 877
882 .bg-green-500 {878 .bg-green-500 {
883 --tw-bg-opacity: 1;879 --tw-bg-opacity: 1;
884- background-color: rgb(34 197 94 / var(--tw-bg-opacity, 1));880+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
885 }881 }
886 882
887 .bg-red-100 {883 .bg-red-100 {
888 --tw-bg-opacity: 1;884 --tw-bg-opacity: 1;
889- background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));885+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
890 }886 }
891 887
892 .bg-slate-100 {888 .bg-slate-100 {
893 --tw-bg-opacity: 1;889 --tw-bg-opacity: 1;
894- background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));890+ background-color: rgb(241 245 249 / var(--tw-bg-opacity));
895 }891 }
896 892
897 .bg-slate-200 {893 .bg-slate-200 {
898 --tw-bg-opacity: 1;894 --tw-bg-opacity: 1;
899- background-color: rgb(226 232 240 / var(--tw-bg-opacity, 1));895+ background-color: rgb(226 232 240 / var(--tw-bg-opacity));
900 }896 }
901 897
902 .bg-transparent {898 .bg-transparent {
@@ -905,12 +901,12 @@ video {
905 901
906 .bg-white {902 .bg-white {
907 --tw-bg-opacity: 1;903 --tw-bg-opacity: 1;
908- background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));904+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
909 }905 }
910 906
911 .bg-zinc-200 {907 .bg-zinc-200 {
912 --tw-bg-opacity: 1;908 --tw-bg-opacity: 1;
913- background-color: rgb(228 228 231 / var(--tw-bg-opacity, 1));909+ background-color: rgb(228 228 231 / var(--tw-bg-opacity));
914 }910 }
915 911
916 .p-0 {912 .p-0 {
@@ -1038,72 +1034,72 @@ video {
1038 1034
1039 .text-blue-600 {1035 .text-blue-600 {
1040 --tw-text-opacity: 1;1036 --tw-text-opacity: 1;
1041- color: rgb(37 99 235 / var(--tw-text-opacity, 1));1037+ color: rgb(37 99 235 / var(--tw-text-opacity));
1042 }1038 }
1043 1039
1044 .text-blue-800 {1040 .text-blue-800 {
1045 --tw-text-opacity: 1;1041 --tw-text-opacity: 1;
1046- color: rgb(30 64 175 / var(--tw-text-opacity, 1));1042+ color: rgb(30 64 175 / var(--tw-text-opacity));
1047 }1043 }
1048 1044
1049 .text-gordonmascot-hblue {1045 .text-gordonmascot-hblue {
1050 --tw-text-opacity: 1;1046 --tw-text-opacity: 1;
1051- color: rgb(1 30 92 / var(--tw-text-opacity, 1));1047+ color: rgb(1 30 92 / var(--tw-text-opacity));
1052 }1048 }
1053 1049
1054 .text-gordonmascot-mblue {1050 .text-gordonmascot-mblue {
1055 --tw-text-opacity: 1;1051 --tw-text-opacity: 1;
1056- color: rgb(0 82 147 / var(--tw-text-opacity, 1));1052+ color: rgb(0 82 147 / var(--tw-text-opacity));
1057 }1053 }
1058 1054
1059 .text-gray-100 {1055 .text-gray-100 {
1060 --tw-text-opacity: 1;1056 --tw-text-opacity: 1;
1061- color: rgb(243 244 246 / var(--tw-text-opacity, 1));1057+ color: rgb(243 244 246 / var(--tw-text-opacity));
1062 }1058 }
1063 1059
1064 .text-gray-400 {1060 .text-gray-400 {
1065 --tw-text-opacity: 1;1061 --tw-text-opacity: 1;
1066- color: rgb(156 163 175 / var(--tw-text-opacity, 1));1062+ color: rgb(156 163 175 / var(--tw-text-opacity));
1067 }1063 }
1068 1064
1069 .text-gray-500 {1065 .text-gray-500 {
1070 --tw-text-opacity: 1;1066 --tw-text-opacity: 1;
1071- color: rgb(107 114 128 / var(--tw-text-opacity, 1));1067+ color: rgb(107 114 128 / var(--tw-text-opacity));
1072 }1068 }
1073 1069
1074 .text-gray-700 {1070 .text-gray-700 {
1075 --tw-text-opacity: 1;1071 --tw-text-opacity: 1;
1076- color: rgb(55 65 81 / var(--tw-text-opacity, 1));1072+ color: rgb(55 65 81 / var(--tw-text-opacity));
1077 }1073 }
1078 1074
1079 .text-gray-800 {1075 .text-gray-800 {
1080 --tw-text-opacity: 1;1076 --tw-text-opacity: 1;
1081- color: rgb(31 41 55 / var(--tw-text-opacity, 1));1077+ color: rgb(31 41 55 / var(--tw-text-opacity));
1082 }1078 }
1083 1079
1084 .text-green-300 {1080 .text-green-300 {
1085 --tw-text-opacity: 1;1081 --tw-text-opacity: 1;
1086- color: rgb(134 239 172 / var(--tw-text-opacity, 1));1082+ color: rgb(134 239 172 / var(--tw-text-opacity));
1087 }1083 }
1088 1084
1089 .text-red-500 {1085 .text-red-500 {
1090 --tw-text-opacity: 1;1086 --tw-text-opacity: 1;
1091- color: rgb(239 68 68 / var(--tw-text-opacity, 1));1087+ color: rgb(239 68 68 / var(--tw-text-opacity));
1092 }1088 }
1093 1089
1094 .text-red-600 {1090 .text-red-600 {
1095 --tw-text-opacity: 1;1091 --tw-text-opacity: 1;
1096- color: rgb(220 38 38 / var(--tw-text-opacity, 1));1092+ color: rgb(220 38 38 / var(--tw-text-opacity));
1097 }1093 }
1098 1094
1099 .text-red-700 {1095 .text-red-700 {
1100 --tw-text-opacity: 1;1096 --tw-text-opacity: 1;
1101- color: rgb(185 28 28 / var(--tw-text-opacity, 1));1097+ color: rgb(185 28 28 / var(--tw-text-opacity));
1102 }1098 }
1103 1099
1104 .text-white {1100 .text-white {
1105 --tw-text-opacity: 1;1101 --tw-text-opacity: 1;
1106- color: rgb(255 255 255 / var(--tw-text-opacity, 1));1102+ color: rgb(255 255 255 / var(--tw-text-opacity));
1107 }1103 }
1108 1104
1109 .shadow {1105 .shadow {
@@ -1277,37 +1273,37 @@ video {
1277 1273
1278 .hover\:bg-blue-700:hover {1274 .hover\:bg-blue-700:hover {
1279 --tw-bg-opacity: 1;1275 --tw-bg-opacity: 1;
1280- background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));1276+ background-color: rgb(29 78 216 / var(--tw-bg-opacity));
1281 }1277 }
1282 1278
1283 .hover\:bg-gordonmascot-hblue:hover {1279 .hover\:bg-gordonmascot-hblue:hover {
1284 --tw-bg-opacity: 1;1280 --tw-bg-opacity: 1;
1285- background-color: rgb(1 30 92 / var(--tw-bg-opacity, 1));1281+ background-color: rgb(1 30 92 / var(--tw-bg-opacity));
1286 }1282 }
1287 1283
1288 .hover\:bg-gordonmascot-mblue:hover {1284 .hover\:bg-gordonmascot-mblue:hover {
1289 --tw-bg-opacity: 1;1285 --tw-bg-opacity: 1;
1290- background-color: rgb(0 82 147 / var(--tw-bg-opacity, 1));1286+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
1291 }1287 }
1292 1288
1293 .hover\:bg-slate-100:hover {1289 .hover\:bg-slate-100:hover {
1294 --tw-bg-opacity: 1;1290 --tw-bg-opacity: 1;
1295- background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));1291+ background-color: rgb(241 245 249 / var(--tw-bg-opacity));
1296 }1292 }
1297 1293
1298 .hover\:text-gordonmascot-hblue:hover {1294 .hover\:text-gordonmascot-hblue:hover {
1299 --tw-text-opacity: 1;1295 --tw-text-opacity: 1;
1300- color: rgb(1 30 92 / var(--tw-text-opacity, 1));1296+ color: rgb(1 30 92 / var(--tw-text-opacity));
1301 }1297 }
1302 1298
1303 .hover\:text-red-900:hover {1299 .hover\:text-red-900:hover {
1304 --tw-text-opacity: 1;1300 --tw-text-opacity: 1;
1305- color: rgb(127 29 29 / var(--tw-text-opacity, 1));1301+ color: rgb(127 29 29 / var(--tw-text-opacity));
1306 }1302 }
1307 1303
1308 .focus\:border-gordonmascot-hblue:focus {1304 .focus\:border-gordonmascot-hblue:focus {
1309 --tw-border-opacity: 1;1305 --tw-border-opacity: 1;
1310- border-color: rgb(1 30 92 / var(--tw-border-opacity, 1));1306+ border-color: rgb(1 30 92 / var(--tw-border-opacity));
1311 }1307 }
1312 1308
1313 .focus\:outline-none:focus {1309 .focus\:outline-none:focus {
@@ -1317,7 +1313,7 @@ video {
1317 1313
1318 .focus\:ring-gordonmascot-hblue:focus {1314 .focus\:ring-gordonmascot-hblue:focus {
1319 --tw-ring-opacity: 1;1315 --tw-ring-opacity: 1;
1320- --tw-ring-color: rgb(1 30 92 / var(--tw-ring-opacity, 1));1316+ --tw-ring-color: rgb(1 30 92 / var(--tw-ring-opacity));
1321 }1317 }
1322 1318
1323 @media (min-width: 640px) {1319 @media (min-width: 640px) {
M internal/webui/public/assets/js/custom.js +280 -5
@@ -12,14 +12,14 @@ function initializeVersionCheck() {
1212 "%VERSION%",
1313 fetchedVersionNumber,
1414 );
15- console.log(updateStr);
15+ console.debug("Update available:", updateStr);
1616 const updateElement = document.getElementById("update-available");
1717 updateElement.title = updateStr;
1818 updateElement.toggleAttribute("hidden", false);
1919 updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class
2020 updateElement.classList.add("block"); // Add the Tailwind `block` class
2121 } else {
22- console.log(
22+ console.debug(
2323 "No new version. Current version is up-to-date:",
2424 currentVersion,
2525 );
@@ -36,12 +36,12 @@ function initializeVersionCheck() {
3636 const init = () => {
3737 const currentVersionElement = document.getElementById("actual-version");
3838 if (!currentVersionElement) {
39- console.log("Error: #actual-version element not found.");
39+ console.debug("Error: #actual-version element not found.");
4040 return;
4141 }
4242 const currentVersion = currentVersionElement.textContent.trim();
4343 if (!currentVersion) {
44- console.log("Dev mode detected, skipping version check.");
44+ console.debug("Dev mode detected, skipping version check.");
4545 return;
4646 }
4747 fetchVersionInfo(currentVersion);
@@ -50,4 +50,279 @@ function initializeVersionCheck() {
5050 init();
5151 }
5252
53-document.addEventListener("DOMContentLoaded", initializeVersionCheck);
53+// Initialize jQuery functionality for the admin dashboard
54+function initializeAdminDashboard() {
55+ // Handle edit button clicks to toggle action response visibility
56+ $(document).on('click', '[id^="edit-button-"]', function() {
57+ const id = $(this).attr('id').replace('edit-button-', '');
58+ const actionResponse = $(`#actionResponse-${id}`);
59+
60+ console.debug(`Toggle visibility for actionResponse-${id}`);
61+ actionResponse.toggleClass('visible hidden');
62+
63+ // Update the active state
64+ const isActive = $(this).data('active') === true;
65+ $(this).data('active', !isActive);
66+ });
67+
68+ // Handle upload button click
69+ $(document).on('click', '#upload-button', function() {
70+ console.debug('Upload button clicked');
71+ $('#upload-image').toggleClass('visible hidden');
72+ $('.create-container').addClass('hidden');
73+ });
74+
75+ // Handle add button click
76+ $(document).on('click', '[id^="add-button-img-"]', function() {
77+ const id = $(this).attr('id').replace('add-button-img-', '');
78+ console.debug(`Add button clicked for ${id}`);
79+ $(`#create-container-${id}`).toggleClass('visible hidden');
80+ $('#upload-image').addClass('hidden');
81+ });
82+
83+ // Handle upload submit button
84+ $(document).on('click', '#upload-submit-button', function() {
85+ console.debug('Upload submit button clicked');
86+ $(this).prop('disabled', true);
87+
88+ // Re-enable the button after the HTMX request completes
89+ document.body.addEventListener('htmx:afterOnLoad', function() {
90+ $('#upload-submit-button').prop('disabled', false);
91+ }, { once: true });
92+ });
93+
94+ // Handle success message without page reload
95+ $(document).on('htmx:afterSwap', function(event) {
96+ if ($(event.detail.target).find('#success').length > 0) {
97+ console.debug('Success message detected');
98+
99+ // Instead of reloading the page, refresh the container list
100+ refreshContainerList();
101+
102+ // Animate and remove the success message
103+ setTimeout(function() {
104+ $('#success').animate({ opacity: 0 }, 1000, function() {
105+ $(this).remove();
106+ });
107+ }, 1000);
108+ }
109+ });
110+
111+ // Handle refresh buttons
112+ $(document).on('click', '#refresh-containers-button', function() {
113+ console.debug('Refreshing container list manually');
114+ refreshContainerList();
115+ });
116+
117+ $(document).on('click', '#refresh-images-button', function() {
118+ console.debug('Refreshing image list manually');
119+ refreshImageList();
120+ });
121+
122+ // Setup HTMX event listeners to avoid page reloads
123+ setupHtmxEventListeners();
124+
125+ // Initialize notification system
126+ initializeNotifications();
127+
128+ console.debug("Admin dashboard jQuery functionality initialized");
129+}
130+
131+// Function to initialize notification system
132+function initializeNotifications() {
133+ // Create notification container if it doesn't exist
134+ if ($('#notification-container').length === 0) {
135+ $('body').append('<div id="notification-container" class="fixed top-4 right-4 z-50 flex flex-col space-y-2"></div>');
136+ }
137+
138+ console.debug("Notification system initialized");
139+}
140+
141+// Function to show a notification
142+function showNotification(message, type = 'success', duration = 3000) {
143+ const notificationId = 'notification-' + Date.now();
144+ const bgColor = type === 'success' ? 'bg-green-500' : type === 'error' ? 'bg-red-500' : 'bg-blue-500';
145+
146+ const notification = $(`
147+ <div id="${notificationId}" class="${bgColor} text-white p-3 rounded-md shadow-md opacity-0 transition-opacity duration-300">
148+ ${message}
149+ </div>
150+ `);
151+
152+ $('#notification-container').append(notification);
153+
154+ // Fade in
155+ setTimeout(() => {
156+ $(`#${notificationId}`).css('opacity', '1');
157+ }, 10);
158+
159+ // Fade out and remove after duration
160+ setTimeout(() => {
161+ $(`#${notificationId}`).css('opacity', '0');
162+ setTimeout(() => {
163+ $(`#${notificationId}`).remove();
164+ }, 300);
165+ }, duration);
166+
167+ console.debug(`Notification shown: ${message}`);
168+}
169+
170+// Function to refresh container list without page reload
171+function refreshContainerList() {
172+ console.debug("Refreshing container list");
173+
174+ // Show loading indicator
175+ const containerManager = $('#container-manager');
176+ const originalContent = containerManager.html();
177+ containerManager.html('<div class="text-center p-4"><span class="iconf text-3xl animate-spin inline-block">󰑐</span> Loading containers...</div>');
178+
179+ $.ajax({
180+ url: '/htmx/container-manager',
181+ type: 'GET',
182+ success: function(data) {
183+ $('#container-manager').html(data);
184+ console.debug("Container list refreshed successfully");
185+ },
186+ error: function(xhr, status, error) {
187+ console.error("Error refreshing container list:", error);
188+ // Restore original content on error
189+ containerManager.html(originalContent);
190+ }
191+ });
192+}
193+
194+// Function to refresh image list without page reload
195+function refreshImageList() {
196+ console.debug("Refreshing image list");
197+
198+ // Show loading indicator
199+ const imageManager = $('#image-manager');
200+ const originalContent = imageManager.html();
201+ imageManager.html('<div class="text-center p-4"><span class="iconf text-3xl animate-spin inline-block">󰑐</span> Loading images...</div>');
202+
203+ $.ajax({
204+ url: '/htmx/image-manager',
205+ type: 'GET',
206+ success: function(data) {
207+ $('#image-manager').html(data);
208+ console.debug("Image list refreshed successfully");
209+ },
210+ error: function(xhr, status, error) {
211+ console.error("Error refreshing image list:", error);
212+ // Restore original content on error
213+ imageManager.html(originalContent);
214+ }
215+ });
216+}
217+
218+// Setup HTMX event listeners to avoid page reloads
219+function setupHtmxEventListeners() {
220+ // Listen for container actions (start, stop, edit, delete)
221+ document.body.addEventListener('htmx:afterOnLoad', function(event) {
222+ const targetId = event.detail.target.id;
223+ console.debug(`HTMX afterOnLoad event for target: ${targetId}`);
224+
225+ // Check if the target is an action response
226+ if (targetId && targetId.startsWith('actionResponse-')) {
227+ console.debug(`Action completed for ${targetId}`);
228+
229+ // Show notification instead of reloading
230+ const responseText = $(event.detail.target).text().trim();
231+ if (responseText.includes('Success')) {
232+ showNotification('Action completed successfully', 'success');
233+ } else if (responseText) {
234+ showNotification(responseText, 'info');
235+ }
236+
237+ // Refresh container list after a short delay
238+ setTimeout(refreshContainerList, 500);
239+ }
240+
241+ // Check if the target is the image manager (after upload)
242+ if (targetId === 'image-manager') {
243+ console.debug('Image manager updated');
244+
245+ // Show notification for successful upload
246+ if (event.detail.xhr.responseText.includes('Success')) {
247+ showNotification('Image uploaded successfully', 'success');
248+ }
249+
250+ // Hide the upload form after successful upload
251+ $('#upload-image').addClass('hidden');
252+ }
253+
254+ // Check if this is a container creation form response
255+ if (targetId && targetId.includes('create-container-')) {
256+ console.debug(`Container creation form response for ${targetId}`);
257+ const responseText = $(event.detail.target).text().trim();
258+
259+ // If the response contains success message
260+ if (responseText.includes('Success') || responseText.includes('Container created successfully')) {
261+ showNotification('Container created successfully', 'success');
262+
263+ // Hide the container creation form
264+ $(event.detail.target).closest('.create-container').addClass('hidden');
265+
266+ // Refresh container list to show the new container
267+ setTimeout(refreshContainerList, 500);
268+ }
269+ }
270+ });
271+
272+ // Listen for form submissions
273+ $(document).on('submit', 'form[hx-post^="/htmx/create-container/"]', function(event) {
274+ console.debug('Container creation form submitted');
275+
276+ // Disable the submit button to prevent double submission
277+ $('#create-container-submit').prop('disabled', true);
278+
279+ // We don't prevent default because HTMX needs to handle the form submission
280+ });
281+
282+ // Listen for container creation
283+ $(document).on('htmx:afterRequest', function(event) {
284+ const path = event.detail.pathInfo.requestPath;
285+ console.debug(`HTMX afterRequest for path: ${path}`);
286+
287+ // If this was a container creation request
288+ if (path && path.includes('/htmx/create-container/')) {
289+ console.debug('Container creation request completed');
290+
291+ // Re-enable the submit button
292+ $('#create-container-submit').prop('disabled', false);
293+
294+ // Check if the response indicates success
295+ const responseText = event.detail.xhr.responseText;
296+ if (responseText && (responseText.includes('Success') || responseText.includes('Container created successfully'))) {
297+ showNotification('Container created successfully', 'success');
298+
299+ // Refresh container list after creation
300+ setTimeout(refreshContainerList, 500);
301+ } else if (responseText && responseText.includes('Error')) {
302+ showNotification('Error creating container: ' + responseText, 'error');
303+ }
304+ }
305+
306+ // If this was an image deletion request
307+ if (path && path.includes('/htmx/image-manager/delete/')) {
308+ console.debug('Image deletion request completed');
309+
310+ // Show notification
311+ showNotification('Image deleted successfully', 'success');
312+
313+ // Refresh image list after deletion
314+ setTimeout(refreshImageList, 500);
315+ }
316+ });
317+
318+ // Handle errors
319+ $(document).on('htmx:responseError', function(event) {
320+ console.error('HTMX response error:', event.detail.error);
321+ showNotification('Error: ' + (event.detail.error || 'Unknown error occurred'), 'error');
322+ });
323+}
324+
325+document.addEventListener("DOMContentLoaded", function() {
326+ initializeVersionCheck();
327+ initializeAdminDashboard();
328+});
@@ -12,14 +12,14 @@ function initializeVersionCheck() {
12 "%VERSION%",12 "%VERSION%",
13 fetchedVersionNumber,13 fetchedVersionNumber,
14 );14 );
15- console.log(updateStr);15+ console.debug("Update available:", updateStr);
16 const updateElement = document.getElementById("update-available");16 const updateElement = document.getElementById("update-available");
17 updateElement.title = updateStr;17 updateElement.title = updateStr;
18 updateElement.toggleAttribute("hidden", false);18 updateElement.toggleAttribute("hidden", false);
19 updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class19 updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class
20 updateElement.classList.add("block"); // Add the Tailwind `block` class20 updateElement.classList.add("block"); // Add the Tailwind `block` class
21 } else {21 } else {
22- console.log(22+ console.debug(
23 "No new version. Current version is up-to-date:",23 "No new version. Current version is up-to-date:",
24 currentVersion,24 currentVersion,
25 );25 );
@@ -36,12 +36,12 @@ function initializeVersionCheck() {
36 const init = () => {36 const init = () => {
37 const currentVersionElement = document.getElementById("actual-version");37 const currentVersionElement = document.getElementById("actual-version");
38 if (!currentVersionElement) {38 if (!currentVersionElement) {
39- console.log("Error: #actual-version element not found.");39+ console.debug("Error: #actual-version element not found.");
40 return;40 return;
41 }41 }
42 const currentVersion = currentVersionElement.textContent.trim();42 const currentVersion = currentVersionElement.textContent.trim();
43 if (!currentVersion) {43 if (!currentVersion) {
44- console.log("Dev mode detected, skipping version check.");44+ console.debug("Dev mode detected, skipping version check.");
45 return;45 return;
46 }46 }
47 fetchVersionInfo(currentVersion);47 fetchVersionInfo(currentVersion);
@@ -50,4 +50,279 @@ function initializeVersionCheck() {
50 init();50 init();
51 }51 }
52 52
53-document.addEventListener("DOMContentLoaded", initializeVersionCheck);53+// Initialize jQuery functionality for the admin dashboard
54+function initializeAdminDashboard() {
55+ // Handle edit button clicks to toggle action response visibility
56+ $(document).on('click', '[id^="edit-button-"]', function() {
57+ const id = $(this).attr('id').replace('edit-button-', '');
58+ const actionResponse = $(`#actionResponse-${id}`);
59+
60+ console.debug(`Toggle visibility for actionResponse-${id}`);
61+ actionResponse.toggleClass('visible hidden');
62+
63+ // Update the active state
64+ const isActive = $(this).data('active') === true;
65+ $(this).data('active', !isActive);
66+ });
67+
68+ // Handle upload button click
69+ $(document).on('click', '#upload-button', function() {
70+ console.debug('Upload button clicked');
71+ $('#upload-image').toggleClass('visible hidden');
72+ $('.create-container').addClass('hidden');
73+ });
74+
75+ // Handle add button click
76+ $(document).on('click', '[id^="add-button-img-"]', function() {
77+ const id = $(this).attr('id').replace('add-button-img-', '');
78+ console.debug(`Add button clicked for ${id}`);
79+ $(`#create-container-${id}`).toggleClass('visible hidden');
80+ $('#upload-image').addClass('hidden');
81+ });
82+
83+ // Handle upload submit button
84+ $(document).on('click', '#upload-submit-button', function() {
85+ console.debug('Upload submit button clicked');
86+ $(this).prop('disabled', true);
87+
88+ // Re-enable the button after the HTMX request completes
89+ document.body.addEventListener('htmx:afterOnLoad', function() {
90+ $('#upload-submit-button').prop('disabled', false);
91+ }, { once: true });
92+ });
93+
94+ // Handle success message without page reload
95+ $(document).on('htmx:afterSwap', function(event) {
96+ if ($(event.detail.target).find('#success').length > 0) {
97+ console.debug('Success message detected');
98+
99+ // Instead of reloading the page, refresh the container list
100+ refreshContainerList();
101+
102+ // Animate and remove the success message
103+ setTimeout(function() {
104+ $('#success').animate({ opacity: 0 }, 1000, function() {
105+ $(this).remove();
106+ });
107+ }, 1000);
108+ }
109+ });
110+
111+ // Handle refresh buttons
112+ $(document).on('click', '#refresh-containers-button', function() {
113+ console.debug('Refreshing container list manually');
114+ refreshContainerList();
115+ });
116+
117+ $(document).on('click', '#refresh-images-button', function() {
118+ console.debug('Refreshing image list manually');
119+ refreshImageList();
120+ });
121+
122+ // Setup HTMX event listeners to avoid page reloads
123+ setupHtmxEventListeners();
124+
125+ // Initialize notification system
126+ initializeNotifications();
127+
128+ console.debug("Admin dashboard jQuery functionality initialized");
129+}
130+
131+// Function to initialize notification system
132+function initializeNotifications() {
133+ // Create notification container if it doesn't exist
134+ if ($('#notification-container').length === 0) {
135+ $('body').append('<div id="notification-container" class="fixed top-4 right-4 z-50 flex flex-col space-y-2"></div>');
136+ }
137+
138+ console.debug("Notification system initialized");
139+}
140+
141+// Function to show a notification
142+function showNotification(message, type = 'success', duration = 3000) {
143+ const notificationId = 'notification-' + Date.now();
144+ const bgColor = type === 'success' ? 'bg-green-500' : type === 'error' ? 'bg-red-500' : 'bg-blue-500';
145+
146+ const notification = $(`
147+ <div id="${notificationId}" class="${bgColor} text-white p-3 rounded-md shadow-md opacity-0 transition-opacity duration-300">
148+ ${message}
149+ </div>
150+ `);
151+
152+ $('#notification-container').append(notification);
153+
154+ // Fade in
155+ setTimeout(() => {
156+ $(`#${notificationId}`).css('opacity', '1');
157+ }, 10);
158+
159+ // Fade out and remove after duration
160+ setTimeout(() => {
161+ $(`#${notificationId}`).css('opacity', '0');
162+ setTimeout(() => {
163+ $(`#${notificationId}`).remove();
164+ }, 300);
165+ }, duration);
166+
167+ console.debug(`Notification shown: ${message}`);
168+}
169+
170+// Function to refresh container list without page reload
171+function refreshContainerList() {
172+ console.debug("Refreshing container list");
173+
174+ // Show loading indicator
175+ const containerManager = $('#container-manager');
176+ const originalContent = containerManager.html();
177+ containerManager.html('<div class="text-center p-4"><span class="iconf text-3xl animate-spin inline-block">󰑐</span> Loading containers...</div>');
178+
179+ $.ajax({
180+ url: '/htmx/container-manager',
181+ type: 'GET',
182+ success: function(data) {
183+ $('#container-manager').html(data);
184+ console.debug("Container list refreshed successfully");
185+ },
186+ error: function(xhr, status, error) {
187+ console.error("Error refreshing container list:", error);
188+ // Restore original content on error
189+ containerManager.html(originalContent);
190+ }
191+ });
192+}
193+
194+// Function to refresh image list without page reload
195+function refreshImageList() {
196+ console.debug("Refreshing image list");
197+
198+ // Show loading indicator
199+ const imageManager = $('#image-manager');
200+ const originalContent = imageManager.html();
201+ imageManager.html('<div class="text-center p-4"><span class="iconf text-3xl animate-spin inline-block">󰑐</span> Loading images...</div>');
202+
203+ $.ajax({
204+ url: '/htmx/image-manager',
205+ type: 'GET',
206+ success: function(data) {
207+ $('#image-manager').html(data);
208+ console.debug("Image list refreshed successfully");
209+ },
210+ error: function(xhr, status, error) {
211+ console.error("Error refreshing image list:", error);
212+ // Restore original content on error
213+ imageManager.html(originalContent);
214+ }
215+ });
216+}
217+
218+// Setup HTMX event listeners to avoid page reloads
219+function setupHtmxEventListeners() {
220+ // Listen for container actions (start, stop, edit, delete)
221+ document.body.addEventListener('htmx:afterOnLoad', function(event) {
222+ const targetId = event.detail.target.id;
223+ console.debug(`HTMX afterOnLoad event for target: ${targetId}`);
224+
225+ // Check if the target is an action response
226+ if (targetId && targetId.startsWith('actionResponse-')) {
227+ console.debug(`Action completed for ${targetId}`);
228+
229+ // Show notification instead of reloading
230+ const responseText = $(event.detail.target).text().trim();
231+ if (responseText.includes('Success')) {
232+ showNotification('Action completed successfully', 'success');
233+ } else if (responseText) {
234+ showNotification(responseText, 'info');
235+ }
236+
237+ // Refresh container list after a short delay
238+ setTimeout(refreshContainerList, 500);
239+ }
240+
241+ // Check if the target is the image manager (after upload)
242+ if (targetId === 'image-manager') {
243+ console.debug('Image manager updated');
244+
245+ // Show notification for successful upload
246+ if (event.detail.xhr.responseText.includes('Success')) {
247+ showNotification('Image uploaded successfully', 'success');
248+ }
249+
250+ // Hide the upload form after successful upload
251+ $('#upload-image').addClass('hidden');
252+ }
253+
254+ // Check if this is a container creation form response
255+ if (targetId && targetId.includes('create-container-')) {
256+ console.debug(`Container creation form response for ${targetId}`);
257+ const responseText = $(event.detail.target).text().trim();
258+
259+ // If the response contains success message
260+ if (responseText.includes('Success') || responseText.includes('Container created successfully')) {
261+ showNotification('Container created successfully', 'success');
262+
263+ // Hide the container creation form
264+ $(event.detail.target).closest('.create-container').addClass('hidden');
265+
266+ // Refresh container list to show the new container
267+ setTimeout(refreshContainerList, 500);
268+ }
269+ }
270+ });
271+
272+ // Listen for form submissions
273+ $(document).on('submit', 'form[hx-post^="/htmx/create-container/"]', function(event) {
274+ console.debug('Container creation form submitted');
275+
276+ // Disable the submit button to prevent double submission
277+ $('#create-container-submit').prop('disabled', true);
278+
279+ // We don't prevent default because HTMX needs to handle the form submission
280+ });
281+
282+ // Listen for container creation
283+ $(document).on('htmx:afterRequest', function(event) {
284+ const path = event.detail.pathInfo.requestPath;
285+ console.debug(`HTMX afterRequest for path: ${path}`);
286+
287+ // If this was a container creation request
288+ if (path && path.includes('/htmx/create-container/')) {
289+ console.debug('Container creation request completed');
290+
291+ // Re-enable the submit button
292+ $('#create-container-submit').prop('disabled', false);
293+
294+ // Check if the response indicates success
295+ const responseText = event.detail.xhr.responseText;
296+ if (responseText && (responseText.includes('Success') || responseText.includes('Container created successfully'))) {
297+ showNotification('Container created successfully', 'success');
298+
299+ // Refresh container list after creation
300+ setTimeout(refreshContainerList, 500);
301+ } else if (responseText && responseText.includes('Error')) {
302+ showNotification('Error creating container: ' + responseText, 'error');
303+ }
304+ }
305+
306+ // If this was an image deletion request
307+ if (path && path.includes('/htmx/image-manager/delete/')) {
308+ console.debug('Image deletion request completed');
309+
310+ // Show notification
311+ showNotification('Image deleted successfully', 'success');
312+
313+ // Refresh image list after deletion
314+ setTimeout(refreshImageList, 500);
315+ }
316+ });
317+
318+ // Handle errors
319+ $(document).on('htmx:responseError', function(event) {
320+ console.error('HTMX response error:', event.detail.error);
321+ showNotification('Error: ' + (event.detail.error || 'Unknown error occurred'), 'error');
322+ });
323+}
324+
325+document.addEventListener("DOMContentLoaded", function() {
326+ initializeVersionCheck();
327+ initializeAdminDashboard();
328+});
D internal/webui/public/libs/chart.js +0 -20 too large to show in the browser

too large to show in the browser

M internal/webui/public/libs/htmx-response-targets.js +14 -1
@@ -1,10 +1,20 @@
11 (function(){
22
3+ if (htmx.version && !htmx.version.startsWith("1.")) {
4+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
5+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
6+ }
7+
38 /** @type {import("../htmx").HtmxInternalApi} */
49 var api;
510
611 var attrPrefix = 'hx-target-';
712
13+ // IE11 doesn't support string.startsWith
14+ function startsWith(str, prefix) {
15+ return str.substring(0, prefix.length) === prefix
16+ }
17+
818 /**
919 * @param {HTMLElement} elt
1020 * @param {number} respCode
@@ -38,6 +48,9 @@
3848 '***',
3949 'xxx',
4050 ];
51+ if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
52+ attrPossibilities.push('error');
53+ }
4154
4255 for (var i = 0; i < attrPossibilities.length; i++) {
4356 var attr = attrPrefix + attrPossibilities[i];
@@ -50,7 +63,7 @@
5063 }
5164 }
5265 }
53-
66+
5467 return null;
5568 }
5669
@@ -1,10 +1,20 @@
1 (function(){1 (function(){
2 2
3+ if (htmx.version && !htmx.version.startsWith("1.")) {
4+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
5+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
6+ }
7+
3 /** @type {import("../htmx").HtmxInternalApi} */8 /** @type {import("../htmx").HtmxInternalApi} */
4 var api;9 var api;
5 10
6 var attrPrefix = 'hx-target-';11 var attrPrefix = 'hx-target-';
7 12
13+ // IE11 doesn't support string.startsWith
14+ function startsWith(str, prefix) {
15+ return str.substring(0, prefix.length) === prefix
16+ }
17+
8 /**18 /**
9 * @param {HTMLElement} elt19 * @param {HTMLElement} elt
10 * @param {number} respCode20 * @param {number} respCode
@@ -38,6 +48,9 @@
38 '***',48 '***',
39 'xxx',49 'xxx',
40 ];50 ];
51+ if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
52+ attrPossibilities.push('error');
53+ }
41 54
42 for (var i = 0; i < attrPossibilities.length; i++) {55 for (var i = 0; i < attrPossibilities.length; i++) {
43 var attr = attrPrefix + attrPossibilities[i];56 var attr = attrPrefix + attrPossibilities[i];
@@ -50,7 +63,7 @@
50 }63 }
51 }64 }
52 }65 }
53- 66+
54 return null;67 return null;
55 }68 }
56 69
M internal/webui/public/libs/htmx-sse.js +156 -104
@@ -5,7 +5,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
55
66 */
77
8-(function(){
8+(function() {
9+
10+ if (htmx.version && !htmx.version.startsWith("1.")) {
11+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
12+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
13+ }
914
1015 /** @type {import("../htmx").HtmxInternalApi} */
1116 var api;
@@ -14,8 +19,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
1419
1520 /**
1621 * Init saves the provided reference to the internal HTMX API.
17- *
18- * @param {import("../htmx").HtmxInternalApi} api
22+ *
23+ * @param {import("../htmx").HtmxInternalApi} api
1924 * @returns void
2025 */
2126 init: function(apiRef) {
@@ -30,26 +35,28 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
3035
3136 /**
3237 * onEvent handles all events passed to this extension.
33- *
34- * @param {string} name
35- * @param {Event} evt
38+ *
39+ * @param {string} name
40+ * @param {Event} evt
3641 * @returns void
3742 */
3843 onEvent: function(name, evt) {
3944
45+ var parent = evt.target || evt.detail.elt;
4046 switch (name) {
4147
42- // Try to remove remove an EventSource when elements are removed
43- case "htmx:beforeCleanupElement":
44- var internalData = api.getInternalData(evt.target)
45- if (internalData.sseEventSource) {
46- internalData.sseEventSource.close();
47- }
48- return;
48+ case "htmx:beforeCleanupElement":
49+ var internalData = api.getInternalData(parent)
50+ // Try to remove remove an EventSource when elements are removed
51+ if (internalData.sseEventSource) {
52+ internalData.sseEventSource.close();
53+ }
54+
55+ return;
4956
50- // Try to create EventSources when elements are processed
51- case "htmx:afterProcessNode":
52- createEventSourceOnElement(evt.target);
57+ // Try to create EventSources when elements are processed
58+ case "htmx:afterProcessNode":
59+ ensureEventSourceOnElement(parent);
5360 }
5461 }
5562 });
@@ -62,12 +69,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
6269 /**
6370 * createEventSource is the default method for creating new EventSource objects.
6471 * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
65- *
66- * @param {string} url
72+ *
73+ * @param {string} url
6774 * @returns EventSource
6875 */
69- function createEventSource(url) {
70- return new EventSource(url, {withCredentials:true});
76+ function createEventSource(url) {
77+ return new EventSource(url, { withCredentials: true });
7178 }
7279
7380 function splitOnWhitespace(trigger) {
@@ -90,7 +97,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
9097 function getLegacySSESwaps(elt) {
9198 var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
9299 var returnArr = [];
93- if (legacySSEValue) {
100+ if (legacySSEValue != null) {
94101 var values = splitOnWhitespace(legacySSEValue);
95102 for (var i = 0; i < values.length; i++) {
96103 var value = values[i].split(/:(.+)/);
@@ -103,65 +110,25 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
103110 }
104111
105112 /**
106- * createEventSourceOnElement creates a new EventSource connection on the provided element.
107- * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
108- * is created and stored in the element's internalData.
113+ * registerSSE looks for attributes that can contain sse events, right
114+ * now hx-trigger and sse-swap and adds listeners based on these attributes too
115+ * the closest event source
116+ *
109117 * @param {HTMLElement} elt
110- * @param {number} retryCount
111- * @returns {EventSource | null}
112118 */
113- function createEventSourceOnElement(elt, retryCount) {
114-
115- if (elt == null) {
116- return null;
117- }
118-
119- var internalData = api.getInternalData(elt);
120-
121- // get URL from element's attribute
122- var sseURL = api.getAttributeValue(elt, "sse-connect");
123-
124-
125- if (sseURL == undefined) {
126- var legacyURL = getLegacySSEURL(elt)
127- if (legacyURL) {
128- sseURL = legacyURL;
129- } else {
130- return null;
131- }
132- }
133-
134- // Connect to the EventSource
135- var source = htmx.createEventSource(sseURL);
136- internalData.sseEventSource = source;
137-
138- // Create event handlers
139- source.onerror = function (err) {
140-
141- // Log an error event
142- api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
143-
144- // If parent no longer exists in the document, then clean up this EventSource
145- if (maybeCloseSSESource(elt)) {
146- return;
119+ function registerSSE(elt) {
120+ // Add message handlers for every `sse-swap` attribute
121+ queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) {
122+ // Find closest existing event source
123+ var sourceElement = api.getClosestMatch(child, hasEventSource);
124+ if (sourceElement == null) {
125+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
126+ return null; // no eventsource in parentage, orphaned element
147127 }
148128
149- // Otherwise, try to reconnect the EventSource
150- if (source.readyState === EventSource.CLOSED) {
151- retryCount = retryCount || 0;
152- var timeout = Math.random() * (2 ^ retryCount) * 500;
153- window.setTimeout(function() {
154- createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155- }, timeout);
156- }
157- };
158-
159- source.onopen = function (evt) {
160- api.triggerEvent(elt, "htmx:sseOpen", {source: source});
161- }
162-
163- // Add message handlers for every `sse-swap` attribute
164- queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
129+ // Set internalData and source
130+ var internalData = api.getInternalData(sourceElement);
131+ var source = internalData.sseEventSource;
165132
166133 var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
167134 if (sseSwapAttr) {
@@ -170,29 +137,47 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
170137 var sseEventNames = getLegacySSESwaps(child);
171138 }
172139
173- for (var i = 0 ; i < sseEventNames.length ; i++) {
140+ for (var i = 0; i < sseEventNames.length; i++) {
174141 var sseEventName = sseEventNames[i].trim();
175142 var listener = function(event) {
176143
177- // If the parent is missing then close SSE and remove listener
178- if (maybeCloseSSESource(elt)) {
144+ // If the source is missing then close SSE
145+ if (maybeCloseSSESource(sourceElement)) {
146+ return;
147+ }
148+
149+ // If the body no longer contains the element, remove the listener
150+ if (!api.bodyContains(child)) {
179151 source.removeEventListener(sseEventName, listener);
180152 return;
181153 }
182154
183155 // swap the response into the DOM and trigger a notification
156+ if(!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
157+ return;
158+ }
184159 swap(child, event.data);
185160 api.triggerEvent(elt, "htmx:sseMessage", event);
186161 };
187162
188163 // Register the new listener
189- api.getInternalData(elt).sseEventListener = listener;
164+ api.getInternalData(child).sseEventListener = listener;
190165 source.addEventListener(sseEventName, listener);
191166 }
192167 });
193168
194169 // Add message handlers for every `hx-trigger="sse:*"` attribute
195170 queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
171+ // Find closest existing event source
172+ var sourceElement = api.getClosestMatch(child, hasEventSource);
173+ if (sourceElement == null) {
174+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
175+ return null; // no eventsource in parentage, orphaned element
176+ }
177+
178+ // Set internalData and source
179+ var internalData = api.getInternalData(sourceElement);
180+ var source = internalData.sseEventSource;
196181
197182 var sseEventName = api.getAttributeValue(child, "hx-trigger");
198183 if (sseEventName == null) {
@@ -204,30 +189,93 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
204189 return;
205190 }
206191
207- var listener = function(event) {
192+ // remove the sse: prefix from here on out
193+ sseEventName = sseEventName.substr(4);
194+
195+ var listener = function() {
196+ if (maybeCloseSSESource(sourceElement)) {
197+ return
198+ }
208199
209- // If parent is missing, then close SSE and remove listener
210- if (maybeCloseSSESource(elt)) {
200+ if (!api.bodyContains(child)) {
211201 source.removeEventListener(sseEventName, listener);
212- return;
213202 }
203+ }
204+ });
205+ }
206+
207+ /**
208+ * ensureEventSourceOnElement creates a new EventSource connection on the provided element.
209+ * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
210+ * is created and stored in the element's internalData.
211+ * @param {HTMLElement} elt
212+ * @param {number} retryCount
213+ * @returns {EventSource | null}
214+ */
215+ function ensureEventSourceOnElement(elt, retryCount) {
214216
215- // Trigger events to be handled by the rest of htmx
216- htmx.trigger(child, sseEventName, event);
217- htmx.trigger(child, "htmx:sseMessage", event);
217+ if (elt == null) {
218+ return null;
219+ }
220+
221+ // handle extension source creation attribute
222+ queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
223+ var sseURL = api.getAttributeValue(child, "sse-connect");
224+ if (sseURL == null) {
225+ return;
226+ }
227+
228+ ensureEventSource(child, sseURL, retryCount);
229+ });
230+
231+ // handle legacy sse, remove for HTMX2
232+ queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
233+ var sseURL = getLegacySSEURL(child);
234+ if (sseURL == null) {
235+ return;
218236 }
219237
220- // Register the new listener
221- api.getInternalData(elt).sseEventListener = listener;
222- source.addEventListener(sseEventName.slice(4), listener);
238+ ensureEventSource(child, sseURL, retryCount);
223239 });
240+
241+ registerSSE(elt);
242+ }
243+
244+ function ensureEventSource(elt, url, retryCount) {
245+ var source = htmx.createEventSource(url);
246+
247+ source.onerror = function(err) {
248+
249+ // Log an error event
250+ api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
251+
252+ // If parent no longer exists in the document, then clean up this EventSource
253+ if (maybeCloseSSESource(elt)) {
254+ return;
255+ }
256+
257+ // Otherwise, try to reconnect the EventSource
258+ if (source.readyState === EventSource.CLOSED) {
259+ retryCount = retryCount || 0;
260+ var timeout = Math.random() * (2 ^ retryCount) * 500;
261+ window.setTimeout(function() {
262+ ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
263+ }, timeout);
264+ }
265+ };
266+
267+ source.onopen = function(evt) {
268+ api.triggerEvent(elt, "htmx:sseOpen", { source: source });
269+ }
270+
271+ api.getInternalData(elt).sseEventSource = source;
224272 }
225273
226274 /**
227275 * maybeCloseSSESource confirms that the parent element still exists.
228276 * If not, then any associated SSE source is closed and the function returns true.
229- *
230- * @param {HTMLElement} elt
277+ *
278+ * @param {HTMLElement} elt
231279 * @returns boolean
232280 */
233281 function maybeCloseSSESource(elt) {
@@ -244,21 +292,21 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
244292
245293 /**
246294 * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
247- *
248- * @param {HTMLElement} elt
249- * @param {string} attributeName
295+ *
296+ * @param {HTMLElement} elt
297+ * @param {string} attributeName
250298 */
251299 function queryAttributeOnThisOrChildren(elt, attributeName) {
252300
253301 var result = [];
254302
255303 // If the parent element also contains the requested attribute, then add it to the results too.
256- if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
304+ if (api.hasAttribute(elt, attributeName)) {
257305 result.push(elt);
258306 }
259307
260308 // Search all child nodes that match the requested attribute
261- elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
309+ elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
262310 result.push(node);
263311 });
264312
@@ -267,7 +315,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
267315
268316 /**
269317 * @param {HTMLElement} elt
270- * @param {string} content
318+ * @param {string} content
271319 */
272320 function swap(elt, content) {
273321
@@ -281,7 +329,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
281329
282330 api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283331
284- settleInfo.elts.forEach(function (elt) {
332+ settleInfo.elts.forEach(function(elt) {
285333 if (elt.classList) {
286334 elt.classList.add(htmx.config.settlingClass);
287335 }
@@ -297,20 +345,20 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
297345 }
298346
299347 /**
300- * doSettle mirrors much of the functionality in htmx that
348+ * doSettle mirrors much of the functionality in htmx that
301349 * settles elements after their content has been swapped.
302350 * TODO: this should be published by htmx, and not duplicated here
303- * @param {import("../htmx").HtmxSettleInfo} settleInfo
351+ * @param {import("../htmx").HtmxSettleInfo} settleInfo
304352 * @returns () => void
305353 */
306354 function doSettle(settleInfo) {
307355
308356 return function() {
309- settleInfo.tasks.forEach(function (task) {
357+ settleInfo.tasks.forEach(function(task) {
310358 task.call();
311359 });
312360
313- settleInfo.elts.forEach(function (elt) {
361+ settleInfo.elts.forEach(function(elt) {
314362 if (elt.classList) {
315363 elt.classList.remove(htmx.config.settlingClass);
316364 }
@@ -319,4 +367,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
319367 }
320368 }
321369
322-})();⏎̸
370+ function hasEventSource(node) {
371+ return api.getInternalData(node).sseEventSource != null;
372+ }
373+
374+})();
@@ -5,7 +5,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
5 5
6 */6 */
7 7
8-(function(){8+(function() {
9+
10+ if (htmx.version && !htmx.version.startsWith("1.")) {
11+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
12+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
13+ }
9 14
10 /** @type {import("../htmx").HtmxInternalApi} */15 /** @type {import("../htmx").HtmxInternalApi} */
11 var api;16 var api;
@@ -14,8 +19,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
14 19
15 /**20 /**
16 * Init saves the provided reference to the internal HTMX API.21 * Init saves the provided reference to the internal HTMX API.
17- * 22+ *
18- * @param {import("../htmx").HtmxInternalApi} api 23+ * @param {import("../htmx").HtmxInternalApi} api
19 * @returns void24 * @returns void
20 */25 */
21 init: function(apiRef) {26 init: function(apiRef) {
@@ -30,26 +35,28 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
30 35
31 /**36 /**
32 * onEvent handles all events passed to this extension.37 * onEvent handles all events passed to this extension.
33- * 38+ *
34- * @param {string} name 39+ * @param {string} name
35- * @param {Event} evt 40+ * @param {Event} evt
36 * @returns void41 * @returns void
37 */42 */
38 onEvent: function(name, evt) {43 onEvent: function(name, evt) {
39 44
45+ var parent = evt.target || evt.detail.elt;
40 switch (name) {46 switch (name) {
41 47
42- // Try to remove remove an EventSource when elements are removed48+ case "htmx:beforeCleanupElement":
43- case "htmx:beforeCleanupElement":49+ var internalData = api.getInternalData(parent)
44- var internalData = api.getInternalData(evt.target)50+ // Try to remove remove an EventSource when elements are removed
45- if (internalData.sseEventSource) {51+ if (internalData.sseEventSource) {
46- internalData.sseEventSource.close();52+ internalData.sseEventSource.close();
47- }53+ }
48- return;54+
55+ return;
49 56
50- // Try to create EventSources when elements are processed57+ // Try to create EventSources when elements are processed
51- case "htmx:afterProcessNode":58+ case "htmx:afterProcessNode":
52- createEventSourceOnElement(evt.target);59+ ensureEventSourceOnElement(parent);
53 }60 }
54 }61 }
55 });62 });
@@ -62,12 +69,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
62 /**69 /**
63 * createEventSource is the default method for creating new EventSource objects.70 * createEventSource is the default method for creating new EventSource objects.
64 * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.71 * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
65- * 72+ *
66- * @param {string} url 73+ * @param {string} url
67 * @returns EventSource74 * @returns EventSource
68 */75 */
69- function createEventSource(url) {76+ function createEventSource(url) {
70- return new EventSource(url, {withCredentials:true});77+ return new EventSource(url, { withCredentials: true });
71 }78 }
72 79
73 function splitOnWhitespace(trigger) {80 function splitOnWhitespace(trigger) {
@@ -90,7 +97,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
90 function getLegacySSESwaps(elt) {97 function getLegacySSESwaps(elt) {
91 var legacySSEValue = api.getAttributeValue(elt, "hx-sse");98 var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
92 var returnArr = [];99 var returnArr = [];
93- if (legacySSEValue) {100+ if (legacySSEValue != null) {
94 var values = splitOnWhitespace(legacySSEValue);101 var values = splitOnWhitespace(legacySSEValue);
95 for (var i = 0; i < values.length; i++) {102 for (var i = 0; i < values.length; i++) {
96 var value = values[i].split(/:(.+)/);103 var value = values[i].split(/:(.+)/);
@@ -103,65 +110,25 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
103 }110 }
104 111
105 /**112 /**
106- * createEventSourceOnElement creates a new EventSource connection on the provided element.113+ * registerSSE looks for attributes that can contain sse events, right
107- * If a usable EventSource already exists, then it is returned. If not, then a new EventSource114+ * now hx-trigger and sse-swap and adds listeners based on these attributes too
108- * is created and stored in the element's internalData.115+ * the closest event source
116+ *
109 * @param {HTMLElement} elt117 * @param {HTMLElement} elt
110- * @param {number} retryCount
111- * @returns {EventSource | null}
112 */118 */
113- function createEventSourceOnElement(elt, retryCount) {119+ function registerSSE(elt) {
114-120+ // Add message handlers for every `sse-swap` attribute
115- if (elt == null) {121+ queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) {
116- return null;122+ // Find closest existing event source
117- }123+ var sourceElement = api.getClosestMatch(child, hasEventSource);
118-124+ if (sourceElement == null) {
119- var internalData = api.getInternalData(elt);125+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
120-126+ return null; // no eventsource in parentage, orphaned element
121- // get URL from element's attribute
122- var sseURL = api.getAttributeValue(elt, "sse-connect");
123-
124-
125- if (sseURL == undefined) {
126- var legacyURL = getLegacySSEURL(elt)
127- if (legacyURL) {
128- sseURL = legacyURL;
129- } else {
130- return null;
131- }
132- }
133-
134- // Connect to the EventSource
135- var source = htmx.createEventSource(sseURL);
136- internalData.sseEventSource = source;
137-
138- // Create event handlers
139- source.onerror = function (err) {
140-
141- // Log an error event
142- api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
143-
144- // If parent no longer exists in the document, then clean up this EventSource
145- if (maybeCloseSSESource(elt)) {
146- return;
147 }127 }
148 128
149- // Otherwise, try to reconnect the EventSource129+ // Set internalData and source
150- if (source.readyState === EventSource.CLOSED) {130+ var internalData = api.getInternalData(sourceElement);
151- retryCount = retryCount || 0;131+ var source = internalData.sseEventSource;
152- var timeout = Math.random() * (2 ^ retryCount) * 500;
153- window.setTimeout(function() {
154- createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155- }, timeout);
156- }
157- };
158-
159- source.onopen = function (evt) {
160- api.triggerEvent(elt, "htmx:sseOpen", {source: source});
161- }
162-
163- // Add message handlers for every `sse-swap` attribute
164- queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
165 132
166 var sseSwapAttr = api.getAttributeValue(child, "sse-swap");133 var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
167 if (sseSwapAttr) {134 if (sseSwapAttr) {
@@ -170,29 +137,47 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
170 var sseEventNames = getLegacySSESwaps(child);137 var sseEventNames = getLegacySSESwaps(child);
171 }138 }
172 139
173- for (var i = 0 ; i < sseEventNames.length ; i++) {140+ for (var i = 0; i < sseEventNames.length; i++) {
174 var sseEventName = sseEventNames[i].trim();141 var sseEventName = sseEventNames[i].trim();
175 var listener = function(event) {142 var listener = function(event) {
176 143
177- // If the parent is missing then close SSE and remove listener144+ // If the source is missing then close SSE
178- if (maybeCloseSSESource(elt)) {145+ if (maybeCloseSSESource(sourceElement)) {
146+ return;
147+ }
148+
149+ // If the body no longer contains the element, remove the listener
150+ if (!api.bodyContains(child)) {
179 source.removeEventListener(sseEventName, listener);151 source.removeEventListener(sseEventName, listener);
180 return;152 return;
181 }153 }
182 154
183 // swap the response into the DOM and trigger a notification155 // swap the response into the DOM and trigger a notification
156+ if(!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
157+ return;
158+ }
184 swap(child, event.data);159 swap(child, event.data);
185 api.triggerEvent(elt, "htmx:sseMessage", event);160 api.triggerEvent(elt, "htmx:sseMessage", event);
186 };161 };
187 162
188 // Register the new listener163 // Register the new listener
189- api.getInternalData(elt).sseEventListener = listener;164+ api.getInternalData(child).sseEventListener = listener;
190 source.addEventListener(sseEventName, listener);165 source.addEventListener(sseEventName, listener);
191 }166 }
192 });167 });
193 168
194 // Add message handlers for every `hx-trigger="sse:*"` attribute169 // Add message handlers for every `hx-trigger="sse:*"` attribute
195 queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {170 queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
171+ // Find closest existing event source
172+ var sourceElement = api.getClosestMatch(child, hasEventSource);
173+ if (sourceElement == null) {
174+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
175+ return null; // no eventsource in parentage, orphaned element
176+ }
177+
178+ // Set internalData and source
179+ var internalData = api.getInternalData(sourceElement);
180+ var source = internalData.sseEventSource;
196 181
197 var sseEventName = api.getAttributeValue(child, "hx-trigger");182 var sseEventName = api.getAttributeValue(child, "hx-trigger");
198 if (sseEventName == null) {183 if (sseEventName == null) {
@@ -204,30 +189,93 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
204 return;189 return;
205 }190 }
206 191
207- var listener = function(event) {192+ // remove the sse: prefix from here on out
193+ sseEventName = sseEventName.substr(4);
194+
195+ var listener = function() {
196+ if (maybeCloseSSESource(sourceElement)) {
197+ return
198+ }
208 199
209- // If parent is missing, then close SSE and remove listener200+ if (!api.bodyContains(child)) {
210- if (maybeCloseSSESource(elt)) {
211 source.removeEventListener(sseEventName, listener);201 source.removeEventListener(sseEventName, listener);
212- return;
213 }202 }
203+ }
204+ });
205+ }
206+
207+ /**
208+ * ensureEventSourceOnElement creates a new EventSource connection on the provided element.
209+ * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
210+ * is created and stored in the element's internalData.
211+ * @param {HTMLElement} elt
212+ * @param {number} retryCount
213+ * @returns {EventSource | null}
214+ */
215+ function ensureEventSourceOnElement(elt, retryCount) {
214 216
215- // Trigger events to be handled by the rest of htmx217+ if (elt == null) {
216- htmx.trigger(child, sseEventName, event);218+ return null;
217- htmx.trigger(child, "htmx:sseMessage", event);219+ }
220+
221+ // handle extension source creation attribute
222+ queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
223+ var sseURL = api.getAttributeValue(child, "sse-connect");
224+ if (sseURL == null) {
225+ return;
226+ }
227+
228+ ensureEventSource(child, sseURL, retryCount);
229+ });
230+
231+ // handle legacy sse, remove for HTMX2
232+ queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
233+ var sseURL = getLegacySSEURL(child);
234+ if (sseURL == null) {
235+ return;
218 }236 }
219 237
220- // Register the new listener238+ ensureEventSource(child, sseURL, retryCount);
221- api.getInternalData(elt).sseEventListener = listener;
222- source.addEventListener(sseEventName.slice(4), listener);
223 });239 });
240+
241+ registerSSE(elt);
242+ }
243+
244+ function ensureEventSource(elt, url, retryCount) {
245+ var source = htmx.createEventSource(url);
246+
247+ source.onerror = function(err) {
248+
249+ // Log an error event
250+ api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
251+
252+ // If parent no longer exists in the document, then clean up this EventSource
253+ if (maybeCloseSSESource(elt)) {
254+ return;
255+ }
256+
257+ // Otherwise, try to reconnect the EventSource
258+ if (source.readyState === EventSource.CLOSED) {
259+ retryCount = retryCount || 0;
260+ var timeout = Math.random() * (2 ^ retryCount) * 500;
261+ window.setTimeout(function() {
262+ ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
263+ }, timeout);
264+ }
265+ };
266+
267+ source.onopen = function(evt) {
268+ api.triggerEvent(elt, "htmx:sseOpen", { source: source });
269+ }
270+
271+ api.getInternalData(elt).sseEventSource = source;
224 }272 }
225 273
226 /**274 /**
227 * maybeCloseSSESource confirms that the parent element still exists.275 * maybeCloseSSESource confirms that the parent element still exists.
228 * If not, then any associated SSE source is closed and the function returns true.276 * If not, then any associated SSE source is closed and the function returns true.
229- * 277+ *
230- * @param {HTMLElement} elt 278+ * @param {HTMLElement} elt
231 * @returns boolean279 * @returns boolean
232 */280 */
233 function maybeCloseSSESource(elt) {281 function maybeCloseSSESource(elt) {
@@ -244,21 +292,21 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
244 292
245 /**293 /**
246 * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.294 * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
247- * 295+ *
248- * @param {HTMLElement} elt 296+ * @param {HTMLElement} elt
249- * @param {string} attributeName 297+ * @param {string} attributeName
250 */298 */
251 function queryAttributeOnThisOrChildren(elt, attributeName) {299 function queryAttributeOnThisOrChildren(elt, attributeName) {
252 300
253 var result = [];301 var result = [];
254 302
255 // If the parent element also contains the requested attribute, then add it to the results too.303 // If the parent element also contains the requested attribute, then add it to the results too.
256- if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {304+ if (api.hasAttribute(elt, attributeName)) {
257 result.push(elt);305 result.push(elt);
258 }306 }
259 307
260 // Search all child nodes that match the requested attribute308 // Search all child nodes that match the requested attribute
261- elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {309+ elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
262 result.push(node);310 result.push(node);
263 });311 });
264 312
@@ -267,7 +315,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
267 315
268 /**316 /**
269 * @param {HTMLElement} elt317 * @param {HTMLElement} elt
270- * @param {string} content 318+ * @param {string} content
271 */319 */
272 function swap(elt, content) {320 function swap(elt, content) {
273 321
@@ -281,7 +329,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
281 329
282 api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);330 api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283 331
284- settleInfo.elts.forEach(function (elt) {332+ settleInfo.elts.forEach(function(elt) {
285 if (elt.classList) {333 if (elt.classList) {
286 elt.classList.add(htmx.config.settlingClass);334 elt.classList.add(htmx.config.settlingClass);
287 }335 }
@@ -297,20 +345,20 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
297 }345 }
298 346
299 /**347 /**
300- * doSettle mirrors much of the functionality in htmx that 348+ * doSettle mirrors much of the functionality in htmx that
301 * settles elements after their content has been swapped.349 * settles elements after their content has been swapped.
302 * TODO: this should be published by htmx, and not duplicated here350 * TODO: this should be published by htmx, and not duplicated here
303- * @param {import("../htmx").HtmxSettleInfo} settleInfo 351+ * @param {import("../htmx").HtmxSettleInfo} settleInfo
304 * @returns () => void352 * @returns () => void
305 */353 */
306 function doSettle(settleInfo) {354 function doSettle(settleInfo) {
307 355
308 return function() {356 return function() {
309- settleInfo.tasks.forEach(function (task) {357+ settleInfo.tasks.forEach(function(task) {
310 task.call();358 task.call();
311 });359 });
312 360
313- settleInfo.elts.forEach(function (elt) {361+ settleInfo.elts.forEach(function(elt) {
314 if (elt.classList) {362 if (elt.classList) {
315 elt.classList.remove(htmx.config.settlingClass);363 elt.classList.remove(htmx.config.settlingClass);
316 }364 }
@@ -319,4 +367,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
319 }367 }
320 }368 }
321 369
322-})();⏎̸370+ function hasEventSource(node) {
371+ return api.getInternalData(node).sseEventSource != null;
372+ }
373+
374+})();
M internal/webui/public/libs/htmx.min.js +1 -1 generated file: collapsed
A internal/webui/public/libs/jquery.min.js +2 -0 generated file: collapsed
M package.json +4 -4
@@ -1,15 +1,15 @@
11 {
22 "devDependencies": {
3- "@catppuccin/tailwindcss": "^0.1.6",
43 "prettier": "^3.5.3",
54 "prettier-plugin-tailwindcss": "^0.5.14",
6- "tailwindcss": "^3.4.17"
5+ "tailwindcss": "^4.0.14"
76 },
87 "scripts": {
9- "build:css": "tailwindcss build src/styles.css -o dist/styles.css",
10- "dev:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css --watch"
8+ "dev:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css --watch",
9+ "build:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css"
1110 },
1211 "dependencies": {
12+ "caniuse-lite": "^1.0.30001705",
1313 "dotenv": "^16.4.7"
1414 }
1515 }
@@ -1,15 +1,15 @@
1 {1 {
2 "devDependencies": {2 "devDependencies": {
3- "@catppuccin/tailwindcss": "^0.1.6",
4 "prettier": "^3.5.3",3 "prettier": "^3.5.3",
5 "prettier-plugin-tailwindcss": "^0.5.14",4 "prettier-plugin-tailwindcss": "^0.5.14",
6- "tailwindcss": "^3.4.17"5+ "tailwindcss": "^4.0.14"
7 },6 },
8 "scripts": {7 "scripts": {
9- "build:css": "tailwindcss build src/styles.css -o dist/styles.css",8+ "dev:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css --watch",
10- "dev:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css --watch"9+ "build:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css"
11 },10 },
12 "dependencies": {11 "dependencies": {
12+ "caniuse-lite": "^1.0.30001705",
13 "dotenv": "^16.4.7"13 "dotenv": "^16.4.7"
14 }14 }
15 }15 }
M tailwind.config.js +0 -1
@@ -17,5 +17,4 @@ module.exports = {
1717 },
1818 },
1919 },
20- plugins: [require("@catppuccin/tailwindcss")],
2120 };
@@ -17,5 +17,4 @@ module.exports = {
17 },17 },
18 },18 },
19 },19 },
20- plugins: [require("@catppuccin/tailwindcss")],
21 };20 };

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close