diff --git a/homelab/docker-compose.yaml b/homelab/docker-compose.yaml
index d6be65a..d00907a 100644
--- a/homelab/docker-compose.yaml
+++ b/homelab/docker-compose.yaml
@@ -17,13 +17,15 @@ services:
- type: bind
source: /srv/sys/caddy
target: /data/caddy
- - type: bind
- source: /srv/storage/www
- target: /www
- read_only: true
configs:
- source: Caddyfile
target: /etc/caddy/Caddyfile
+ - source: index.html
+ target: /www/index.html
+ - source: index.mjs
+ target: /www/index.mjs
+ - source: index.css
+ target: /www/index.css
extra_hosts:
- host.docker.internal:host-gateway
@@ -223,7 +225,17 @@ configs:
name: dave.yaml-v3
Caddyfile:
file: Caddyfile
- name: Caddyfile-v63
+ name: Caddyfile-v65
+ index.html:
+ file: www/index.html
+ name: index.html-v32
+ index.mjs:
+ file: www/index.mjs
+ name: index.mjs-v1
+ index.css:
+ file: www/index.css
+ name: index.css-v1
+
secrets:
diff --git a/homelab/www/index.css b/homelab/www/index.css
new file mode 100644
index 0000000..93ca0b4
--- /dev/null
+++ b/homelab/www/index.css
@@ -0,0 +1,78 @@
+body {
+ display: flex;
+ flex-flow: column;
+ height: 100vh;
+ margin: 0;
+ font-family: Helvetica, sans-serif;
+ background-color: #222;
+ color: white;
+}
+
+.hidden {
+ display: none;
+}
+
+nav {
+ overflow-x: auto;
+ font-weight: bold;
+ font-size: 12pt;
+ display: flex;
+}
+nav img {
+ max-height: 2em;
+}
+nav a {
+ padding: 0.5em 1.5em;
+ color: #aaa;
+ text-decoration: none;
+ white-space: nowrap;
+}
+nav a[target] {
+ display: none;
+}
+nav a:hover {
+ background: #555;
+}
+nav a.active {
+ background: #8b8;
+ color: black;
+}
+
+#app {
+ flex-grow: 1;
+}
+iframe {
+ display: block;
+ border: none;
+ width: 100%;
+ height: 100%;
+}
+
+.icons {
+ margin: auto 2em;
+}
+.icons {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ max-width: 40em;
+ margin: auto;
+ gap: 20px;
+}
+.icons a {
+ background-color: #444;
+ color: #fff;
+ width: 120px;
+ height: 120px;
+ border-radius: 10px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.icons a img {
+ max-width: 66%;
+ max-height: 66%;
+ width: 100%;
+ height: 100%;
+ image-rendering: pixelated;
+}
diff --git a/homelab/www/index.html b/homelab/www/index.html
new file mode 100644
index 0000000..5df8d27
--- /dev/null
+++ b/homelab/www/index.html
@@ -0,0 +1,35 @@
+
+
+
+ Deer Grove
+
+
+
+
+
+
+
+
+
+
+
diff --git a/homelab/www/index.mjs b/homelab/www/index.mjs
new file mode 100644
index 0000000..774d219
--- /dev/null
+++ b/homelab/www/index.mjs
@@ -0,0 +1,94 @@
+let frames = {}
+function activate(event, element) {
+ if (element.target) {
+ return
+ }
+ event.preventDefault()
+
+ let parent = element.parentElement
+ for (let e of parent.getElementsByClassName("active")) {
+ e.classList.remove("active")
+ }
+ element.classList.add("active")
+ let href = element.href
+ let app = document.querySelector("#app")
+
+ let frame = frames[href]
+ if (frame) {
+ frame.dispatchEvent(new Event("load"))
+ } else {
+ frame = app.appendChild(document.createElement("iframe"))
+ frame.addEventListener("load", e => frameLoaded(frame))
+ frame.src = href
+ frames[href] = frame
+ }
+
+ for (let fhref in frames) {
+ let f = frames[fhref]
+ if (fhref == href) {
+ f.classList.remove("hidden")
+ } else {
+ f.classList.add("hidden")
+ }
+ }
+}
+
+
+function frameLoaded(frame) {
+ let doc = frame.contentDocument
+ if (doc.title.length > 0) {
+ document.title = doc.title
+ }
+ let icon = document.querySelector("link[rel~='icon']")
+ let dicon = doc.querySelector("link[rel~='icon']")
+ if (dicon) {
+ icon.href = dicon.href
+ } else {
+ icon.href = defaultIcon
+ }
+}
+
+let defaultIcon = null
+function init() {
+ let doc = document.querySelector("iframe").contentDocument
+
+ defaultIcon = document.querySelector("link[rel~='icon']").href
+
+ for (let l of document.head.querySelectorAll("style")) {
+ doc.head.appendChild(l.cloneNode(true))
+ }
+ for (let l of document.head.querySelectorAll("link[rel='stylesheet']")) {
+ doc.head.appendChild(l.cloneNode())
+ }
+ for (let f of document.querySelectorAll("#app iframe")) {
+ frames[f.src] = f
+ }
+
+
+ let icons = doc.body.appendChild(doc.createElement("section"))
+ icons.classList.add("icons")
+
+ for (let link of document.querySelectorAll("nav a")) {
+ let dlink = icons.appendChild(link.cloneNode(true))
+ dlink.textContent = ""
+
+ if (link.dataset.icon) {
+ let icon = dlink.appendChild(doc.createElement("img"))
+ icon.src = link.dataset.icon
+ icon.style.objectFit = "cover"
+ } else {
+ let text = dlink.appendChild(doc.createElement("div"))
+ text.textContent = link.textContent
+ }
+
+ // Make both of them update the selected tab
+ link.addEventListener("click", event => activate(event, link))
+ dlink.addEventListener("click", event => activate(event, link))
+ }
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init)
+} else {
+ init()
+}