From c1863b3e00ec233fa6a39580d7ce8a50472e4d7a Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 22 May 2026 00:45:01 +0100 Subject: [PATCH 1/5] feat(site): data-driven Eleventy marketing site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A nebula-themed marketing/get site under site/, matching the dashboard (aurora background, glass cards, system fonts — no Google/third-party calls). The app grid + category filters are generated from the repo: scripts/gen-data.mjs reads each containers//.config and emits the data Eleventy renders. `npm run build` -> static site in dist/. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- site/.gitignore | 6 + site/eleventy.config.cjs | 12 + site/package-lock.json | 1522 ++++++++++++++++++++++++++ site/package.json | 15 + site/scripts/gen-data.mjs | 78 ++ site/src/_data/site.json | 12 + site/src/_includes/app-card.njk | 10 + site/src/_includes/footer.njk | 26 + site/src/_includes/layout.njk | 24 + site/src/_includes/topbar.njk | 31 + site/src/assets/apps/adguard.svg | 1 + site/src/assets/apps/authelia.svg | 1 + site/src/assets/apps/bookstack.svg | 1 + site/src/assets/apps/crowdsec.svg | 32 + site/src/assets/apps/dashy.svg | 161 +++ site/src/assets/apps/default.svg | 1 + site/src/assets/apps/focalboard.svg | 1 + site/src/assets/apps/gitea.svg | 4 + site/src/assets/apps/gluetun.svg | 13 + site/src/assets/apps/grafana.svg | 70 ++ site/src/assets/apps/headscale.svg | 1 + site/src/assets/apps/invidious.svg | 2 + site/src/assets/apps/ipinfo.svg | 1 + site/src/assets/apps/jellyfin.svg | 1 + site/src/assets/apps/jitsimeet.svg | 650 +++++++++++ site/src/assets/apps/libreportal.svg | 605 ++++++++++ site/src/assets/apps/linkding.svg | 1 + site/src/assets/apps/mastodon.svg | 1 + site/src/assets/apps/nextcloud.svg | 1 + site/src/assets/apps/ollama.svg | 1 + site/src/assets/apps/onlyoffice.svg | 1 + site/src/assets/apps/owncloud.svg | 1 + site/src/assets/apps/pihole.svg | 1 + site/src/assets/apps/portainer.svg | 1 + site/src/assets/apps/prometheus.svg | 1 + site/src/assets/apps/searxng.svg | 1 + site/src/assets/apps/speedtest.svg | 1 + site/src/assets/apps/traefik.svg | 1 + site/src/assets/apps/trilium.svg | 1 + site/src/assets/apps/unbound.svg | 1 + site/src/assets/apps/vaultwarden.svg | 1 + site/src/assets/apps/wireguard.svg | 1 + site/src/assets/favicon.ico | Bin 0 -> 87483 bytes site/src/assets/libreportal.svg | 605 ++++++++++ site/src/assets/main.js | 66 ++ site/src/assets/style.css | 276 +++++ site/src/index.njk | 162 +++ 47 files changed, 4406 insertions(+) create mode 100644 site/.gitignore create mode 100644 site/eleventy.config.cjs create mode 100644 site/package-lock.json create mode 100644 site/package.json create mode 100644 site/scripts/gen-data.mjs create mode 100644 site/src/_data/site.json create mode 100644 site/src/_includes/app-card.njk create mode 100644 site/src/_includes/footer.njk create mode 100644 site/src/_includes/layout.njk create mode 100644 site/src/_includes/topbar.njk create mode 100644 site/src/assets/apps/adguard.svg create mode 100644 site/src/assets/apps/authelia.svg create mode 100644 site/src/assets/apps/bookstack.svg create mode 100644 site/src/assets/apps/crowdsec.svg create mode 100644 site/src/assets/apps/dashy.svg create mode 100644 site/src/assets/apps/default.svg create mode 100644 site/src/assets/apps/focalboard.svg create mode 100644 site/src/assets/apps/gitea.svg create mode 100644 site/src/assets/apps/gluetun.svg create mode 100644 site/src/assets/apps/grafana.svg create mode 100644 site/src/assets/apps/headscale.svg create mode 100644 site/src/assets/apps/invidious.svg create mode 100644 site/src/assets/apps/ipinfo.svg create mode 100644 site/src/assets/apps/jellyfin.svg create mode 100644 site/src/assets/apps/jitsimeet.svg create mode 100644 site/src/assets/apps/libreportal.svg create mode 100644 site/src/assets/apps/linkding.svg create mode 100644 site/src/assets/apps/mastodon.svg create mode 100644 site/src/assets/apps/nextcloud.svg create mode 100644 site/src/assets/apps/ollama.svg create mode 100644 site/src/assets/apps/onlyoffice.svg create mode 100644 site/src/assets/apps/owncloud.svg create mode 100644 site/src/assets/apps/pihole.svg create mode 100644 site/src/assets/apps/portainer.svg create mode 100644 site/src/assets/apps/prometheus.svg create mode 100644 site/src/assets/apps/searxng.svg create mode 100644 site/src/assets/apps/speedtest.svg create mode 100644 site/src/assets/apps/traefik.svg create mode 100644 site/src/assets/apps/trilium.svg create mode 100644 site/src/assets/apps/unbound.svg create mode 100644 site/src/assets/apps/vaultwarden.svg create mode 100644 site/src/assets/apps/wireguard.svg create mode 100644 site/src/assets/favicon.ico create mode 100644 site/src/assets/libreportal.svg create mode 100644 site/src/assets/main.js create mode 100644 site/src/assets/style.css create mode 100644 site/src/index.njk diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..9045c73 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +# generated from the repo at build time (npm run data) +src/_data/apps.json +src/_data/categories.json +*.bak diff --git a/site/eleventy.config.cjs b/site/eleventy.config.cjs new file mode 100644 index 0000000..eb0ebf1 --- /dev/null +++ b/site/eleventy.config.cjs @@ -0,0 +1,12 @@ +module.exports = function (eleventyConfig) { + // copy static assets straight through to the build output + eleventyConfig.addPassthroughCopy({ "src/assets": "assets" }); + eleventyConfig.addWatchTarget("src/assets"); + + return { + dir: { input: "src", includes: "_includes", data: "_data", output: "dist" }, + htmlTemplateEngine: "njk", + markdownTemplateEngine: "njk", + templateFormats: ["njk", "html", "md"], + }; +}; diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..54c11ee --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,1522 @@ +{ + "name": "libreportal-site", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "libreportal-site", + "version": "0.1.0", + "devDependencies": { + "@11ty/eleventy": "^3.1.5" + } + }, + "node_modules/@11ty/dependency-tree": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.2.tgz", + "integrity": "sha512-RTF6VTZHatYf7fSZBUN3RKwiUeJh5dhWV61gDPrHhQF2/gzruAkYz8yXuvGLx3w3ZBKreGrR+MfYpSVkdbdbLA==", + "dev": true, + "dependencies": { + "@11ty/eleventy-utils": "^2.0.1" + } + }, + "node_modules/@11ty/dependency-tree-esm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.4.tgz", + "integrity": "sha512-MYKC0Ac77ILr1HnRJalzKDlb9Z8To3kXQCltx299pUXXUFtJ1RIONtULlknknqW8cLe19DLVgmxVCtjEFm7h0A==", + "dev": true, + "dependencies": { + "@11ty/eleventy-utils": "^2.0.7", + "acorn": "^8.15.0", + "dependency-graph": "^1.0.0", + "normalize-path": "^3.0.0" + } + }, + "node_modules/@11ty/eleventy": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.5.tgz", + "integrity": "sha512-hZ0g6MwZyRxCqXsPm82gIM304LraKbUz3ZmezOSjsqxttZG6cHTib3Qq8QkESJoKwnr+yX1eyfOkPC5/mEgZnQ==", + "dev": true, + "dependencies": { + "@11ty/dependency-tree": "^4.0.2", + "@11ty/dependency-tree-esm": "^2.0.4", + "@11ty/eleventy-dev-server": "^2.0.8", + "@11ty/eleventy-plugin-bundle": "^3.0.7", + "@11ty/eleventy-utils": "^2.0.7", + "@11ty/lodash-custom": "^4.17.21", + "@11ty/posthtml-urls": "^1.0.2", + "@11ty/recursive-copy": "^4.0.4", + "@sindresorhus/slugify": "^2.2.1", + "bcp-47-normalize": "^2.3.0", + "chokidar": "^3.6.0", + "debug": "^4.4.3", + "dependency-graph": "^1.0.0", + "entities": "^6.0.1", + "filesize": "^10.1.6", + "gray-matter": "^4.0.3", + "iso-639-1": "^3.1.5", + "js-yaml": "^4.1.1", + "kleur": "^4.1.5", + "liquidjs": "^10.25.0", + "luxon": "^3.7.2", + "markdown-it": "^14.1.1", + "minimist": "^1.2.8", + "moo": "0.5.2", + "node-retrieve-globals": "^6.0.1", + "nunjucks": "^3.2.4", + "picomatch": "^4.0.3", + "please-upgrade-node": "^3.2.0", + "posthtml": "^0.16.7", + "posthtml-match-helper": "^2.0.3", + "semver": "^7.7.4", + "slugify": "^1.6.8", + "tinyglobby": "^0.2.15" + }, + "bin": { + "eleventy": "cmd.cjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-dev-server": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-2.0.8.tgz", + "integrity": "sha512-15oC5M1DQlCaOMUq4limKRYmWiGecDaGwryr7fTE/oM9Ix8siqMvWi+I8VjsfrGr+iViDvWcH/TVI6D12d93mA==", + "dev": true, + "dependencies": { + "@11ty/eleventy-utils": "^2.0.1", + "chokidar": "^3.6.0", + "debug": "^4.4.0", + "finalhandler": "^1.3.1", + "mime": "^3.0.0", + "minimist": "^1.2.8", + "morphdom": "^2.7.4", + "please-upgrade-node": "^3.2.0", + "send": "^1.1.0", + "ssri": "^11.0.0", + "urlpattern-polyfill": "^10.0.0", + "ws": "^8.18.1" + }, + "bin": { + "eleventy-dev-server": "cmd.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-plugin-bundle": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", + "integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==", + "dev": true, + "dependencies": { + "@11ty/eleventy-utils": "^2.0.2", + "debug": "^4.4.0", + "posthtml-match-helper": "^2.0.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-utils": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz", + "integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/lodash-custom": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz", + "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/posthtml-urls": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.3.tgz", + "integrity": "sha512-1YvhnkaNlFnnJic1rBMWmTC2adbuy+JQiBfl1Hecr1Wjjik1pQZmGyk/eC9zKX/FQv52s2Nht1Gi/UwhYqrBeg==", + "dev": true, + "dependencies": { + "evaluate-value": "^2.0.0", + "http-equiv-refresh": "^2.0.1", + "list-to-array": "^1.1.0", + "parse-srcset": "^1.0.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@11ty/recursive-copy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.4.tgz", + "integrity": "sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==", + "dev": true, + "dependencies": { + "errno": "^1.0.0", + "junk": "^3.1.0", + "minimatch": "^3.1.5", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "dev": true, + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "dev": true, + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", + "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", + "dev": true, + "dependencies": { + "bcp-47": "^2.0.0", + "bcp-47-match": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", + "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm-import-transformer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", + "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/evaluate-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz", + "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filesize": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", + "dev": true, + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-equiv-refresh": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-2.0.1.tgz", + "integrity": "sha512-XJpDL/MLkV3dKwLzHwr2dY05dYNfBNlyPu4STQ8WvKCFdc6vC5tPXuq28of663+gHVg03C+16pHHs/+FmmDjcw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/iso-639-1": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", + "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/liquidjs": { + "version": "10.27.0", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.27.0.tgz", + "integrity": "sha512-tw/OA59K7aIBlMKIrKlumr37fiZUheShVHXY8cVctWisgY1p9mc5hreOvlreoS0wTiwlWk14Ya7305c2a/Cg5w==", + "dev": true, + "dependencies": { + "commander": "^10.0.0" + }, + "bin": { + "liquid": "bin/liquid.js", + "liquidjs": "bin/liquid.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/liquidjs" + } + }, + "node_modules/list-to-array": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz", + "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==", + "dev": true + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, + "node_modules/morphdom": { + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz", + "integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/node-retrieve-globals": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", + "integrity": "sha512-j0DeFuZ/Wg3VlklfbxUgZF/mdHMTEiEipBb3q0SpMMbHaV3AVfoUQF8UGxh1s/yjqO0TgRZd4Pi/x2yRqoQ4Eg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.1", + "acorn-walk": "^8.3.4", + "esm-import-transformer": "^3.0.3" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "dev": true, + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/posthtml": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", + "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", + "dev": true, + "dependencies": { + "posthtml-parser": "^0.11.0", + "posthtml-render": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/posthtml-match-helper": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz", + "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "posthtml": "^0.16.6" + } + }, + "node_modules/posthtml-parser": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", + "dev": true, + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml-render": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", + "dev": true, + "dependencies": { + "is-json": "^2.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz", + "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..803889c --- /dev/null +++ b/site/package.json @@ -0,0 +1,15 @@ +{ + "name": "libreportal-site", + "version": "0.1.0", + "private": true, + "description": "LibrePortal marketing/get site — data-driven from the repo via Eleventy.", + "scripts": { + "data": "node scripts/gen-data.mjs", + "build": "npm run data && eleventy", + "start": "npm run data && eleventy --serve --port 8777", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@11ty/eleventy": "^3.1.5" + } +} diff --git a/site/scripts/gen-data.mjs b/site/scripts/gen-data.mjs new file mode 100644 index 0000000..506d84e --- /dev/null +++ b/site/scripts/gen-data.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/* + * gen-data.mjs — single source of truth: the LibrePortal repo. + * + * Reads every containers//.config METADATA block and emits + * src/_data/apps.json (array of apps, used by the app grid) + * src/_data/categories.json (categories present, with friendly names + counts) + * + * Run via `npm run data` (also runs automatically before `npm run build`). + * Add an app to the repo -> rebuild -> the website updates itself. + */ +import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, statSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, '..', '..'); // site/scripts -> repo root +const containersDir = join(repoRoot, 'containers'); +const dataDir = join(here, '..', 'src', '_data'); +const iconsDir = join(here, '..', 'src', 'assets', 'apps'); + +// Only these metadata fields are pulled from each .config. LONG_DESCRIPTION must +// precede DESCRIPTION in the alternation so it wins the match. +const FIELD_RE = /^CFG_.+?_(LONG_DESCRIPTION|DESCRIPTION|CATEGORY|TITLE|URL)\s*=\s*"?(.*?)"?\s*$/; + +const CATEGORY_NAMES = { + recommended: 'Recommended', communication: 'Communication', development: 'Development', + knowledge: 'Knowledge', media: 'Media', monitoring: 'Monitoring', storage: 'Storage', + network: 'Network', security: 'Security', productivity: 'Productivity', finance: 'Finance', + ai: 'AI', utilities: 'Utilities', privacy: 'Privacy', social: 'Social', system: 'System', +}; +const titleCase = (s) => s.replace(/[-_]/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()); + +function parseConfig(text) { + const out = {}; + for (const raw of text.split(/\r?\n/)) { + const m = FIELD_RE.exec(raw.trim()); + if (m && out[m[1]] === undefined) out[m[1]] = m[2]; + } + return out; +} + +const apps = []; +for (const slug of readdirSync(containersDir)) { + const dir = join(containersDir, slug); + if (!statSync(dir).isDirectory()) continue; + const cfg = join(dir, `${slug}.config`); + if (!existsSync(cfg)) continue; + const meta = parseConfig(readFileSync(cfg, 'utf8')); + if (!meta.TITLE) continue; // skip anything without a display title (infra without metadata) + const cats = (meta.CATEGORY || 'utilities').split(',').map((s) => s.trim()).filter(Boolean); + apps.push({ + slug, + title: meta.TITLE, + category: cats[0], // primary category + categories: cats, // an app can belong to several + description: meta.DESCRIPTION || '', + longDescription: meta.LONG_DESCRIPTION || meta.DESCRIPTION || '', + url: meta.URL || '', + icon: existsSync(join(iconsDir, `${slug}.svg`)) ? `assets/apps/${slug}.svg` : 'assets/apps/default.svg', + }); +} +apps.sort((a, b) => a.title.localeCompare(b.title)); + +const catMap = new Map(); +for (const a of apps) { + for (const cat of a.categories) { + const c = catMap.get(cat) || { id: cat, name: CATEGORY_NAMES[cat] || titleCase(cat), count: 0 }; + c.count++; + catMap.set(cat, c); + } +} +const categories = [...catMap.values()].sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); + +mkdirSync(dataDir, { recursive: true }); +writeFileSync(join(dataDir, 'apps.json'), JSON.stringify(apps, null, 2) + '\n'); +writeFileSync(join(dataDir, 'categories.json'), JSON.stringify(categories, null, 2) + '\n'); +console.log(`✓ ${apps.length} apps across ${categories.length} categories → src/_data/{apps,categories}.json`); diff --git a/site/src/_data/site.json b/site/src/_data/site.json new file mode 100644 index 0000000..315890e --- /dev/null +++ b/site/src/_data/site.json @@ -0,0 +1,12 @@ +{ + "name": "LibrePortal", + "tagline": "your own private corner of the universe", + "version": "v0.1.0", + "installUrl": "https://get.libreportal.org", + "installCmd": "bash <(curl -fsSL https://get.libreportal.org) init", + "repo": "https://gitea.scottwebstar.co.uk/Webstar/LibrePortal", + "promiseUrl": "https://gitea.scottwebstar.co.uk/Webstar/LibrePortal/raw/branch/main/PROMISE.md", + "contributingUrl": "https://gitea.scottwebstar.co.uk/Webstar/LibrePortal/raw/branch/main/CONTRIBUTING.md", + "initUrl": "https://gitea.scottwebstar.co.uk/Webstar/LibrePortal/raw/branch/main/init.sh", + "inspiration": "https://gitlab.com/bmcgonag/docker_installs" +} diff --git a/site/src/_includes/app-card.njk b/site/src/_includes/app-card.njk new file mode 100644 index 0000000..e2b5a4e --- /dev/null +++ b/site/src/_includes/app-card.njk @@ -0,0 +1,10 @@ +
+
+ +
+

{{ app.title }}

+ {{ app.category }} +
+
+

{{ app.description }}

+
diff --git a/site/src/_includes/footer.njk b/site/src/_includes/footer.njk new file mode 100644 index 0000000..56c0765 --- /dev/null +++ b/site/src/_includes/footer.njk @@ -0,0 +1,26 @@ + diff --git a/site/src/_includes/layout.njk b/site/src/_includes/layout.njk new file mode 100644 index 0000000..2070463 --- /dev/null +++ b/site/src/_includes/layout.njk @@ -0,0 +1,24 @@ + + + + + +{{ site.name }} — {{ site.tagline }} + + + + + + +
+
+ +{% include "topbar.njk" %} + +{{ content | safe }} + +{% include "footer.njk" %} + + + + diff --git a/site/src/_includes/topbar.njk b/site/src/_includes/topbar.njk new file mode 100644 index 0000000..2927da7 --- /dev/null +++ b/site/src/_includes/topbar.njk @@ -0,0 +1,31 @@ + diff --git a/site/src/assets/apps/adguard.svg b/site/src/assets/apps/adguard.svg new file mode 100644 index 0000000..f6118fc --- /dev/null +++ b/site/src/assets/apps/adguard.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/authelia.svg b/site/src/assets/apps/authelia.svg new file mode 100644 index 0000000..9880b3b --- /dev/null +++ b/site/src/assets/apps/authelia.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/bookstack.svg b/site/src/assets/apps/bookstack.svg new file mode 100644 index 0000000..a6ad581 --- /dev/null +++ b/site/src/assets/apps/bookstack.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/crowdsec.svg b/site/src/assets/apps/crowdsec.svg new file mode 100644 index 0000000..fd4ffac --- /dev/null +++ b/site/src/assets/apps/crowdsec.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/apps/dashy.svg b/site/src/assets/apps/dashy.svg new file mode 100644 index 0000000..ce68744 --- /dev/null +++ b/site/src/assets/apps/dashy.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/apps/default.svg b/site/src/assets/apps/default.svg new file mode 100644 index 0000000..343d2de --- /dev/null +++ b/site/src/assets/apps/default.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/focalboard.svg b/site/src/assets/apps/focalboard.svg new file mode 100644 index 0000000..b78e7e3 --- /dev/null +++ b/site/src/assets/apps/focalboard.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/gitea.svg b/site/src/assets/apps/gitea.svg new file mode 100644 index 0000000..11c6df8 --- /dev/null +++ b/site/src/assets/apps/gitea.svg @@ -0,0 +1,4 @@ + + + + diff --git a/site/src/assets/apps/gluetun.svg b/site/src/assets/apps/gluetun.svg new file mode 100644 index 0000000..a39521c --- /dev/null +++ b/site/src/assets/apps/gluetun.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/site/src/assets/apps/grafana.svg b/site/src/assets/apps/grafana.svg new file mode 100644 index 0000000..54be1e2 --- /dev/null +++ b/site/src/assets/apps/grafana.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/apps/headscale.svg b/site/src/assets/apps/headscale.svg new file mode 100644 index 0000000..06f406a --- /dev/null +++ b/site/src/assets/apps/headscale.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/invidious.svg b/site/src/assets/apps/invidious.svg new file mode 100644 index 0000000..80e78a4 --- /dev/null +++ b/site/src/assets/apps/invidious.svg @@ -0,0 +1,2 @@ + + diff --git a/site/src/assets/apps/ipinfo.svg b/site/src/assets/apps/ipinfo.svg new file mode 100644 index 0000000..656169c --- /dev/null +++ b/site/src/assets/apps/ipinfo.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/jellyfin.svg b/site/src/assets/apps/jellyfin.svg new file mode 100644 index 0000000..0e56a50 --- /dev/null +++ b/site/src/assets/apps/jellyfin.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/jitsimeet.svg b/site/src/assets/apps/jitsimeet.svg new file mode 100644 index 0000000..5a3526a --- /dev/null +++ b/site/src/assets/apps/jitsimeet.svg @@ -0,0 +1,650 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/apps/libreportal.svg b/site/src/assets/apps/libreportal.svg new file mode 100644 index 0000000..a476796 --- /dev/null +++ b/site/src/assets/apps/libreportal.svg @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/src/assets/apps/linkding.svg b/site/src/assets/apps/linkding.svg new file mode 100644 index 0000000..089630d --- /dev/null +++ b/site/src/assets/apps/linkding.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/mastodon.svg b/site/src/assets/apps/mastodon.svg new file mode 100644 index 0000000..dd5075e --- /dev/null +++ b/site/src/assets/apps/mastodon.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/nextcloud.svg b/site/src/assets/apps/nextcloud.svg new file mode 100644 index 0000000..336aff5 --- /dev/null +++ b/site/src/assets/apps/nextcloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/src/assets/apps/ollama.svg b/site/src/assets/apps/ollama.svg new file mode 100644 index 0000000..6bba73a --- /dev/null +++ b/site/src/assets/apps/ollama.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/onlyoffice.svg b/site/src/assets/apps/onlyoffice.svg new file mode 100644 index 0000000..364522c --- /dev/null +++ b/site/src/assets/apps/onlyoffice.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/owncloud.svg b/site/src/assets/apps/owncloud.svg new file mode 100644 index 0000000..cf650c7 --- /dev/null +++ b/site/src/assets/apps/owncloud.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/pihole.svg b/site/src/assets/apps/pihole.svg new file mode 100644 index 0000000..5bda461 --- /dev/null +++ b/site/src/assets/apps/pihole.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/portainer.svg b/site/src/assets/apps/portainer.svg new file mode 100644 index 0000000..45cf83a --- /dev/null +++ b/site/src/assets/apps/portainer.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/prometheus.svg b/site/src/assets/apps/prometheus.svg new file mode 100644 index 0000000..309d704 --- /dev/null +++ b/site/src/assets/apps/prometheus.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/searxng.svg b/site/src/assets/apps/searxng.svg new file mode 100644 index 0000000..2ddf53b --- /dev/null +++ b/site/src/assets/apps/searxng.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/speedtest.svg b/site/src/assets/apps/speedtest.svg new file mode 100644 index 0000000..2fd0d2b --- /dev/null +++ b/site/src/assets/apps/speedtest.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/traefik.svg b/site/src/assets/apps/traefik.svg new file mode 100644 index 0000000..a86b9b7 --- /dev/null +++ b/site/src/assets/apps/traefik.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/trilium.svg b/site/src/assets/apps/trilium.svg new file mode 100644 index 0000000..2ecb6e4 --- /dev/null +++ b/site/src/assets/apps/trilium.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/unbound.svg b/site/src/assets/apps/unbound.svg new file mode 100644 index 0000000..cfc5d8d --- /dev/null +++ b/site/src/assets/apps/unbound.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/vaultwarden.svg b/site/src/assets/apps/vaultwarden.svg new file mode 100644 index 0000000..41ca105 --- /dev/null +++ b/site/src/assets/apps/vaultwarden.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/apps/wireguard.svg b/site/src/assets/apps/wireguard.svg new file mode 100644 index 0000000..b778001 --- /dev/null +++ b/site/src/assets/apps/wireguard.svg @@ -0,0 +1 @@ + diff --git a/site/src/assets/favicon.ico b/site/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..622f2d3b8283a74729862a1279d05e8db8c2c8e4 GIT binary patch literal 87483 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

3w+mQ{uOKS$Hp1i z_9XxL*>7OkCNIDFWvnR8Wb>~5bTm!ST$d#Xj|x^;ra-BPL^4e_)5x~2A*!qUS=drf zV_K57Wx17@j*S6F4Fbe_?TZ(-viU$S8+IOM)vPA6XVLsa2!R(y%xS1$)!f-^+Owa> z4>ocByw+n3Nu?Oc5Mq)Ftci2+j%{-A@Bp9v^mqB$&+cb@B<^UdwMP2^^Hwh7qHEXE zv|ui(ww54Uguj{rQWjZ?58xUVkCG_Vn@F-|pt#NBVg1 z{$G>ZxrO#O{3+?VizyWfwAH4#c2N@xn=$~nUVxR(@Z$|nu=%Us#ImhO8c9>6`M-ys z{LXLVsQ>yu_`u&(JYNjJ$x!wRZjCzHU-fm|=$2A(B6#r!|H4{(#Rb&q5IdP*xa9Lh z|1kL=L|0d%GX>0iiAAq?8QTj5atAv3>LBMoQ zcv3ahHL~^aC_4}LW7%msN1!@sP@giXP8g(Zi7DfNn$~fED$ZjD0mThFQVitN?wRz zS(J)neB|Aq=7~)^NoO1|4YY>V*&VEX<+ZGN#Y>nlEqZfBims2!6^M|Ol*F?VWVAvn z6KR@gKVa|e_mM9YXiOR`ZM1pdwmUI7%I=d=Fq$jjdl3mn*!9>w%%1gHX0*4`*i_Bp zmGcNgHQ5o@vMsEaUB#a5d%0kd=1nhcCT)#j>O+9QutU%hDufp;mdyUpZ0y|_oP^DV=g;H9ix%>p;y9oB{C#}-YrC+1@}D%_@Eo z!?;;%Hk0J8ClB+BfBZbA?}pMzn_2`v?Arg+zdtI<`NG2U)c{l`ziMlH)z@WY-;y66 z3|8H|&bsDRFQGw43w(AEexR3g7q;UE(WD<8D*!2KGD#Y0vh3=aps8+@_QnPV3n4>=5L3oo zf-{aJV@pz2>>b#KKnl$ji(A=txS#uXb+K|*6CHJ#6Z8g8chy=!W7_8YMT_|LV~_Eh zt-H8x^-A(yNZySocoA-(2$haWO_3s*awv`!`1Id=j$PaKQBxHgj1?EG=A}2knvUg* z8I=~>21Y1)vGQ#sOg!P>V<1xrR9_E;!9fPcCMb?iFw)h{!G|BgA05Wb_wf@vBES(g zhKQXt!;nnmTqg1^*I(7f{_X*8``*{+Sa2RQ7c2%#6ZipcvB1RmIQ@OY4D=2%Fj~aW z8le?KV=yD@0RnB9NGFY*NMWSXEIFrv<*%Lrr4&Ia!Ykx}SW3M(Fv4iDgvLM!&AV=B zp>N3J>p$E})23hZn%DjC85My=5oA+H!z5 zUb5_ri-anWk)orqhWRt+vg_b}?%Z~W_WDNLASk=vBIe#GMYbwUU(X=_@{upHd&_PN zLvqOtS8>@Zuco@OfpNoPYwr*fUWhVmd@Dtu<>V$;O2NMGew*(5Hc~1Sv4|iFkzqha zDpsG@z?GL>f|In_v#*<<|KdRkrGO}mnULcX9(mVi?dlfZ^S0SM@W>&K^dDq=-~e5{ zf=ET>fUBZ9R5WBItFEwFeSQnGSJg2wk|JeSW7sB!lZ@*Jv<4O67Yh`&JdO@LbQG5D z^wH)UmJ(sRWm%sih&b{s$ z^5X^0ThvB-b1i-l9c7Ygjcpoi?d;?614o$MT*sx$W*^h;OH{s0i3cd+D7XLG?%dCz{t+%%IE#7Bb@)*fm&_BS=vc8w0F-I4`+xF3?EL>(yzd>?@`{(O zASgFhjf;iK%`m0FFeOgHAY12{Kh?xNVTX`ocCo{7)a|Wp7-ER*I3aJh#60A35;YHl505wbr=W z39jimeA~vd>;Hn<_fN(1%L7=q&e*X2)^O&6*ZqagM<0j+HxP-0b@lt+$-(4e`t*u9js6U4>1jk&aEitBXT-+<~%^yku6Mzr41ICl6WN zx8*PgdUK5R?BQ?T`EeGXGoSM=IEQmDSjmmAyO#g_+K)KUTL1xul(e_EGG}f*W*5x4aHnD&Uv~JBkq#NKVZt z=cpG@C^3GhH}2S5W{w^C#chl}zJ)+(S{BUY{2Q*OdHx)h&a9`Sp{h&+KDyczJ)e8F z?xWy_T(M#;BZua-gS|?FTz)YpCbEh4asPE1=6>fa`law0$r6QoxIs z&tqnLGe7>;ulU@pUquB0=fCcH{^Y}Nr=`YX&yg{n>>no=2};5t;>4S*mBPwqIJka2 z+kW^nX4MG3dP9kM)gx$O5ov?Iv5>vJMYbF)vb}SR{_&8ix+-pX?KQmkvp>G8J3(gmq#`{N~EiB z5{XC}j$ziO{^H<{Z+?-rYt2~K?}d%$i3gwse2n&s*43%*=-XO4ww5S6l@?20v<4OU z1fgO)1XoCmGw`Fe(wGSc!?Fnd03C)PCGD?zC3amcefQqS;T`+w@9bgjc}to5ftyIo zYGPu%5VO~WAdD30M1l(zv~%yagFL#oo2ypLBVk*oE!K(O8!@Y;iJ^%Jjtq@Zlj&oA zdn-X0o^e*7@;Q#}aLMw8JbAE-UH!xCfAS#T`N~fSJ)bqNy^cTr&@E($cy#|L2S*B& zghfeMr(`k;DN&^oqkHxcMsWQS$*ej>>5^URMu(yZt z{v%kaR!m_jY1)QWmHL|lJHGkFwX8KaOizB`d1C-xCt-ag`d;(*Iy7cs%Fr(q&3PA| zOZ}{Nl;;ylNl$bNxBnE7hD6#Xcs@uOD-bC~%T+JNtgfc>CqE_fJa*i_i7yR}@WD^N zkNHdIGBKKq8#)R>5QenW)v$6-Gmq}=Ve|eYT(WfbQ^t#9!Ar^FnH}T`MfM))p*oYI zv8Lv%jX)GdNWJxx8UMRhmtl7J*H$5=oUAqX z!-&Q0Ei7uQ<3R5STMzU;rTB*+R?4kPrC2<(gD6sLKX8ab$&JNF&tf|;X#{MO`+spi zgNJ%(TDp+8{_USrWeT3{zv88fnHiyif|Uj3hH#YG}6)6ZSsJ`2PEu*moa?_w6TC zg1*5Kq6j(%JPJVy!>9vHgg|Jmv<{=W7p;4Laz{M*p9jzV^8?QV4`4k2{)GQ`LJ33K zW?0M>#G2Q>oSEk=B9|-Bjftzl;B2cu#mQG%AyA%6dS(anZ+SBV_dUSK!;dgNILaqK z_^;gXrt7%zmRFIfN>j)eW22!3tLCLZn~Sk&HxADntMuXqrxjSWl`i|p+_ z!nOmQtXaGOc@{=MYmM#L^mh+%`}gm}%A~pR18-wqYaNdr8eqWnDaiySEuZqquJi*` z6Jta{L_)N=i?eKzifC!B;ZI)O$l7xq+(2WBvLcW#2m*oU3fz*wbp;9ABtZhhG%30v zUa1(TDTTn424iC-1n|q>j`5wl_pxsPiX_q6V{|k}GU*7%7D^YQcXmJ6xwL)hwKsO} zxO0Tq2)rOue^c=s8-R~WxHXz{{+kvC-MN>BQK0>NXtXb0z{Rh9Ir)-Hw>EHjI+Gtj zqr#~2zn>p``Nur=+bz8LJ#S>q<>%p*e0TR8Vj$WQ9_eW3)bIV;u}Bhr==;y(xpqe?CN@wNgKyD*|V>gPk-SK z>auY0(m9L_MR=Y;sVE6NNf-#EkY#N~jj#D=HG?}g4KYwD6`3giVdrMR-lAS-FWXlZt?)eSl zTefg;#{oX^-cR$A8?R#BpS+gFmO3WJ3utX{?wr|d+JBG-c64&(@_97YX7NvB1S-cm zXU=R~-{)}O014Y-@yw2LM&UGNmZTq0tErOq6hiMNykP zHN#-?L_kLvHT6^lV@wQ{=o)rtuCB(jOiVR_2nz_}DE^7YKwydx3_)96n#~U#;N|Df zp@={$kS1(DpxJ#G-gnDlF1dCsML+%w$2Mu3*~))>^EV`9g08(O#tR{)lySOHXtY#} z7hL*=OY{ttIM`p}@QA`Rk8L|vi5V749)6&4s_IEME#csf$C$gQjW2!uuQ+!}E8Rzi z_`>IYz<+$@XO?5Dz!Zs#jC{lTwZHkz^&77Il7{ssw{$NMJjVuLg96aVYx8c2Qn4sj zUVk0gWlQKB%i|dG?DLdy^bM^vh5q5mHshy*kdnxCF%k~#uY3iWd2<=~^>1*8hxpmI zf5qkpwy^H)ujYzZTuz8Y6l3ERH#@ntAj`P9pt|42e{&#xztvrPq`fsA`S|s6A4x=n8)LL_ObI&7lt93Goytd zIME1H_Wpg{{rvLVx3TKl%Xrb{=drtYi1AQS5{Wa`95{L+#F+pAwQu+{a$`PYy+?Sg zbAnBK_EMd3nBP{-dGo4SIj5Pq^(r<7Du;;Nn45qi3=33elUOoCeq0iuaZ3_EZ&Anv zNU6}$K&pV07{)Oogpk;>k^1TccRmqt=aZvYx`Z?g0vxp6gpsbttgFUKR*^B|Zbj0x zDfH||>l~&bNT(#W34wx=7osV92hywz920pLO*KIfqP6CAZ+I~qe*F;N`_`|F+FGKZ zQ26_%+RJ}FFuH#G3)u)fmj*xpdj3ma>f|1c98EmgJuir5osaTzIiU!DD#~qnMLBF8 zmOOHOLvf3-vz6E`A2TeKFjFZyZg?e`xpNu3{{e#Wakf0Non2dYv-bKcx$3poGH=m5 zLNpKW+|Pa6_jBp8+0<88$DY7RKR(XuSEtjQw`c*6?b*ln1BXDvoEa^I;jv9oC-?qe z{xL%%Io@&0t8s&Xu3U*g*!ak^T>Ynxo5D;c@sss<4zw@595sFzcjzFG?;B$C?m?>3 zHuGAuoI9_Ym9txzQx{QVmvE#Dm}Ik6T(H!k>##)>N&E;JGbW+$Pl|CM1R4t^420I0 zLXk{5n29Q^#wBPYiL_HBoMg-qC}(PfkYL1>i2{hMYAmC`ctNpwN0B{;U7pxoq_aCu zF-Ra2Hqx?Voqt1;FZ%dFM8dI%qFDd$ZXY+|EOHZ{(_1y_gHGJ)e%2EWINoezRpi z7cOk2wXq>)6rSYcry7CPix%+s-hFI4(23HDd9AI4Q54S`4VG;&GBC>RKe&^XYcF8= zszq$+8pPFtk~mw}|2R|#IVuD~WU>w;)r8r24rzCoaJ(CDyq9fh-(U_*G z%0%iA!|)Jd1VhABU5G0lXSUYy#GxXNHML=CY~FPgVM#?Y;gGCr!D?E74!u|@SW1L6 z5vGMO%vc&;#D-saKIv?lpWi#kZ4c(?9ta2`1IxBZHO1VMl_<^AknZf91#Acia7(fAx9Hz2F?I`g-;anb+2;(doMQ*8vdz+H z6+l$h)-zKWwR17*BuP}vrVL9I`*-6NCa|0oW+II=9fUAwwoS$dx(UXIFpM~0=Xq8v zpn^e7+Mqh?z<|I^W(d7v`E-sypQEQKEgQ3@9W&cwNa&7!I%OCsqIDMhJJVq|EH?t?vSe{3gP9^TF`zjqgR z{^-{%J#Q%&T)Bodmz_uZ+%{5~6k2NnFCYxVxU^O)(4ZoPG$bqL&7rxzmVG_FJaM3t z3s)|sdrucTHa*GA%U{Hhogm6h5K8-ZUj5@C(5GqHHKiy=N9Ztua$}BI3oJphZ52sC z`8h;6OCY4gN>(AqM({&HuB1shFf?ehb2Q>`e@JhxlPC9=(|RhLjNv>L%w(!uH+1$^i7xgs%om3w|EX$yzCOjhR1pGiQU|H`$itwu$j#dJWgFx9ShEx$BOgMVb0>&%vv~; zy2e_Zl!NWqaapm__@0mF`P3wm%*ocWd(Q!Ww)+tK9^FN$RHVA03Z){v2t0Y(I><9Z zOiofnemKd6IL)gkV=hMrGD0BnT$fTlPhCqh!iXdGrjy1_rWx}bzW58xZ=Rf>b5Jl+ zFc~c-@Jl69Rt?PyXQ9+t1m2GdFi!dXlrKgIL80VN>JmsPnV2Z>u@8TdCm!2Hds~b2 zdCT7VB7Z#pOC1DN#bQan{-$fhwJ*Jp z?K`^p`ZsQ8qTrDX208f6Z&ANy4b|r?#j2@^y}u|r>iVDRA*i_7MjS{IQcix?N>Lgb zW_bG!hPUs;8ymwg3=FM^ibZl-@Txbygg3tJ1{zx%Ddmgg#`CfGgbIjafBi7`-Toj?Y}~>lzu82#CPQs~4Gl9Im@%`3`leb;Q{t9f zMg~S0>>FaJZwR;KqQVHb=n`p-X@FNMA_e#uL}lH=rwy&akOYMi>MV+qjvL@Ox~~*| zpfHo!@;GRsz$M$d2o(m5X`6d{EG$!ER!eH@B!+2XXR49Onz(B4X&wJ0*EIE6(6LyE z>xG1&!ZajdsHn=OsBf(0!2Tn&v^0ny3?s{MYC>ID4jhJcqC!o7q2O6G03Vg>*WVh= zTm7f!`TZlmA_%I+$8+lafB8oF@qhRe0A35+c;jV!<}=^tjypFqex#p#-)*EHd6cSo zb4kycjnmkGkxFAE9HebcMt*_G2SF#pF!%g^PUwdYd|6<$7%Aq@;^pj4~|T%qq)9ycjuxzJEi$i;imwplZ{ z=^g8M`RlG_%Og*6{~a57{Gn|O4~{Z1mSfMheS|?k7`Q}HKoErpB!)ENprHUK;gC!x zaVJ7j_4Q~C?&(z!&J3~G0mb3b-|b_{-$UPx4at;Xr?ech?Mo<)^pTcj@{nm^Iw{O# z8X-+k0Z+^2o-+QiV5+hLJ^xj8vR6DJL*|Z*jz({K%XP+=zWgElAVjJ7 z203B88YHr5oa!pfR0`9v5r%;Vlpn;+M0MFqh(eSf zpnMNia#2x)h$1WjRw98g3{(_RU6tnAmtVr`-}Ex(Eto~zp*%#3_^32a(1L z6(v6^EAuK!L4?wAuR$p9ua(9y4U(w@K@ig4Gr+h0eLeTxu@OT`4AVe}2s>f1`l3~& zGAW|6cVj0U4(&U{uC03^3|aN(x3KJGSJImce~0uYlL6H6o&gF2!&vDQ2fp!5O5KBW zEIa?K9tJ5T#eAN@-X7+hvpjCqHFY1Z{OlgR`1L|eNJ5 zyAJLD>6hY^#tUZ~@GN-%A4ThrF>}t1Ul1X4bNO6&?s<#NPyO?|iBv3iZI+wb`$2>u z1?_WZ^6n44h0)m+txcKr*_=kUf2d)SCTmievBH@_FJ)YtiE>h*YCa#&!xe-T3 zJqB|DT5Aj`&_=w!&rcNMG?-TO_Y6`~SH)#7c@ZntEM-PVGqz(fXKn|MZO8KBQABOM z#dp4X8`~e;yL)j*bMZKxr9#mOAFJbHtCLkV3L(KX zDleQ&(Ut%AsZUr}f_WnL7|;Y>ktQ+cp)N{N>E*wC6RCtQV<3a6T{<#euxl~+Iqpg z_uS4u-kRYRuez3%D`%D6d41g8Sf4mo%8Qs~2pTdL4H=8!Ldc%JA~`pf57&aSun2tf zi$7q+nx$O-*$>dzQioESFp4mR#4CBY1s90RWerhdyE&Qeqcg0pm+%jYOf z13zD6tgDZ(INYu6fV*bqih;tE4V_Gm7r zUvuLX^76}9;s?i0eu!CR4K+z>vkpP1@Pi1~kMR5u(~+c{6gAZ;X3lITnMv{akADfz zb%~<558d2(?VNkTIjlZ^DGL|2)6`T;Hk*ozM_ix7oqZfW)W^N|JjS*yPjKW&H{blm zclqwOe$1c0`^~)XFK<3+bozuTF@>odmI?U{X_Km186NM-Ggd4cf%41$;-l{%kxt@! zK4YVKq#@$&KBgJFvr?2-J%N@6h`10+nkL#w66muC{#EGFrbI`IT;CA+f#H}TX&T3- z%%TX5#w!&`BvS|>$8^D6fwJXL^GN)-R}#wTzR4wtSymz)0RWn6y6dDPZsPI)>rI~w7#<=pf~ z*U-~D%-wf9$oIbebGB{W!RP+@E38;KpDVAvkRS+8%Hlg2D$ki8X)IH+WJa1zofDJu zEC@BWB}oy`QE!t?q%e(2Mj?(Z#Nz=`7?Df~+G`ymEwGUIgF}b~b5DM7J^6)-;!#YW z)_CK2^8Lej`C{ye7$>^@PHjE;LxUXIvxS6hQ{CK7vZ@Z#v_XiI8-q#0kSY`Q^ivdW zojBp85i2~XD8$W=Qy3p*e5jvNejF(xnwn}jf6XkGpSOrv^V-Qx6e*65arwmyX>6`z z(ZY_&Pghz`#*w6~#0pM#zv9Y8C+YCDZ+@ItUH3k*fB)eqnXak|b{{?$=&F>epZZh& zFz_sF0p=>$j3Ss3ltSjrYZd3MS$uTAeiA5Eak-~FQspW(DdYb3AN%kZx%bWodG)%h zxaCh?$yL`}F!>OA{!vD!5aO7Jp`z_CfYurHy!oxK;I*&6mLL7#F24Br|KtPj`2-st z{4Uw53{iA;oq45zKxwLy22E8qokK1uN77O4&{5}*O&F7bML0Sarye4T1f~hi^=VAo zLi;`wPwu4ovWt(VG>?ZUv)_~=3PSusk<#b{-b4Y=$f;zhDdQ6P0WH^DNoL^!!m&{b z-QA3I9z^tYlFZhStg0iCuEsE(GG`%n`71^z_LQci;40gPDMltv!5SEbL?DQw05?BQ zZghzJ=rD!x5o`;pYcs4|HH&2{ui)G@OPDjijrxWfas{`1AL6(|Yki8ADw`mTChw&z zZx_xcJ{rdZ0$SS|X>D)f$tU;eWFjOKZcF@%7X~x%Sugt!JK(*qP{MR zA4Eub>dAGoc^4^-W1IZq7r$j_Xq?;c{Sue2Jr`i|fsj&^Rbx&L!#MiEmG?M~#hc#p za;|yFMSTA|zamolX{VREw<;i!tjf2(*+opQ)0%XFWzT|S? z>;FZur;mIzKw*3k%XV;5RU|XDIO%FkI|0Pri>|~1CiCz*j`$l#w=hxY6O{7gMh3}^ z#1r0-iiXB&mM@>l;^mh!Z_ylP&1p!Io=+iHA{rP%2!UnWq^fF2SJhHg+lUH%gorCIrDdao zAWqA9C5pLG#s>!|jE|r~pZdmX7A$CE>8guZvV0-4=FTLONh3{(=LH0QKrvs!BE&Kz z2}_c+Y-%zNQpN_yG~$1@X`Jj*u*%O=r1U8-U2rsA>xbYeaZCs2N<*R-3ZA7b09u7X zE|GNDySIzp-eEdAnx3-3#t9!J;{E2_xvg=RTt9qT6aU1|6&Qv=5Jqt&;AuRFGlkai z-v4w`2B$igpj^kk?xw5x@c;P_%wmb2dp6Me@|RI5S3dfN_)8^TGWqzP>M<_6=fW`1 zQG}UE;SCJX{ga>L_Vl6*lhRm@s@f{%Et*9`a~+9V5kr=ocFO) zDcnGn{TQuPA>VZtx9j&F&w>G1E72QNSo99sc8b2G?S2wNO6-I~GL_(_Kf94NFIvIeMKj4(XHbzM2twSVOD<9vQqWkJ zU`9=nx~zq5%8KMec^Yy|b>758TIN~wx#+n34OJJ4d?{ zixy7?Oot1^qBr|EA+_;H7JG*qcs?NX=`RbVsoBIZ#qF4Ecj%UHj z#jL7XD{s0o>?{0QMWGf#$giyb49l0#C1omVGj`0L(?{LA%E`sg^3fc4s5CiG5h+b= z%J^N$Y|m>brLjzd5C7E{`P@JM7d6?qL8i6zd}z96gBC%Lr_%C^c)|~ zkOGYIACI}U8bYlp`ihZaNM9~w+>NQ?ur6~(YWL{xYZmTB;X&wZay-1;?2xg1XY z3<9$XX`9&DEKYS6r?wiqDvOy+;8bTx*4Ch-2zOwB;YT)6>gkDtb+(P?2jp`_=C(EP z&Uf9wTi$vtsiad5d~!ws+DOGMzKUMN$XJPqyoav@NXb8Z_)9dj)bZ{QzlDkMJj-X- z(p2rl#Z*%RqQ}9Mf}*b&E`}5WMHs~#iTF9txPhV+DuPIt-FxwrvCfkr3_@xfYq|5g zzvSQk?(-zm$w)|3SoP_P4nFzqO|VYBP^iGq(mv;-5^<}Vwdf6RjK;k0prSCJ%bRb0 z_ZxWI``*In@OZiOHP!``WmirCE|Nf%Q)uNAP9|+UFJwiF!;C7MK%Kf#=kJO$O)WiB zsPG{y(_r)FJ$&NUuW|o^$ zIQq+n5B=yJFBAq~1<&Gh9S>~#?AQE@aK-zh$P0YWx8C;tH?i)PS7S&?7)BIak1&dF zxRR0xIr*?vaCq+_nr61rT9;;3i-URwE~FA+uDF-sa~yAoq6o*1-RIAJ_PgBqi+kzs z8zu;2W~5;l7?y$7f+&n9bfM@HVbRdsNNa07%T~BGW0{qKohOlXO{#;{%T4@VUSFES6>R zj*q{W`E@39>m7pV)UM^(pt3JDr6AJKGZB#UA{I6{->VBwP4)J>sBYryxz$yK%Tw3|~{laW43 zQ9j%2)#WNYQO@0;YT|0*6yE+}k6i;U_LSNQhzqGa*W=OqH*xWm7oBm+SeGT=CJ!PY znNA^v=I$Td&VPLBKM2AwwC#i`Y-8ANNw3}i=r^Byp{@S#>_5u_(5t`vWozg&_kJtN zd9Twtz%98U5}C@RNFgrAZcLk`>{#0SR1)W> z!%0(kmA~n77hoYcIO4H;&^;mVuazd1N%D;^e3Re)dLy6y>OYaG%1j0oPyEbSgF#4v zX_=UoSuQ1x8@}w{x{IHD?MFOt*Mr(gI+0~Mrm{qjGo$8)1CM@v6YJI)tbbwb`={c$ zc#Pv`Qb5P-b??_+_;HDmF>FB;26&!_U-DV9YB3kS_#zUiB%bGE7%}@x8j?rvd4vPI z4zS|F6}Re$9@14ek`tcoOsdUsy$t_#5@z@y$n-G*N?YW~)il}93a`{H zc~DRf*MtDYw7IztfaCG8g!y7=VMETW{8j~v)Z5_{+gqTX#IkzsS$etfoA&RAZ=_ZS zdQ|FHFsl%!*DeR*)kZRNlFk>VBF;Q+9hoPbIg1Yu>5KeXb*(OCg%vi%9P#r)#a&y&=ePacKLfPmh&EE*y+O(B%IyY0vJG0 zJ(p>Kps;m4l@C5EZO^K1u+)72ZV_UalhEx=^W&|A`&T8Q`qzo|Pzc6lBt$n&<~TlJ zwSl=QR7=}Ya& zCU2-5?!?dcdP~Kh-VssE)k4p$OoPZbo)_Ds!Yn9yOPiwV=W6lBA7wjg!o$c&{Z z<|rL2d=bl8&6<@vs6c6Xel2-)@zSi+UrT$$D$6eN4YwOQE1V%F%vrSFbJAwKA+!bt z9QL48<4x{As7bG*yM6OJtfxGDu{vC(bX^Rt#qBtFlbgUynIYm08n0{hoWuy#2S5Vj zI)`OWR)NcC9TpvOAo_R2f8#tohL=BXRp{#o8A>NxUtb!6Yi>$gb=fCW)Z5mSo;fUMHlT#gpTu zgU=3O;l#BAl`sc&=ud~v=f?sv_Y@j6#n%2cn%IcMT7%9tbYOyyw`ikdA41uLGxfD# zH&&#!UhJv)e>}D#E4QwMotT(&wRN(gS&2r0u8;M_a!~5KAsPuIe1&|=%+~1A`qA6D*IeJEM{T()V=85oDiWU8C)3h|B&WME^9C z3SxkN&HTu+&9q6^{Ij7r6{gGav!Qt`%$RFFi$|75Ff*U^Ji}D&Ak}uheWx^p+kh=V zJH?VYcG_kuEQ={Ekcssbb(5j(7x{5%<`Al&d_d)OV}mB8mO<3~#H2BqCY;iLrnf_9 z?RXm;p|SQ10Wi9L?P!XWRQvhr{@qvGgRob#t|H^*xlc>ed3nb~khVN^r~{g1=Wi3b zaj!D{3G`M6scGE>525-pjtM&hQ;31*RmRbYtVT2ZfAH=s&Es}-IYSB*hMY;9kCQMJ zFWm09$=~VqjufG~45KPEP2E7^zed#n8@dEPN{1Xz`QdpP`CrS`!G?Xh`BQxqq07-T z@LEx;j_Wy>SJ+g}xZC*{@vEC3&c1|`O!pUpP2Ao+)`BFM1E^{O!u`q;KyS@Q-&Q88 zrZlp0;(j-ojZ9$S#|>^d!52huPwW0KK>b|=Hma=Q-=hPSyMQJ|fK^TPd;P%8o`4h= zE8lE08xT< zitKQ6HgroPeVD1%CK#OC80qw@66LF7ENLn#YLy4pKz9hVs8c1?>HFf%@~yGTK-SzY zMY@+ImD^9_1wmHfriq&Ae-gD>R>0SjE8dXR#kC$S)eUb0LBEeuRY(6OqM#~MbbT_S zB@4>8Zo~u|D(}rVnm0HsA-$F_a}dN!kdNG_2t`FT_ZYJt5D*K;ymbhaka{){+%n-Q zNT=DAY3tBB_1|+p**tLyQ>H%|?NP7YHl;SJPP;s1`xMTL&bAVv5e7t-|GXW-GWe5uN<^CddMyyn|` zNk1!?e@U;mk%*8LBECJx%uED1^JbK3-$4G!-g5WBj;it68sSGPl7ObS674t+1IedA z!qh;~)ejaYT3d;joz}*v1ok$!g0jTy$I$E=% zZ5bmO8nh<@=-+P|9EsUS9fV(A==OdpiCmw0c6Hy^y>8LYrPe8*%eaPuPOOps1!T?4 zVwhqf-5Q+$dXlRxRaFclbh9xEihr0Zo9TEll!Pz2`Xzcm@WOM|)@|kKfp2>(mS6n6 zU?}8=4!2}6V9jG3BaTpKg!U-dX0egmef{6&&4@My``ONOF~;G2>nCtY09&291$OUm zth(*x9T1=f_@b@N%Hb9J3g7g*7KRol3@q*k`6Du$mlKmx%H!z$_jIxoK{*l>U?y|2 zUisJmFI-I9M@3i0QAj=aP5RioX9I$ZO~jDc)DC=()P}M8A%`fvPm_5rNzMOWT4 z2KC>-TTPXUs&hRfmcYEJ`Lo&4$>pN5UFBk4V@{tt4LaPH@Z_1<( ziD8Jdy^?-Ly09f$1x1nz-EWBe}oFPtnP@)rDBW`tA)TAhojvfrD8?j+1Pk(uzvH7TJ*Xu{)tE>BrtGOyU zaBz;>ya*1EwUBi;bAR#ZQ#*sv=0=JA`J!LSwohEQ<%0JH52A4X%$_$|u{;Jk) z*tH%km4kHj7e20^IE#w5I!utiG^)U>vAhzF8iN^0|1|D&?PxIU zs@Jt>f@g2h4e-%v!6@@K=BPYvbNwncFE1SbVn9@LsLNpDb{?vw3^mq90t;N(lnFbGe3W3z zmwA^z<`ANk*Ndl7W}h6AHeAfKl6(raTKOoK8&5W zJMTC#;f0EtKX|R?ku06KEKe3>QbV*cLGLPGdqqFJAJU0(NC4>EL~s2FPXH@=yKK7L zS8Cjep4_Cd(ZgnqWINgTU&YM))ZkCKp(COsKHb9*WSB1H8V6wqWq9ZyD-ta7W2Off zmkw)voiT0f&a!Ci+Pj(S{1r@Q)EbLM_Et4G8gJ1Q_JTU|L&>6msIRR@y}b9h`mB*S zbU=KDr^>?F4Fy0JQURCKvL?9}i00H^`1MLsI;?C%*qiQ(fMkSAmH~*Q6+J_^DI@W3js> zRm3QTpw9ut82X=!m5NL2DS49YW_E90B6|2)(3bqgJ3l)=eroeF!?MC4*=7OG5f6jL zeXFa=sM5#PpFjfHC!r;kd~})tEaXW?(@qJt;xnPH(E}GBZbgN0xamw6JaaI9wsZ?br9C;>1UG?_vqQXVpItr~QL^-!dw4E z5;e~i?l(Y#!r3RWEB@lMa|MRH(_d6D@YH5K%kC;H27>s_U|^QV^i`?t69lG$nOLWU zsxAp8>PIlAY-A2SB+hf*+{dM}l~PCfAhxl7e?cDm;Y6M2Sdd1*4PZT+Z2E>NelzI? zAC&At?PZb;pJPy@%a1MB&Q=EB2kVTR6)=YaHXM#pMIxnpWli=vMg2X^Mde__kA(!D zcH<~Mb-4VuTfro-hq3Md3@Foy(-1$e$A{Rxo_X}5-8lZe?)Qx_VCN|i_x-bC*tf_8 z$omlFHwk8KZnd5SF1xppV&m0Q#ivA1m!OsEB%5(H$qk|IL+KCv-!>4bDW z@2@LAg*Jq*g!ir%h0!1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/src/assets/main.js b/site/src/assets/main.js new file mode 100644 index 0000000..d9dda04 --- /dev/null +++ b/site/src/assets/main.js @@ -0,0 +1,66 @@ +// LibrePortal site — interactions (mobile drawer, copy, scrollspy, reveal, app filter) +(() => { + // mobile drawer (same behaviour as the dashboard) + const menuToggle = document.getElementById('mobile-menu-toggle'); + const drawer = document.getElementById('mobile-drawer'); + if (menuToggle && drawer) { + menuToggle.addEventListener('click', () => { + const open = drawer.classList.toggle('mobile-open'); + document.body.style.overflow = open ? 'hidden' : ''; + }); + drawer.querySelectorAll('a').forEach((a) => + a.addEventListener('click', () => { drawer.classList.remove('mobile-open'); document.body.style.overflow = ''; })); + } + + // copy the install command + const cmdEl = document.querySelector('[data-install-cmd]'); + const CMD = cmdEl ? cmdEl.getAttribute('data-install-cmd') : ''; + document.querySelectorAll('.copy').forEach((btn) => { + btn.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(CMD); } + catch { const t = document.createElement('textarea'); t.value = CMD; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove(); } + btn.classList.add('done'); + const span = btn.querySelector('span'); const prev = span.textContent; span.textContent = 'Copied ✓'; + setTimeout(() => { btn.classList.remove('done'); span.textContent = prev; }, 1700); + }); + }); + + // scrollspy → light up the active topbar pill + const spies = [...document.querySelectorAll('.nav-item[data-spy]')]; + if (spies.length) { + const spyIO = new IntersectionObserver((entries) => { + entries.forEach((e) => { + if (e.isIntersecting) spies.forEach((s) => s.classList.toggle('nav-active', s.dataset.spy === e.target.id)); + }); + }, { rootMargin: '-45% 0px -50% 0px', threshold: 0 }); + spies.map((s) => document.getElementById(s.dataset.spy)).filter(Boolean).forEach((s) => spyIO.observe(s)); + } + + // reveal on scroll + const io = new IntersectionObserver((entries) => { + entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); + }, { threshold: 0.16, rootMargin: '0px 0px -8% 0px' }); + document.querySelectorAll('.reveal, .stagger').forEach((el) => io.observe(el)); + + // app category filter + const filters = document.querySelector('.app-filters'); + if (filters) { + const cards = [...document.querySelectorAll('.app-card')]; + const countEl = document.querySelector('.app-count'); + const update = (cat) => { + let shown = 0; + cards.forEach((c) => { + const match = cat === 'all' || (c.dataset.cats || '').split(' ').includes(cat); + c.classList.toggle('hidden', !match); + if (match) shown++; + }); + if (countEl) countEl.textContent = `${shown} app${shown === 1 ? '' : 's'}`; + }; + filters.addEventListener('click', (e) => { + const chip = e.target.closest('.chip'); + if (!chip) return; + filters.querySelectorAll('.chip').forEach((c) => c.classList.toggle('active', c === chip)); + update(chip.dataset.cat); + }); + } +})(); diff --git a/site/src/assets/style.css b/site/src/assets/style.css new file mode 100644 index 0000000..c187888 --- /dev/null +++ b/site/src/assets/style.css @@ -0,0 +1,276 @@ +/* ============ NEBULA THEME (lifted from the dashboard) ============ */ +:root{ + --gradient-from:#1a1442; /* indigo */ + --gradient-mid:#1b2a5e; /* violet-blue */ + --gradient-to:#0f3b6e; /* ocean */ + --surface-solid:#0f1729; + + --accent:#00d4ff; + --accent-hover:#0099cc; + --accent-rgb:0,212,255; + --accent-soft:rgba(0,212,255,.15); + + --text-primary:#ffffff; + --text-secondary:rgba(255,255,255,.82); + --text-muted:rgba(255,255,255,.65); + --text-faint:rgba(255,255,255,.45); + --text-on-accent:#0a1426; + --text-rgb:255,255,255; + + --border:rgba(255,255,255,.16); + --border-strong:rgba(255,255,255,.26); + --border-subtle:rgba(255,255,255,.08); + + --card-bg:linear-gradient(155deg, rgba(255,255,255,.09) 0%, rgba(0,212,255,.05) 100%); + --card-border:rgba(255,255,255,.16); + --card-shadow:0 4px 18px rgba(0,0,0,.30), inset 0 1px 0 rgba(255,255,255,.06); + --card-shadow-hover:0 10px 32px rgba(0,212,255,.18), 0 4px 18px rgba(0,0,0,.40), inset 0 1px 0 rgba(255,255,255,.10); + + --topbar-bg:rgba(15,25,50,.40); + --console-bg:rgba(0,0,0,.40); + --surface-bg-solid:#0f1729; + + --font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + --font-mono:"SF Mono", "Monaco", "Menlo", "Ubuntu Mono", "Courier New", monospace; + + --maxw:1180px; + --ease:cubic-bezier(.22,.61,.36,1); +} + +*{box-sizing:border-box;margin:0;padding:0} +html{scroll-behavior:smooth} +body{font-family:var(--font-sans);background:var(--surface-solid);color:var(--text-primary); + line-height:1.6;overflow-x:hidden;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility} + +/* ============ AURORA BACKDROP (from aurora-background.css) ============ */ +.cosmos{position:fixed;inset:0;z-index:-2;pointer-events:none;overflow:hidden; + background: + radial-gradient(ellipse at 20% 22%, var(--gradient-mid) 0%, transparent 55%), + radial-gradient(ellipse at 82% 74%, var(--gradient-to) 0%, transparent 55%), + linear-gradient(135deg, var(--gradient-from) 0%, var(--gradient-from) 40%, var(--gradient-mid) 100%);} +.cosmos::before{content:'';position:absolute;inset:-25%; + background:conic-gradient(from 0deg at 50% 50%, + rgba(var(--accent-rgb),0) 0deg, rgba(var(--accent-rgb),.18) 60deg, + rgba(var(--accent-rgb),.22) 130deg, rgba(var(--accent-rgb),.20) 200deg, + rgba(var(--accent-rgb),.18) 280deg, rgba(var(--accent-rgb),0) 360deg); + filter:blur(70px);animation:auroraSpin 38s linear infinite;} +.cosmos::after{content:'';position:absolute;inset:-10%; + background: + radial-gradient(circle at 18% 22%, rgba(var(--accent-rgb),.38) 0%, transparent 35%), + radial-gradient(circle at 78% 18%, rgba(var(--accent-rgb),.32) 0%, transparent 32%), + radial-gradient(circle at 30% 80%, rgba(var(--accent-rgb),.30) 0%, transparent 38%), + radial-gradient(circle at 84% 82%, rgba(var(--accent-rgb),.34) 0%, transparent 36%), + radial-gradient(circle at 50% 50%, rgba(var(--accent-rgb),.14) 0%, transparent 50%); + filter:blur(50px);animation:auroraDrift 22s ease-in-out infinite alternate;} +.stars{position:fixed;inset:0;z-index:-1;pointer-events:none} +.stars::before,.stars::after{content:'';position:absolute;inset:0;background-repeat:repeat} +.stars::before{background-image: + radial-gradient(1.5px 1.5px at 12px 18px, rgba(var(--text-rgb),.9), transparent 60%), + radial-gradient(1px 1px at 47px 92px, rgba(var(--accent-rgb),.85), transparent 60%), + radial-gradient(1.2px 1.2px at 110px 40px, rgba(var(--text-rgb),.75), transparent 60%), + radial-gradient(1px 1px at 165px 130px, rgba(var(--accent-rgb),.7), transparent 60%); + background-size:200px 200px;animation:auroraTwinkleA 4.5s ease-in-out infinite;} +.stars::after{background-image: + radial-gradient(1px 1px at 30px 60px, rgba(var(--accent-rgb),.8), transparent 60%), + radial-gradient(1.4px 1.4px at 88px 22px, rgba(var(--text-rgb),.7), transparent 60%), + radial-gradient(1px 1px at 140px 100px, rgba(var(--accent-rgb),.85), transparent 60%), + radial-gradient(1.2px 1.2px at 195px 70px, rgba(var(--text-rgb),.6), transparent 60%); + background-size:240px 240px;background-position:80px 50px;animation:auroraTwinkleB 6.5s ease-in-out infinite;} +@keyframes auroraSpin{to{transform:rotate(360deg)}} +@keyframes auroraDrift{0%{transform:translate(0,0) scale(1);opacity:.85}50%{transform:translate(-3%,4%) scale(1.08);opacity:1}100%{transform:translate(2%,-3%) scale(.95);opacity:.9}} +@keyframes auroraTwinkleA{0%,100%{opacity:.55}50%{opacity:1}} +@keyframes auroraTwinkleB{0%,100%{opacity:1}50%{opacity:.45}} + +/* ============ shared ============ */ +.grad{background:linear-gradient(100deg,#7ae9ff,#00d4ff 55%,#0aa6d6); + -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent} +.wrap{max-width:var(--maxw);margin:0 auto;padding:0 clamp(18px,5vw,48px)} +section{position:relative;z-index:1} +.eyebrow{font-family:var(--font-mono);font-size:.74rem;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);margin-bottom:16px;display:block} +h2{font-size:clamp(2rem,4.6vw,3.2rem);line-height:1.08;letter-spacing:-.02em;font-weight:800} +.lead{color:var(--text-secondary);font-size:1.08rem;max-width:54ch;margin-top:18px;font-weight:400} +.pad{padding:clamp(80px,12vh,140px) 0} +.glass{background:var(--card-bg);border:1px solid var(--card-border);box-shadow:var(--card-shadow); + backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)} + +/* ============ TOPBAR — grafted from the dashboard (topbar.css) ============ */ +.topbar{display:flex;justify-content:space-between;align-items:center;padding:0 24px;height:60px; + position:fixed;top:0;left:0;right:0;z-index:1000;background:var(--topbar-bg); + border-bottom:1px solid var(--border);backdrop-filter:blur(12px) saturate(140%);-webkit-backdrop-filter:blur(12px) saturate(140%)} +.mobile-menu-toggle{display:none;background:none;border:none;color:var(--text-primary);cursor:pointer;padding:6px;margin-right:4px} +.topbar-left{display:flex;align-items:center;flex:0 0 auto} +.libreportal-logo{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--text-primary);font-weight:700;font-size:1.05rem} +.libreportal-logo img{width:32px;height:32px} +.libreportal-logo b{color:var(--accent);font-weight:700} +.mobile-drawer{display:flex;align-items:center;flex:1;justify-content:space-between;gap:12px;min-width:0;margin-left:24px} +.topbar-nav{display:flex;gap:8px;align-items:center} +.topbar-nav .nav-item{background:rgba(var(--text-rgb),.10);border:1px solid rgba(var(--text-rgb),.20);border-radius:8px; + padding:10px 16px;font-size:14px;font-weight:500;color:var(--text-muted);text-decoration:none;display:flex;align-items:center;gap:8px; + transition:all .2s ease;cursor:pointer;min-height:42px;white-space:nowrap} +.topbar-nav .nav-item:hover{background:rgba(var(--text-rgb),.20);transform:translateY(-1px);color:var(--text-primary)} +.topbar-nav .nav-item.nav-active{background:var(--accent);color:var(--text-primary);border-color:var(--accent)} +.topbar-nav .nav-item.nav-active:hover{background:var(--accent-hover);border-color:var(--accent-hover)} +.topbar-nav .nav-item svg{width:16px;height:16px} +.topbar-controls{display:flex;align-items:center;gap:12px} +.tb-ghost{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border:1px solid var(--border);border-radius:8px; + background:rgba(var(--text-rgb),.05);color:var(--text-secondary);text-decoration:none;font-size:.85rem;font-weight:600;transition:all .2s} +.tb-ghost:hover{background:rgba(var(--text-rgb),.12);color:var(--text-primary)} +.tb-cta{display:inline-flex;align-items:center;gap:6px;padding:8px 18px;border-radius:8px;background:var(--accent); + color:var(--text-on-accent);text-decoration:none;font-size:.85rem;font-weight:700;transition:all .2s} +.tb-cta:hover{background:#22dbff;transform:translateY(-1px);box-shadow:0 8px 22px -8px rgba(var(--accent-rgb),.6)} +@media(max-width:768px){ + .topbar{padding:0 12px;gap:8px;justify-content:flex-start} + .mobile-menu-toggle{display:flex;align-items:center} + .mobile-drawer{position:fixed;top:60px;left:0;width:100vw;height:calc(100vh - 60px);flex-direction:column;align-items:stretch; + justify-content:flex-start;gap:0;padding:16px;margin-left:0;background:var(--surface-bg-solid);border-right:1px solid var(--border); + box-shadow:6px 0 24px rgba(0,0,0,.35);overflow-y:auto;transform:translateX(-100%);transition:transform .3s ease;z-index:101} + .mobile-drawer.mobile-open{transform:translateX(0)} + .mobile-drawer .topbar-nav{flex-direction:column;align-items:stretch;gap:6px;width:100%} + .mobile-drawer .topbar-nav .nav-item{width:100%;justify-content:flex-start} + .mobile-drawer .topbar-controls{flex-direction:column;align-items:stretch;gap:8px;width:100%;margin-top:auto;padding-top:16px; + border-top:1px solid rgba(var(--text-rgb),.12)} + .mobile-drawer .topbar-controls a{width:100%;justify-content:center} +} + +/* ============ hero ============ */ +header.hero{min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center; + text-align:center;padding:140px clamp(18px,5vw,48px) 90px;position:relative} +.badge{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.72rem; + letter-spacing:.14em;text-transform:uppercase;color:var(--accent);padding:7px 15px;border-radius:999px; + border:1px solid rgba(var(--accent-rgb),.35);background:var(--accent-soft);margin-bottom:30px} +.badge .dot{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent);animation:pulse 2.4s ease-in-out infinite} +@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}} +.hero-logo{width:64px;height:64px;margin-bottom:22px;filter:drop-shadow(0 6px 24px rgba(var(--accent-rgb),.45))} +h1{font-size:clamp(2.6rem,7vw,5.2rem);line-height:1.03;letter-spacing:-.03em;font-weight:800;max-width:15ch} +.sub{margin:26px auto 0;max-width:50ch;font-size:clamp(1.02rem,2vw,1.2rem);color:var(--text-secondary);font-weight:400} + +/* install portal */ +.portal{margin:46px auto 0;width:min(660px,100%);position:relative} +.portal::before{content:"";position:absolute;inset:-26px;border-radius:28px;z-index:-1; + background:radial-gradient(60% 80% at 50% 50%, rgba(var(--accent-rgb),.5), transparent 70%);filter:blur(22px);animation:breathe 6s ease-in-out infinite} +@keyframes breathe{0%,100%{opacity:.45;transform:scale(.98)}50%{opacity:.8;transform:scale(1.03)}} +.term{border:1px solid var(--card-border);border-radius:16px;overflow:hidden; + background:linear-gradient(155deg, rgba(255,255,255,.10) 0%, rgba(0,212,255,.06) 100%); + backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);box-shadow:var(--card-shadow-hover)} +.term-bar{display:flex;align-items:center;gap:7px;padding:12px 16px;border-bottom:1px solid var(--border-subtle);background:var(--console-bg)} +.term-bar i{width:11px;height:11px;border-radius:50%;display:block} +.term-bar i:nth-child(1){background:#ff6b5e}.term-bar i:nth-child(2){background:#ffc107}.term-bar i:nth-child(3){background:var(--accent)} +.term-bar em{margin-left:auto;font-family:var(--font-mono);font-size:.72rem;color:var(--text-faint);font-style:normal} +.term-body{display:flex;align-items:center;gap:14px;padding:22px 20px} +.term-body code{font-family:var(--font-mono);font-size:clamp(.82rem,2.3vw,1.02rem);color:var(--text-primary); + white-space:nowrap;overflow-x:auto;flex:1;text-align:left;scrollbar-width:none} +.term-body code::-webkit-scrollbar{display:none} +.term-body code .p{color:var(--accent)} +.term-body code .u{color:#e6f3ff} +.copy{flex:0 0 auto;display:inline-flex;align-items:center;gap:7px;font-family:var(--font-sans);font-weight:700; + font-size:.84rem;color:var(--text-on-accent);background:var(--accent);border:none;border-radius:9px;padding:9px 15px;cursor:pointer; + transition:transform .15s,background .25s,box-shadow .25s} +.copy:hover{transform:translateY(-1px);background:#22dbff;box-shadow:0 8px 22px -8px rgba(var(--accent-rgb),.7)} +.copy.done{background:#28a745;color:#fff} +.portal-foot{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:16px;flex-wrap:wrap;font-size:.86rem;color:var(--text-faint)} +.portal-foot a{color:var(--text-secondary);text-decoration:none;border-bottom:1px dashed rgba(255,255,255,.3);transition:color .2s,border-color .2s} +.portal-foot a:hover{color:var(--accent);border-color:var(--accent)} +.hint{font-family:var(--font-mono);font-size:.78rem} +.scroll-cue{position:absolute;bottom:28px;left:50%;transform:translateX(-50%);color:var(--text-faint); + font-family:var(--font-mono);font-size:.66rem;letter-spacing:.2em;text-transform:uppercase; + display:flex;flex-direction:column;align-items:center;gap:8px;animation:bob 2.6s ease-in-out infinite} +.scroll-cue::after{content:"";width:1px;height:34px;background:linear-gradient(var(--text-faint),transparent)} +@keyframes bob{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(8px)}} + +/* ============ features ============ */ +.feat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:54px} +.card{border-radius:16px;padding:28px 26px;position:relative;overflow:hidden;transition:transform .4s var(--ease),border-color .4s,box-shadow .4s} +.card:hover{transform:translateY(-6px);border-color:rgba(var(--accent-rgb),.45);box-shadow:var(--card-shadow-hover)} +.card .ico{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;margin-bottom:18px; + background:var(--accent-soft);border:1px solid rgba(var(--accent-rgb),.30)} +.card .ico svg{width:22px;height:22px;stroke:var(--accent);fill:none;stroke-width:1.6} +.card h3{font-size:1.24rem;margin-bottom:8px;font-weight:700;letter-spacing:-.01em} +.card p{color:var(--text-secondary);font-size:.96rem} + +/* ============ apps (data-driven) ============ */ +.apps-section{text-align:center} +.app-filters{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:40px} +.chip{font-family:var(--font-mono);font-size:.76rem;letter-spacing:.04em;padding:8px 15px;border-radius:999px; + border:1px solid var(--border);background:rgba(255,255,255,.05);color:var(--text-secondary);cursor:pointer;transition:all .2s} +.chip:hover{color:var(--text-primary);border-color:rgba(var(--accent-rgb),.45)} +.chip.active{background:var(--accent);color:var(--text-on-accent);border-color:var(--accent);font-weight:600} +.chip .n{opacity:.55;margin-left:6px} +.app-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(248px,1fr));gap:16px;margin-top:34px;text-align:left} +.app-card{border-radius:14px;padding:20px;display:flex;flex-direction:column;gap:11px;transition:transform .3s var(--ease),border-color .3s,box-shadow .3s} +.app-card.hidden{display:none} +.app-card:hover{transform:translateY(-4px);border-color:rgba(var(--accent-rgb),.4);box-shadow:var(--card-shadow-hover)} +.app-card-head{display:flex;align-items:center;gap:12px} +.app-ico{width:42px;height:42px;border-radius:10px;background:rgba(255,255,255,.06);padding:6px;object-fit:contain;border:1px solid var(--border-subtle);flex:0 0 auto} +.app-card h3{font-size:1.06rem;font-weight:700;line-height:1.2} +.app-cat{font-family:var(--font-mono);font-size:.64rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent)} +.app-card p{font-size:.9rem;color:var(--text-secondary);margin:0} +.app-count{margin-top:26px;color:var(--text-faint);font-size:.85rem;font-family:var(--font-mono)} + +/* ============ connect / courier ============ */ +.connect{display:grid;grid-template-columns:1.05fr .95fr;gap:60px;align-items:center} +.pill{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.72rem; + letter-spacing:.1em;text-transform:uppercase;color:var(--text-secondary);padding:7px 14px;border-radius:999px; + border:1px solid var(--border);background:rgba(255,255,255,.04);margin-top:24px} +.connect ul{list-style:none;margin-top:26px;display:flex;flex-direction:column;gap:15px} +.connect li{display:flex;gap:13px;color:var(--text-secondary);font-size:1rem} +.connect li svg{flex:0 0 auto;width:21px;height:21px;stroke:var(--accent);fill:none;stroke-width:1.6;margin-top:3px} +.connect li b{color:var(--text-primary);font-weight:700} +.vault{position:relative;aspect-ratio:1;display:grid;place-items:center} +.vault svg{width:min(360px,82%);height:auto;overflow:visible} +.orbit{transform-origin:center;animation:spin 26s linear infinite} +@keyframes spin{to{transform:rotate(360deg)}} + +/* ============ promise ============ */ +.promise{text-align:center} +.pledges{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:52px} +.pledge{border-radius:14px;padding:28px 18px} +.pledge .big{font-size:2.4rem;font-weight:800;margin-bottom:8px;line-height:1} +.pledge p{font-size:.92rem;color:var(--text-secondary)} +.promise .link{display:inline-block;margin-top:40px;color:var(--accent);text-decoration:none;font-weight:700; + border-bottom:1px solid rgba(var(--accent-rgb),.45);padding-bottom:2px;transition:color .2s,border-color .2s} +.promise .link:hover{color:#fff;border-color:#fff} + +/* ============ final ============ */ +.final{text-align:center;padding:clamp(90px,14vh,160px) 0} +.final h2{max-width:18ch;margin-inline:auto} +.final .portal{margin-top:42px} + +/* ============ footer ============ */ +footer{border-top:1px solid var(--border-subtle);padding:54px 0 70px;margin-top:40px;background:rgba(10,16,32,.55);backdrop-filter:blur(8px)} +.foot-grid{display:flex;justify-content:space-between;gap:40px;flex-wrap:wrap;align-items:flex-start} +.foot-grid .libreportal-logo{margin-bottom:14px} +.foot-col h4{font-size:.74rem;letter-spacing:.16em;text-transform:uppercase;color:var(--text-faint);margin-bottom:14px;font-family:var(--font-mono)} +.foot-col a{display:block;color:var(--text-secondary);text-decoration:none;font-size:.95rem;margin-bottom:9px;transition:color .2s} +.foot-col a:hover{color:var(--accent)} +.colophon{margin-top:46px;padding-top:26px;border-top:1px solid var(--border-subtle); + display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap;color:var(--text-faint);font-size:.86rem} +.colophon a{color:var(--text-secondary);text-decoration:none} +.colophon a:hover{color:var(--accent)} + +/* ============ reveal ============ */ +.reveal{opacity:0;transform:translateY(28px);transition:opacity .9s var(--ease),transform .9s var(--ease)} +.reveal.in{opacity:1;transform:none} +.stagger>*{opacity:0;transform:translateY(20px);transition:opacity .7s var(--ease),transform .7s var(--ease)} +.stagger.in>*{opacity:1;transform:none} +.stagger.in>*:nth-child(2){transition-delay:.07s}.stagger.in>*:nth-child(3){transition-delay:.14s} +.stagger.in>*:nth-child(4){transition-delay:.21s}.stagger.in>*:nth-child(5){transition-delay:.28s}.stagger.in>*:nth-child(6){transition-delay:.35s} +.h-anim{opacity:0;transform:translateY(24px);animation:rise .95s var(--ease) forwards} +.d1{animation-delay:.05s}.d2{animation-delay:.16s}.d3{animation-delay:.28s}.d4{animation-delay:.4s}.d5{animation-delay:.52s} +@keyframes rise{to{opacity:1;transform:none}} + +/* ============ responsive ============ */ +@media(max-width:860px){ + .feat-grid{grid-template-columns:1fr 1fr} + .connect{grid-template-columns:1fr;gap:36px} + .vault{order:-1;max-width:320px;margin-inline:auto} + .pledges{grid-template-columns:1fr 1fr} +} +@media(max-width:540px){ + .feat-grid{grid-template-columns:1fr} + .term-body{flex-direction:column;align-items:stretch} + .copy{justify-content:center} +} +@media(prefers-reduced-motion:reduce){ + *{animation:none!important;transition:none!important} + .reveal,.stagger>*,.h-anim{opacity:1;transform:none} +} diff --git a/site/src/index.njk b/site/src/index.njk new file mode 100644 index 0000000..8815eef --- /dev/null +++ b/site/src/index.njk @@ -0,0 +1,162 @@ +--- +layout: layout.njk +--- + +

+ {{ site.version }} · early days, no telemetry ever + +

Your own private corner
of the universe

+

Self-host the apps you actually rely on — on your own server. + One command brings up a whole platform, and your data never leaves your orbit.

+ +
+
+
~ one command, your whole server
+
+ bash <(curl -fsSL {{ site.installUrl }}) init + +
+
+
+ curl·to·shell, on a privacy tool? read it first → + · + or clone the repo +
+
+ +
explore
+
+ + +
+ What comes online +

A whole platform, handled for you

+

No yak-shaving. LibrePortal wires up the boring, fiddly infrastructure so you can run the good stuff.

+ +
+
+
+

One-click apps

+

Nextcloud, Vaultwarden, Jellyfin, Gitea and dozens more — picked from a menu, deployed clean.

+
+
+
+

Traefik + auto-SSL

+

A reverse proxy with automatic Let's Encrypt certificates. HTTPS everywhere, configured for you.

+
+
+
+

Rootless Docker

+

Containers run without root and with sane security defaults — CrowdSec on guard out of the box.

+
+
+
+

A real dashboard

+

Install, configure, back up and monitor everything from a clean web UI. No SSH archaeology.

+
+
+
+

Optional VPN routing

+

Send any app's traffic through gluetun. Keep the things that should be private, private.

+
+
+
+

No telemetry

+

Nothing phones home. No accounts required to self-host. The software is yours, entirely.

+
+
+
+ + +
+ The constellation +

Dozens of apps, one sky

+

Pulled straight from the repo — {{ apps.length }} apps you can light up from the dashboard, and switch off just as easily.

+ +
+ + {%- for c in categories %} + + {%- endfor %} +
+ +
+ {%- for app in apps %} + {% include "app-card.njk" %} + {%- endfor %} +
+

{{ apps.length }} apps

+
+ + +
+
+
+ LibrePortal Connect · optional +

We're the courier.
You hold the key.

+

Reaching your server from your phone and keeping off-site backups are the fiddly bits. + Connect handles them — but works like a courier carrying a sealed box.

+
    +
  • Reach it anywhere. No port-forwarding, no exposing your box to the internet.
  • +
  • Encrypted off-site backups. We only ever hold a locked box — you keep the only key.
  • +
  • We never run your apps. Your data and keys never leave your machine.
  • +
+ every part is free to self-host, too — you pay only for convenience +
+
+ +
+
+
+ + +
+ The promise +

Free software, and it stays that way

+

In plain language, so you can hold us to it.

+
+
100%

Every feature, free to self-host. Forever. No crippled edition.

+
0

Trackers. No telemetry, no phoning home, no accounts to run it.

+

Feature paywalls. You never pay to unlock — only for convenience.

+

What's open stays open. No rug-pulls, ever. AGPLv3.

+
+ Read the full Promise → +
+ + +
+ Ready when you are +

Light up your corner
of the universe.

+
+
+
~
+
+ bash <(curl -fsSL {{ site.installUrl }}) init + +
+
+
+
From 5d47a6bad508372929f201d25ff0a836aa1226bb Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 22 May 2026 00:45:01 +0100 Subject: [PATCH 2/5] fix(routing): honour HOST_NAME for app subdomain; add @ apex hosting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HOST_NAME was read but ignored — the FQDN was built from app_name, so 8 apps (vault, cloud, search, notes, social, meet, board, bookmark) routed at the wrong host and Traefik disagreed with DNS. Build host_setup from HOST_NAME (falling back to app_name); treat HOST_NAME="@"/"root" as the domain apex (root-of-domain hosting, previously impossible). Document @ in the Hostname field tooltip. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- scripts/network/variables/variables_init_app.sh | 11 ++++++++++- .../categories/webui_create_app_field_mappings.sh | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/network/variables/variables_init_app.sh b/scripts/network/variables/variables_init_app.sh index 9515519..df5e9ad 100755 --- a/scripts/network/variables/variables_init_app.sh +++ b/scripts/network/variables/variables_init_app.sh @@ -35,7 +35,16 @@ initializeAppVariables() app_title="${!app_title_var}" domain_var_name="CFG_DOMAIN_${domain}" domain_full="${!domain_var_name}" - host_setup=${app_name}.${domain_full} + # FQDN for this app's Traefik Host() rule (also feeds the app URL and + # trusted-domains): + # HOST_NAME="@" or "root" -> (apex / root of domain) + # HOST_NAME set -> . (custom subdomain) + # HOST_NAME empty -> . (fallback default) + if [[ "$host_name" == "@" || "$host_name" == "root" ]]; then + host_setup="${domain_full}" + else + host_setup="${host_name:-$app_name}.${domain_full}" + fi ssl_key=${domain_full}.key ssl_crt=${domain_full}.crt diff --git a/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh b/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh index c4e896a..8e29f3a 100755 --- a/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh +++ b/scripts/webui/data/generators/categories/webui_create_app_field_mappings.sh @@ -83,8 +83,8 @@ webuiCreateAppFieldMappings() { "category": "network", "label": "Hostname", "type": "text", - "tooltip": "The hostname for this application", - "placeholder": "myapp" + "tooltip": "Subdomain this app is served on — e.g. myapp becomes myapp.yourdomain.com. Use @ to serve it on the root of your domain (yourdomain.com itself).", + "placeholder": "myapp · @ = root domain" }, "DOMAIN": { "category": "network", From dec3055b630e8a67edd4d899feb2d67bf710efc3 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 22 May 2026 00:45:01 +0100 Subject: [PATCH 3/5] feat(routing): dynamic per-port subdomains + router-block toggle Replace the static one-host-per-app model with per-port routers: each Traefik-managed port carries a subdomain (12-col PORT format) and gets a DOMAINSUBNAME_TAG_ host, so one container can serve unlimited hosts. tagsProcessorPortSubdomains stamps per-port hosts (subdomain @/empty = apex, multi-level allowed); tagsProcessorPortRouterBlocks comments out # TRAEFIK_PORT__BEGIN/END blocks for non-Traefik ports so unfilled placeholders never ship (mirrors GLUETUN_OFF). Convert all 27 router apps (subdomains seeded from HOST_NAME; headscale admin. prefix -> subdomain). Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- containers/adguard/adguard.config | 2 +- containers/adguard/docker-compose.yml | 4 +- containers/authelia/authelia.config | 2 +- containers/authelia/docker-compose.yml | 6 +- containers/bookstack/bookstack.config | 2 +- containers/bookstack/docker-compose.yml | 6 +- containers/dashy/dashy.config | 2 +- containers/dashy/docker-compose.yml | 6 +- containers/focalboard/docker-compose.yml | 6 +- containers/focalboard/focalboard.config | 2 +- containers/gitea/docker-compose.yml | 6 +- containers/gitea/gitea.config | 2 +- containers/grafana/docker-compose.yml | 6 +- containers/grafana/grafana.config | 2 +- containers/headscale/docker-compose.yml | 6 +- containers/headscale/headscale.config | 2 +- containers/invidious/docker-compose.yml | 6 +- containers/invidious/invidious.config | 2 +- containers/ipinfo/docker-compose.yml | 6 +- containers/ipinfo/ipinfo.config | 2 +- containers/jellyfin/docker-compose.yml | 6 +- containers/jellyfin/jellyfin.config | 2 +- containers/jitsimeet/docker-compose.yml | 6 +- containers/jitsimeet/jitsimeet.config | 2 +- containers/libreportal/docker-compose.yml | 6 +- containers/libreportal/libreportal.config | 2 +- containers/linkding/docker-compose.yml | 6 +- containers/linkding/linkding.config | 2 +- containers/mastodon/docker-compose.yml | 6 +- containers/mastodon/mastodon.config | 2 +- containers/moneyapp/docker-compose.yml | 6 +- containers/moneyapp/moneyapp.config | 2 +- containers/nextcloud/docker-compose.yml | 6 +- containers/nextcloud/nextcloud.config | 2 +- containers/ollama/docker-compose.yml | 6 +- containers/ollama/ollama.config | 2 +- containers/onlyoffice/docker-compose.yml | 6 +- containers/onlyoffice/onlyoffice.config | 2 +- containers/owncloud/docker-compose.yml | 6 +- containers/owncloud/owncloud.config | 2 +- containers/pihole/docker-compose.yml | 6 +- containers/pihole/pihole.config | 2 +- containers/prometheus/docker-compose.yml | 6 +- containers/prometheus/prometheus.config | 2 +- containers/searxng/docker-compose.yml | 6 +- containers/searxng/searxng.config | 2 +- containers/speedtest/docker-compose.yml | 6 +- containers/speedtest/speedtest.config | 2 +- containers/traefik/docker-compose.yml | 4 +- containers/traefik/traefik.config | 2 +- containers/trilium/docker-compose.yml | 6 +- containers/trilium/trilium.config | 2 +- containers/vaultwarden/docker-compose.yml | 6 +- containers/vaultwarden/vaultwarden.config | 2 +- .../config/docker/docker_config_setup_data.sh | 9 ++ .../traefik/traefik_port_subdomains.sh | 96 +++++++++++++++++++ scripts/source/files/arrays/files_network.sh | 1 + 57 files changed, 239 insertions(+), 79 deletions(-) create mode 100644 scripts/network/traefik/traefik_port_subdomains.sh diff --git a/containers/adguard/adguard.config b/containers/adguard/adguard.config index 18ab8a8..8640fca 100755 --- a/containers/adguard/adguard.config +++ b/containers/adguard/adguard.config @@ -65,7 +65,7 @@ CFG_ADGUARD_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_ADGUARD_PORT_1="adguard-service|webui|random:3000|public|tcp|true|true|true|Admin Interface|" +CFG_ADGUARD_PORT_1="adguard-service|webui|random:3000|public|tcp|true|true|true|Admin Interface||adguard" CFG_ADGUARD_PORT_2="adguard-service|dns-tcp|random:53|public|tcp|false|false|false|DNS Server (TCP)|" CFG_ADGUARD_PORT_3="adguard-service|dns-udp|random:53|public|udp|false|false|false|DNS Server (UDP)|" CFG_ADGUARD_PORT_4="adguard-service|dns-alt|random:8053|disabled|tcp|false|false|false|Alternative DNS|" diff --git a/containers/adguard/docker-compose.yml b/containers/adguard/docker-compose.yml index 10c9bc5..f782fa9 100644 --- a/containers/adguard/docker-compose.yml +++ b/containers/adguard/docker-compose.yml @@ -20,13 +20,15 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.adguard-service-webui.entrypoints: web,websecure - traefik.http.routers.adguard-service-webui.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.adguard-service-webui.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.adguard-service-webui.service: adguard-service-webui traefik.http.routers.adguard-service-webui.tls: true traefik.http.routers.adguard-service-webui.tls.certresolver: production traefik.http.services.adguard-service-webui.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 traefik.http.routers.adguard-service-webui.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA volumes: diff --git a/containers/authelia/authelia.config b/containers/authelia/authelia.config index bd19af9..f66731d 100755 --- a/containers/authelia/authelia.config +++ b/containers/authelia/authelia.config @@ -71,4 +71,4 @@ CFG_AUTHELIA_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_AUTHELIA_PORT_1="authelia-service|webui|random:9091|public|tcp|false|true|true|Web Interface|" +CFG_AUTHELIA_PORT_1="authelia-service|webui|random:9091|public|tcp|false|true|true|Web Interface||authelia" diff --git a/containers/authelia/docker-compose.yml b/containers/authelia/docker-compose.yml index 65a0a8d..aabbaa8 100755 --- a/containers/authelia/docker-compose.yml +++ b/containers/authelia/docker-compose.yml @@ -24,12 +24,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.authelia-service.entrypoints: web,websecure - traefik.http.routers.authelia-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.authelia-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.authelia-service.tls: true traefik.http.routers.authelia-service.tls.certresolver: production traefik.http.services.authelia-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.authelia-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.authelia-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/bookstack/bookstack.config b/containers/bookstack/bookstack.config index e577ddf..5fbff8d 100644 --- a/containers/bookstack/bookstack.config +++ b/containers/bookstack/bookstack.config @@ -69,7 +69,7 @@ CFG_BOOKSTACK_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_BOOKSTACK_PORT_1="bookstack-service|webui|random:80|public|tcp|false|true|true|Web Interface|" +CFG_BOOKSTACK_PORT_1="bookstack-service|webui|random:80|public|tcp|false|true|true|Web Interface||bookstack" # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) CFG_BOOKSTACK_AUTH_PROFILE=multi_user diff --git a/containers/bookstack/docker-compose.yml b/containers/bookstack/docker-compose.yml index ebeafa5..5bb0f13 100755 --- a/containers/bookstack/docker-compose.yml +++ b/containers/bookstack/docker-compose.yml @@ -33,12 +33,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.bookstack-service.entrypoints: web,websecure - traefik.http.routers.bookstack-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.bookstack-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.bookstack-service.tls: true traefik.http.routers.bookstack-service.tls.certresolver: production traefik.http.services.bookstack-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.bookstack-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.bookstack-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: diff --git a/containers/dashy/dashy.config b/containers/dashy/dashy.config index 235942b..583c201 100755 --- a/containers/dashy/dashy.config +++ b/containers/dashy/dashy.config @@ -59,7 +59,7 @@ CFG_DASHY_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_DASHY_PORT_1="dashy-service|webui|random:8080|private|tcp|false|false|true|Dashboard|" +CFG_DASHY_PORT_1="dashy-service|webui|random:8080|private|tcp|false|false|true|Dashboard||dashy" # Comma-separated list of installed app slugs to surface as shortcuts on # the dashy dashboard. Managed via the Tools tab → "Manage Shortcuts". # Empty = no app shortcuts (only the static page header survives). diff --git a/containers/dashy/docker-compose.yml b/containers/dashy/docker-compose.yml index a67aff2..a63d37e 100755 --- a/containers/dashy/docker-compose.yml +++ b/containers/dashy/docker-compose.yml @@ -28,12 +28,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.dashy-service.entrypoints: web,websecure - traefik.http.routers.dashy-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.dashy-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.dashy-service.tls: true traefik.http.routers.dashy-service.tls.certresolver: production traefik.http.services.dashy-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.dashy-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.dashy-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: diff --git a/containers/focalboard/docker-compose.yml b/containers/focalboard/docker-compose.yml index e3c4152..7861f7e 100755 --- a/containers/focalboard/docker-compose.yml +++ b/containers/focalboard/docker-compose.yml @@ -23,12 +23,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.focalboard-service.entrypoints: web,websecure - traefik.http.routers.focalboard-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.focalboard-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.focalboard-service.tls: true traefik.http.routers.focalboard-service.tls.certresolver: production traefik.http.services.focalboard-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.focalboard-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.focalboard-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: diff --git a/containers/focalboard/focalboard.config b/containers/focalboard/focalboard.config index 21c4388..4357c2e 100755 --- a/containers/focalboard/focalboard.config +++ b/containers/focalboard/focalboard.config @@ -59,7 +59,7 @@ CFG_FOCALBOARD_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_FOCALBOARD_PORT_1="focalboard-service|webui|random:8000|public|tcp|false|true|true|Web Interface|" +CFG_FOCALBOARD_PORT_1="focalboard-service|webui|random:8000|public|tcp|false|true|true|Web Interface||board" # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) CFG_FOCALBOARD_AUTH_PROFILE=multi_user diff --git a/containers/gitea/docker-compose.yml b/containers/gitea/docker-compose.yml index e6869a3..702327c 100755 --- a/containers/gitea/docker-compose.yml +++ b/containers/gitea/docker-compose.yml @@ -65,12 +65,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.gitea-service.entrypoints: web,websecure - traefik.http.routers.gitea-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.gitea-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.gitea.tls: true traefik.http.routers.gitea.tls.certresolver: production traefik.http.services.gitea.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.gitea.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.gitea.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA gitea-cache: #LIBREPORTAL|SERVICE_TAG_2|gitea-cache diff --git a/containers/gitea/gitea.config b/containers/gitea/gitea.config index 8f9da82..332d26f 100755 --- a/containers/gitea/gitea.config +++ b/containers/gitea/gitea.config @@ -63,7 +63,7 @@ CFG_GITEA_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_GITEA_PORT_1="gitea-service|webui|random:3000|public|tcp|false|true|true|Web Interface|" +CFG_GITEA_PORT_1="gitea-service|webui|random:3000|public|tcp|false|true|true|Web Interface||gitea" CFG_GITEA_PORT_2="gitea-service|ssh|random:22|private|tcp|false|false|false|Git SSH Access|" # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) diff --git a/containers/grafana/docker-compose.yml b/containers/grafana/docker-compose.yml index 4329afe..7c518e9 100755 --- a/containers/grafana/docker-compose.yml +++ b/containers/grafana/docker-compose.yml @@ -24,12 +24,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.grafana-service.entrypoints: web,websecure - traefik.http.routers.grafana-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.grafana-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.grafana-service.tls: true traefik.http.routers.grafana-service.tls.certresolver: production traefik.http.services.grafana-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.grafana-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.grafana-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/grafana/grafana.config b/containers/grafana/grafana.config index 47226b8..b3e671a 100755 --- a/containers/grafana/grafana.config +++ b/containers/grafana/grafana.config @@ -64,4 +64,4 @@ CFG_GRAFANA_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_GRAFANA_PORT_1="grafana-service|webui|random:3000|public|tcp|false|true|true|Web Interface|" +CFG_GRAFANA_PORT_1="grafana-service|webui|random:3000|public|tcp|false|true|true|Web Interface||grafana" diff --git a/containers/headscale/docker-compose.yml b/containers/headscale/docker-compose.yml index ac5fc6f..fe36f52 100755 --- a/containers/headscale/docker-compose.yml +++ b/containers/headscale/docker-compose.yml @@ -56,9 +56,11 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA - traefik.http.routers.headscale-webui-service.rule: Host(`admin.DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + # TRAEFIK_PORT_2_BEGIN + traefik.http.routers.headscale-webui-service.rule: Host(`DOMAINSUBNAME_DATA_2`) #LIBREPORTAL|DOMAINSUBNAME_TAG_2|DOMAINSUBNAME_DATA_2 traefik.http.services.headscale-webui-service.loadbalancer.server.port: PORT_INTERNAL_DATA_2 #LIBREPORTAL|PORT_INTERNAL_TAG_2|PORT_INTERNAL_DATA_2 - traefik.http.routers.headscale-webui-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.headscale-webui-service.middlewares: MIDDLEWARE_DATA_2 #LIBREPORTAL|MIDDLEWARE_TAG_2|MIDDLEWARE_DATA_2 + # TRAEFIK_PORT_2_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA ports: - "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2 diff --git a/containers/headscale/headscale.config b/containers/headscale/headscale.config index 234c0d2..07483f1 100755 --- a/containers/headscale/headscale.config +++ b/containers/headscale/headscale.config @@ -60,4 +60,4 @@ CFG_HEADSCALE_NETWORK=default # - description: human-readable description of the service # CFG_HEADSCALE_PORT_1="headscale-service|api|random:8080|private|tcp|false|false|false|Headscale API Server|" -CFG_HEADSCALE_PORT_2="headscale-webui-service|webui|random:5000|private|tcp|false|true|true|Web UI|" +CFG_HEADSCALE_PORT_2="headscale-webui-service|webui|random:5000|private|tcp|false|true|true|Web UI||admin.headscale" diff --git a/containers/invidious/docker-compose.yml b/containers/invidious/docker-compose.yml index 43e11ab..1aba516 100644 --- a/containers/invidious/docker-compose.yml +++ b/containers/invidious/docker-compose.yml @@ -43,12 +43,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.invidious-service.entrypoints: web,websecure - traefik.http.routers.invidious-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.invidious-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.invidious-service.tls: true traefik.http.routers.invidious-service.tls.certresolver: production traefik.http.services.invidious-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.invidious-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.invidious-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: diff --git a/containers/invidious/invidious.config b/containers/invidious/invidious.config index 8522012..2098831 100755 --- a/containers/invidious/invidious.config +++ b/containers/invidious/invidious.config @@ -62,7 +62,7 @@ CFG_INVIDIOUS_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_INVIDIOUS_PORT_1="invidious-service|webui|random:3000|public|tcp|false|true|true|Web Interface|" +CFG_INVIDIOUS_PORT_1="invidious-service|webui|random:3000|public|tcp|false|true|true|Web Interface||invidious" # AUTH_PROFILE = capability tier for the WebUI auth tools (single_password | user_password | multi_user) CFG_INVIDIOUS_AUTH_PROFILE=multi_user diff --git a/containers/ipinfo/docker-compose.yml b/containers/ipinfo/docker-compose.yml index 25c785c..c79384a 100755 --- a/containers/ipinfo/docker-compose.yml +++ b/containers/ipinfo/docker-compose.yml @@ -17,12 +17,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.ipinfo-service.entrypoints: web,websecure - traefik.http.routers.ipinfo-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.ipinfo-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.ipinfo-service.tls: true traefik.http.routers.ipinfo-service.tls.certresolver: production traefik.http.services.ipinfo-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.ipinfo-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.ipinfo-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/ipinfo/ipinfo.config b/containers/ipinfo/ipinfo.config index 9c0be28..cea64f0 100755 --- a/containers/ipinfo/ipinfo.config +++ b/containers/ipinfo/ipinfo.config @@ -59,4 +59,4 @@ CFG_IPINFO_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_IPINFO_PORT_1="ipinfo-service|webui|random:8080|public|tcp|false|true|true|Web Interface|" +CFG_IPINFO_PORT_1="ipinfo-service|webui|random:8080|public|tcp|false|true|true|Web Interface||ipinfo" diff --git a/containers/jellyfin/docker-compose.yml b/containers/jellyfin/docker-compose.yml index 0827078..f62ad95 100755 --- a/containers/jellyfin/docker-compose.yml +++ b/containers/jellyfin/docker-compose.yml @@ -21,12 +21,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.jellyfin-service.entrypoints: web,websecure - traefik.http.routers.jellyfin-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.jellyfin-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.jellyfin-service.tls: true traefik.http.routers.jellyfin-service.tls.certresolver: production traefik.http.services.jellyfin-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.jellyfin-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.jellyfin-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/jellyfin/jellyfin.config b/containers/jellyfin/jellyfin.config index 99947cd..52a4dab 100755 --- a/containers/jellyfin/jellyfin.config +++ b/containers/jellyfin/jellyfin.config @@ -59,4 +59,4 @@ CFG_JELLYFIN_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_JELLYFIN_PORT_1="jellyfin-service|webui|random:8096|public|tcp|false|true|true|Media Server|" +CFG_JELLYFIN_PORT_1="jellyfin-service|webui|random:8096|public|tcp|false|true|true|Media Server||jellyfin" diff --git a/containers/jitsimeet/docker-compose.yml b/containers/jitsimeet/docker-compose.yml index cf05fe5..553da92 100644 --- a/containers/jitsimeet/docker-compose.yml +++ b/containers/jitsimeet/docker-compose.yml @@ -23,12 +23,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.jitsimeet-service.entrypoints: web,websecure - traefik.http.routers.jitsimeet-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.jitsimeet-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.jitsimeet-service.tls: true traefik.http.routers.jitsimeet-service.tls.certresolver: production traefik.http.services.jitsimeet-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.jitsimeet-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.jitsimeet-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA networks: DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA diff --git a/containers/jitsimeet/jitsimeet.config b/containers/jitsimeet/jitsimeet.config index 497cfb5..e736d8c 100755 --- a/containers/jitsimeet/jitsimeet.config +++ b/containers/jitsimeet/jitsimeet.config @@ -59,6 +59,6 @@ CFG_JITSIMEET_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_JITSIMEET_PORT_1="jitsimeet-service|webui|random:80|public|tcp|false|true|true|Web Interface|" +CFG_JITSIMEET_PORT_1="jitsimeet-service|webui|random:80|public|tcp|false|true|true|Web Interface||meet" CFG_JITSIMEET_PORT_2="jitsimeet-jvb|video-bridge|random:10000|public|udp|false|false|false|Jitsi Video Bridge (UDP)|" CFG_JITSIMEET_PORT_3="jitsimeet-jvb|video-tcp|random:30300|public|tcp|false|false|false|Jitsi Video Bridge (TCP)|" diff --git a/containers/libreportal/docker-compose.yml b/containers/libreportal/docker-compose.yml index 1787080..c5e4f2a 100644 --- a/containers/libreportal/docker-compose.yml +++ b/containers/libreportal/docker-compose.yml @@ -36,12 +36,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.libreportal-service.entrypoints: web,websecure - traefik.http.routers.libreportal-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.libreportal-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.libreportal-service.tls: true traefik.http.routers.libreportal-service.tls.certresolver: production traefik.http.services.libreportal-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.libreportal-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.libreportal-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/libreportal/libreportal.config b/containers/libreportal/libreportal.config index d718aff..242f991 100755 --- a/containers/libreportal/libreportal.config +++ b/containers/libreportal/libreportal.config @@ -59,4 +59,4 @@ CFG_LIBREPORTAL_HOST_NAME=libreportal # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_LIBREPORTAL_PORT_1="libreportal-service|webui|random:1111|public|tcp|false|false|true|Web Interface|" +CFG_LIBREPORTAL_PORT_1="libreportal-service|webui|random:1111|public|tcp|false|false|true|Web Interface||libreportal" diff --git a/containers/linkding/docker-compose.yml b/containers/linkding/docker-compose.yml index d42ed70..32056ac 100755 --- a/containers/linkding/docker-compose.yml +++ b/containers/linkding/docker-compose.yml @@ -19,12 +19,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.linkding-service.entrypoints: web,websecure - traefik.http.routers.linkding-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.linkding-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.linkding-service.tls: true traefik.http.routers.linkding-service.tls.certresolver: production traefik.http.services.linkding-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.linkding-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.linkding-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/linkding/linkding.config b/containers/linkding/linkding.config index bdf57a8..6153d79 100755 --- a/containers/linkding/linkding.config +++ b/containers/linkding/linkding.config @@ -59,4 +59,4 @@ CFG_LINKDING_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_LINKDING_PORT_1="linkding-service|webui|random:9090|public|tcp|false|true|true|Web Interface|" +CFG_LINKDING_PORT_1="linkding-service|webui|random:9090|public|tcp|false|true|true|Web Interface||bookmark" diff --git a/containers/mastodon/docker-compose.yml b/containers/mastodon/docker-compose.yml index ead6145..fcadaa0 100755 --- a/containers/mastodon/docker-compose.yml +++ b/containers/mastodon/docker-compose.yml @@ -37,12 +37,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.mastodon-service.entrypoints: web,websecure - traefik.http.routers.mastodon-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.mastodon-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.mastodon-service.tls: true traefik.http.routers.mastodon-service.tls.certresolver: production traefik.http.services.mastodon-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.mastodon-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.mastodon-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA command: bash -c "RAILS_ENV=production bundle exec rails db:migrate && RAILS_ENV=production bundle exec rails s -b 0.0.0.0" healthcheck: diff --git a/containers/mastodon/mastodon.config b/containers/mastodon/mastodon.config index b1c0075..33e49e8 100755 --- a/containers/mastodon/mastodon.config +++ b/containers/mastodon/mastodon.config @@ -59,5 +59,5 @@ CFG_MASTODON_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_MASTODON_PORT_1="mastodon-service|webui|random:3010|public|tcp|false|false|true|Web Interface|" +CFG_MASTODON_PORT_1="mastodon-service|webui|random:3010|public|tcp|false|false|true|Web Interface||social" CFG_MASTODON_PORT_2="mastodon-service|streaming|random:4010|public|tcp|false|false|false|Mastodon Streaming API|" diff --git a/containers/moneyapp/docker-compose.yml b/containers/moneyapp/docker-compose.yml index b42fbd9..18b3811 100644 --- a/containers/moneyapp/docker-compose.yml +++ b/containers/moneyapp/docker-compose.yml @@ -31,12 +31,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.moneyapp-service.entrypoints: web,websecure - traefik.http.routers.moneyapp-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.moneyapp-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.moneyapp-service.tls: true traefik.http.routers.moneyapp-service.tls.certresolver: production traefik.http.services.moneyapp-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.moneyapp-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.moneyapp-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: diff --git a/containers/moneyapp/moneyapp.config b/containers/moneyapp/moneyapp.config index 3eee968..ca1d247 100644 --- a/containers/moneyapp/moneyapp.config +++ b/containers/moneyapp/moneyapp.config @@ -55,4 +55,4 @@ CFG_MONEYAPP_NETWORK=default # Schema (10 cols): service|name|external:internal|access|protocol|login|traefik|button_enabled|button_text|url_path # button_text + url_path may carry comma-separated parallel arrays for multi-button entries. # -CFG_MONEYAPP_PORT_1="moneyapp-service|webui|random:3000|public|tcp|false|true|true|MoneyApp|/" +CFG_MONEYAPP_PORT_1="moneyapp-service|webui|random:3000|public|tcp|false|true|true|MoneyApp|/|moneyapp" diff --git a/containers/nextcloud/docker-compose.yml b/containers/nextcloud/docker-compose.yml index b191e05..c95492e 100644 --- a/containers/nextcloud/docker-compose.yml +++ b/containers/nextcloud/docker-compose.yml @@ -32,12 +32,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.nextcloud-service.entrypoints: web,websecure - traefik.http.routers.nextcloud-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.nextcloud-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.nextcloud-service.tls: true traefik.http.routers.nextcloud-service.tls.certresolver: production traefik.http.services.nextcloud-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.nextcloud-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.nextcloud-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA # GLUETUN_OFF_BEGIN networks: diff --git a/containers/nextcloud/nextcloud.config b/containers/nextcloud/nextcloud.config index 5be79a2..ea1bc42 100755 --- a/containers/nextcloud/nextcloud.config +++ b/containers/nextcloud/nextcloud.config @@ -71,5 +71,5 @@ CFG_NEXTCLOUD_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_NEXTCLOUD_PORT_1="nextcloud-service|webui|random:80|public|tcp|false|true|true|Web Interface|" +CFG_NEXTCLOUD_PORT_1="nextcloud-service|webui|random:80|public|tcp|false|true|true|Web Interface||nextcloud" CFG_NEXTCLOUD_PORT_2="nextcloud-exporter|metrics|9205:9205|disabled|tcp|false|false|false|Metrics Exporter (sidecar, docker-network only)|" diff --git a/containers/ollama/docker-compose.yml b/containers/ollama/docker-compose.yml index eb3f58d..356ff63 100755 --- a/containers/ollama/docker-compose.yml +++ b/containers/ollama/docker-compose.yml @@ -19,12 +19,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.ollama-webui.entrypoints: web,websecure - traefik.http.routers.ollama-webui.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.ollama-webui.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.ollama-webui.tls: true traefik.http.routers.ollama-webui.tls.certresolver: production traefik.http.services.ollama-webui.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.ollama-webui.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.ollama-webui.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA networks: diff --git a/containers/ollama/ollama.config b/containers/ollama/ollama.config index 7517953..a7e8740 100755 --- a/containers/ollama/ollama.config +++ b/containers/ollama/ollama.config @@ -61,6 +61,6 @@ CFG_OLLAMA_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_OLLAMA_PORT_1="ollama-webui|webui|random:8080|public|tcp|false|true|true|Web Interface|" +CFG_OLLAMA_PORT_1="ollama-webui|webui|random:8080|public|tcp|false|true|true|Web Interface||ollama" CFG_OLLAMA_PORT_2="ollama-service|api|random:11434|private|tcp|false|false|false|Ollama API|" CFG_OLLAMA_PORT_3="ollama-exporter|metrics|9778:9778|disabled|tcp|false|false|false|Metrics Exporter (sidecar, docker-network only)|" diff --git a/containers/onlyoffice/docker-compose.yml b/containers/onlyoffice/docker-compose.yml index 50be69c..a1f2331 100755 --- a/containers/onlyoffice/docker-compose.yml +++ b/containers/onlyoffice/docker-compose.yml @@ -18,15 +18,17 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.onlyoffice-service.entrypoints: web,websecure - traefik.http.routers.onlyoffice-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.onlyoffice-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.onlyoffice-service.tls: true traefik.http.routers.onlyoffice-service.tls.certresolver: production traefik.http.services.onlyoffice-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.onlyoffice-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.onlyoffice-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 # Headers for onlyoffice, https://github.com/ONLYOFFICE/onlyoffice-nextcloud/issues/151 traefik.http.middlewares.onlyoffice-headers.headers.customrequestheaders.X-Forwarded-Proto: https traefik.http.middlewares.onlyoffice-headers.headers.accesscontrolalloworiginlist: "*" + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA volumes: diff --git a/containers/onlyoffice/onlyoffice.config b/containers/onlyoffice/onlyoffice.config index db772a8..8d0c6a3 100755 --- a/containers/onlyoffice/onlyoffice.config +++ b/containers/onlyoffice/onlyoffice.config @@ -59,4 +59,4 @@ CFG_ONLYOFFICE_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_ONLYOFFICE_PORT_1="onlyoffice-service|webui|random:80|public|tcp|false|true|true|Web Interface|" +CFG_ONLYOFFICE_PORT_1="onlyoffice-service|webui|random:80|public|tcp|false|true|true|Web Interface||onlyoffice" diff --git a/containers/owncloud/docker-compose.yml b/containers/owncloud/docker-compose.yml index 73b24cd..3200e19 100755 --- a/containers/owncloud/docker-compose.yml +++ b/containers/owncloud/docker-compose.yml @@ -33,12 +33,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.owncloud-service.entrypoints: web,websecure - traefik.http.routers.owncloud-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.owncloud-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.owncloud-service.tls: true traefik.http.routers.owncloud-service.tls.certresolver: production traefik.http.services.owncloud-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.owncloud-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.owncloud-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: test: ["CMD", "/usr/bin/healthcheck"] diff --git a/containers/owncloud/owncloud.config b/containers/owncloud/owncloud.config index 1be7993..460e848 100755 --- a/containers/owncloud/owncloud.config +++ b/containers/owncloud/owncloud.config @@ -69,4 +69,4 @@ CFG_OWNCLOUD_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_OWNCLOUD_PORT_1="owncloud-service|webui|random:8080|public|tcp|false|true|true|Web Interface|" +CFG_OWNCLOUD_PORT_1="owncloud-service|webui|random:8080|public|tcp|false|true|true|Web Interface||cloud" diff --git a/containers/pihole/docker-compose.yml b/containers/pihole/docker-compose.yml index 32a7257..4258987 100755 --- a/containers/pihole/docker-compose.yml +++ b/containers/pihole/docker-compose.yml @@ -20,12 +20,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.pihole-service.entrypoints: web,websecure - traefik.http.routers.pihole-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.pihole-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.pihole-service.tls: true traefik.http.routers.pihole-service.tls.certresolver: production traefik.http.services.pihole-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.pihole-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.pihole-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END environment: - FTLCONF_LOCAL_IPV4=PUBLIC_IP_DATA #LIBREPORTAL|PUBLIC_IP_TAG|PUBLIC_IP_DATA - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA diff --git a/containers/pihole/pihole.config b/containers/pihole/pihole.config index 3f259a8..0b3e502 100755 --- a/containers/pihole/pihole.config +++ b/containers/pihole/pihole.config @@ -70,7 +70,7 @@ CFG_PIHOLE_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_PIHOLE_PORT_1="pihole-service|webui|random:80|private|tcp|false|false|true|Admin Interface|/admin/" +CFG_PIHOLE_PORT_1="pihole-service|webui|random:80|private|tcp|false|false|true|Admin Interface|/admin/|pihole" CFG_PIHOLE_PORT_2="pihole-service|dns-tcp|53:53|private|tcp|false|false|false|DNS Server (TCP)|" CFG_PIHOLE_PORT_3="pihole-service|dns-udp|53:53|private|udp|false|false|false|DNS Server (UDP)|" CFG_PIHOLE_PORT_4="pihole-service|https|random:443|disabled|tcp|false|false|false|HTTPS Interface|" diff --git a/containers/prometheus/docker-compose.yml b/containers/prometheus/docker-compose.yml index 182d391..e9956bc 100755 --- a/containers/prometheus/docker-compose.yml +++ b/containers/prometheus/docker-compose.yml @@ -21,12 +21,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.prometheus-service.entrypoints: web,websecure - traefik.http.routers.prometheus-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.prometheus-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.prometheus-service.tls: true traefik.http.routers.prometheus-service.tls.certresolver: production traefik.http.services.prometheus-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.prometheus-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.prometheus-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA volumes: diff --git a/containers/prometheus/prometheus.config b/containers/prometheus/prometheus.config index 99ac99f..9da5b40 100755 --- a/containers/prometheus/prometheus.config +++ b/containers/prometheus/prometheus.config @@ -59,4 +59,4 @@ CFG_PROMETHEUS_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_PROMETHEUS_PORT_1="prometheus-service|webui|random:9090|public|tcp|false|true|true|Web Interface|" +CFG_PROMETHEUS_PORT_1="prometheus-service|webui|random:9090|public|tcp|false|true|true|Web Interface||prometheus" diff --git a/containers/searxng/docker-compose.yml b/containers/searxng/docker-compose.yml index 10e08a3..e48909b 100755 --- a/containers/searxng/docker-compose.yml +++ b/containers/searxng/docker-compose.yml @@ -16,12 +16,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.searxng-service.entrypoints: web,websecure - traefik.http.routers.searxng-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.searxng-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.searxng-service.tls: true traefik.http.routers.searxng-service.tls.certresolver: production traefik.http.services.searxng-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.searxng-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.searxng-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END cap_drop: - ALL cap_add: diff --git a/containers/searxng/searxng.config b/containers/searxng/searxng.config index 1ed7dc3..8b3547f 100755 --- a/containers/searxng/searxng.config +++ b/containers/searxng/searxng.config @@ -66,4 +66,4 @@ CFG_SEARXNG_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_SEARXNG_PORT_1="searxng-service|webui|8083:8080|public|tcp|false|true|true|Search Interface|" +CFG_SEARXNG_PORT_1="searxng-service|webui|8083:8080|public|tcp|false|true|true|Search Interface||search" diff --git a/containers/speedtest/docker-compose.yml b/containers/speedtest/docker-compose.yml index 5ce3754..969b841 100644 --- a/containers/speedtest/docker-compose.yml +++ b/containers/speedtest/docker-compose.yml @@ -24,12 +24,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.speedtest-service.entrypoints: web,websecure - traefik.http.routers.speedtest-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.speedtest-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.speedtest-service.tls: true traefik.http.routers.speedtest-service.tls.certresolver: production traefik.http.services.speedtest-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.speedtest-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.speedtest-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA volumes: diff --git a/containers/speedtest/speedtest.config b/containers/speedtest/speedtest.config index d55ccb2..afb5df6 100755 --- a/containers/speedtest/speedtest.config +++ b/containers/speedtest/speedtest.config @@ -67,4 +67,4 @@ CFG_SPEEDTEST_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_SPEEDTEST_PORT_1="speedtest-service|webui|random:80|public|tcp|false|true|true|Speedtest,Results|/,/results/stats.php" +CFG_SPEEDTEST_PORT_1="speedtest-service|webui|random:80|public|tcp|false|true|true|Speedtest,Results|/,/results/stats.php|speedtest" diff --git a/containers/traefik/docker-compose.yml b/containers/traefik/docker-compose.yml index cccd4bd..75cd173 100644 --- a/containers/traefik/docker-compose.yml +++ b/containers/traefik/docker-compose.yml @@ -22,13 +22,15 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.traefik-service-dashboard.entrypoints: web,websecure - traefik.http.routers.traefik-service-dashboard.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.traefik-service-dashboard.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.traefik-service-dashboard.service: api@internal traefik.http.routers.traefik-service-dashboard.tls: true traefik.http.routers.traefik-service-dashboard.tls.certresolver: production traefik.http.routers.traefik-service-dashboard.tls.options: modern@file traefik.http.routers.traefik-service-dashboard.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA diff --git a/containers/traefik/traefik.config b/containers/traefik/traefik.config index be8edb1..5967bb2 100755 --- a/containers/traefik/traefik.config +++ b/containers/traefik/traefik.config @@ -80,7 +80,7 @@ CFG_TRAEFIK_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_TRAEFIK_PORT_1="traefik-service|dashboard|random:8080|public|tcp|true|true|true|Admin Dashboard|" +CFG_TRAEFIK_PORT_1="traefik-service|dashboard|random:8080|public|tcp|true|true|true|Admin Dashboard||traefik" CFG_TRAEFIK_PORT_2="traefik-service|https|443:443|public|tcp|false|false|false|HTTPS Traffic|" CFG_TRAEFIK_PORT_3="traefik-service|http|80:80|disabled|tcp|false|false|false|HTTP Traffic|" diff --git a/containers/trilium/docker-compose.yml b/containers/trilium/docker-compose.yml index e7c7338..be3b544 100755 --- a/containers/trilium/docker-compose.yml +++ b/containers/trilium/docker-compose.yml @@ -16,12 +16,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.trilium-service.entrypoints: web,websecure - traefik.http.routers.trilium-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.trilium-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.trilium-service.tls: true traefik.http.routers.trilium-service.tls.certresolver: production traefik.http.services.trilium-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.trilium-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.trilium-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA volumes: diff --git a/containers/trilium/trilium.config b/containers/trilium/trilium.config index b59b70e..2a2d301 100755 --- a/containers/trilium/trilium.config +++ b/containers/trilium/trilium.config @@ -59,4 +59,4 @@ CFG_TRILIUM_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_TRILIUM_PORT_1="trilium-service|webui|random:8080|private|tcp|false|true|true|Notes Interface|" +CFG_TRILIUM_PORT_1="trilium-service|webui|random:8080|private|tcp|false|true|true|Notes Interface||notes" diff --git a/containers/vaultwarden/docker-compose.yml b/containers/vaultwarden/docker-compose.yml index 934efce..d306d3b 100755 --- a/containers/vaultwarden/docker-compose.yml +++ b/containers/vaultwarden/docker-compose.yml @@ -26,12 +26,14 @@ services: libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN traefik.http.routers.vaultwarden-service.entrypoints: web,websecure - traefik.http.routers.vaultwarden-service.rule: Host(`DOMAINSUBNAME_DATA`) #LIBREPORTAL|DOMAINSUBNAME_TAG|DOMAINSUBNAME_DATA + traefik.http.routers.vaultwarden-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 traefik.http.routers.vaultwarden-service.tls: true traefik.http.routers.vaultwarden-service.tls.certresolver: production traefik.http.services.vaultwarden-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 - traefik.http.routers.vaultwarden-service.middlewares: MIDDLEWARE_DATA #LIBREPORTAL|MIDDLEWARE_TAG|MIDDLEWARE_DATA + traefik.http.routers.vaultwarden-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END healthcheck: disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA # GLUETUN_OFF_BEGIN diff --git a/containers/vaultwarden/vaultwarden.config b/containers/vaultwarden/vaultwarden.config index 06955fb..4df0594 100755 --- a/containers/vaultwarden/vaultwarden.config +++ b/containers/vaultwarden/vaultwarden.config @@ -70,5 +70,5 @@ CFG_VAULTWARDEN_NETWORK=default # - webui: if true, this port serves the main web interface # - description: human-readable description of the service # -CFG_VAULTWARDEN_PORT_1="vaultwarden-service|webui|8201:80|public|tcp|false|true|true|Password Manager Interface|" +CFG_VAULTWARDEN_PORT_1="vaultwarden-service|webui|8201:80|public|tcp|false|true|true|Password Manager Interface||vault" CFG_VAULTWARDEN_PORT_2="vaultwarden-exporter|metrics|3001:3001|disabled|tcp|false|false|false|Metrics Exporter (sidecar, docker-network only)|" diff --git a/scripts/config/docker/docker_config_setup_data.sh b/scripts/config/docker/docker_config_setup_data.sh index 6010133..8515916 100755 --- a/scripts/config/docker/docker_config_setup_data.sh +++ b/scripts/config/docker/docker_config_setup_data.sh @@ -60,6 +60,15 @@ dockerConfigSetupFileWithData() # and Authelia takes precedence when installed. tagsProcessorPortMiddlewares "$full_file_path" "$app_name" + # Per-port subdomains (DOMAINSUBNAME_TAG_1, _2, ...). The dynamic + # replacement for the single static HOST_NAME — one host per + # Traefik-managed port, so a container can serve unlimited hosts. + tagsProcessorPortSubdomains "$full_file_path" "$app_name" + + # Strip (comment out) router blocks for ports that aren't Traefik-managed, + # so an unfilled DOMAINSUBNAME_DATA_ placeholder can never ship. + tagsProcessorPortRouterBlocks "$full_file_path" "$app_name" + tagsProcessorTraefikControl "$full_file_path" "$public" tagsManagerUpdateUniversalTag "$full_file_path" "DOMAINSUBNAME_TAG" "$host_setup" diff --git a/scripts/network/traefik/traefik_port_subdomains.sh b/scripts/network/traefik/traefik_port_subdomains.sh new file mode 100644 index 0000000..dd498eb --- /dev/null +++ b/scripts/network/traefik/traefik_port_subdomains.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Per-port Traefik subdomain/host writer. +# +# Companion to traefik_port_middlewares.sh. Where that stamps a per-port +# MIDDLEWARE_TAG_, this stamps a per-port DOMAINSUBNAME_TAG_ holding the +# fully-qualified host for that port's Traefik router, derived from the port's +# `subdomain` column (the 11th field of the 12-col PORT format). +# +# This is the dynamic replacement for the old static, per-app +# CFG__HOST_NAME model: a single container can now expose any number of +# hosts — one per Traefik-managed port — instead of being limited to one. +# +# subdomain "" | "@" | "root" -> (apex / root of domain) +# subdomain "vault" -> vault. (single label) +# subdomain "admin.headscale" -> admin.headscale. (multi-level allowed) +# +# Index scheme matches traefik_port_middlewares.sh exactly: i walks every +# parsed port (0-based), idx = i+1 is the 1-based position used in the tag +# suffix, and only Traefik-managed ports (traefik=true) are stamped — so a +# port's MIDDLEWARE_TAG_ and DOMAINSUBNAME_TAG_ always line up on the +# same router. +tagsProcessorPortSubdomains() +{ + local file="$1" + local app_name="$2" + + if [[ -z "$file" || -z "$app_name" ]]; then + return 0 + fi + + local count=${#port_service_names[@]} + local i=0 + while [[ $i -lt $count ]]; do + local idx=$((i + 1)) + local p_traefik="${port_traefik_managed[$i]}" + local p_sub="${port_subdomains[$i]}" + + if [[ "$p_traefik" == "true" ]]; then + local host + if [[ -z "$p_sub" || "$p_sub" == "@" || "$p_sub" == "root" ]]; then + host="${domain_full}" # apex / root of the domain + else + host="${p_sub}.${domain_full}" # subdomain (single or multi-level) + fi + tagsManagerUpdateUniversalTag "$file" "DOMAINSUBNAME_TAG_${idx}" "$host" + fi + i=$((i + 1)) + done +} + +# Comments out the Traefik router block for any port that is NOT Traefik-managed +# (non-public, metrics-only, or a slot with no router at all), so an unfilled +# DOMAINSUBNAME_DATA_ placeholder can never reach a live compose. Each router +# is wrapped in # TRAEFIK_PORT__BEGIN ... # TRAEFIK_PORT__END markers; +# this mirrors the GLUETUN_OFF region-commenting in tags_processor_network_mode.sh. +tagsProcessorPortRouterBlocks() +{ + local file="$1" + local app_name="$2" + if [[ -z "$file" || ! -f "$file" ]]; then + return 0 + fi + + # Space-padded list of 1-based indices of Traefik-managed ports, e.g. " 1 3 ". + local active=" " + local count=${#port_service_names[@]} + local i=0 + while [[ $i -lt $count ]]; do + [[ "${port_traefik_managed[$i]}" == "true" ]] && active+="$((i + 1)) " + i=$((i + 1)) + done + + local tmp="${file}.routers.$$" + sudo awk -v active="$active" ' + BEGIN { off = 0 } + /#[[:space:]]*TRAEFIK_PORT_[0-9]+_BEGIN/ { + match($0, /TRAEFIK_PORT_[0-9]+/); key = substr($0, RSTART, RLENGTH) + sub(/TRAEFIK_PORT_/, "", key) + off = (index(active, " " key " ") > 0) ? 0 : 1 # off=1 -> comment the block + print; next + } + /#[[:space:]]*TRAEFIK_PORT_[0-9]+_END/ { off = 0; print; next } + { + if (off == 1 && $0 !~ /^[[:space:]]*#/) { + match($0, /^[[:space:]]*/) + indent = substr($0, RSTART, RLENGTH) + rest = substr($0, RLENGTH + 1) + print indent "# " rest + next + } + print + } + ' "$file" | sudo tee "$tmp" >/dev/null + sudo mv "$tmp" "$file" +} diff --git a/scripts/source/files/arrays/files_network.sh b/scripts/source/files/arrays/files_network.sh index 3283f85..14bebe3 100755 --- a/scripts/source/files/arrays/files_network.sh +++ b/scripts/source/files/arrays/files_network.sh @@ -38,6 +38,7 @@ network_scripts=( "network/traefik/traefik_login_credentials.sh" "network/traefik/traefik_middlewares.sh" "network/traefik/traefik_port_middlewares.sh" + "network/traefik/traefik_port_subdomains.sh" "network/traefik/traefik_whitelist.sh" "network/variables/basic_scan.sh" "network/variables/headscale_variables.sh" From 149fce835ed56a2c48d105b5d5a2a8d62f0158e4 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 22 May 2026 01:10:56 +0100 Subject: [PATCH 4/5] fix(routing): per-port subdomain falls back to app-name when unset An empty subdomain previously resolved to the domain apex, which would collide on the root for any unconfigured Traefik port. Treat empty as the app-name default (matching legacy behaviour); apex is reachable only via the explicit @ / root sentinel. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- scripts/network/traefik/traefik_port_subdomains.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/network/traefik/traefik_port_subdomains.sh b/scripts/network/traefik/traefik_port_subdomains.sh index dd498eb..52fa315 100644 --- a/scripts/network/traefik/traefik_port_subdomains.sh +++ b/scripts/network/traefik/traefik_port_subdomains.sh @@ -38,10 +38,12 @@ tagsProcessorPortSubdomains() if [[ "$p_traefik" == "true" ]]; then local host - if [[ -z "$p_sub" || "$p_sub" == "@" || "$p_sub" == "root" ]]; then - host="${domain_full}" # apex / root of the domain - else + if [[ "$p_sub" == "@" || "$p_sub" == "root" ]]; then + host="${domain_full}" # apex / root of the domain (explicit only) + elif [[ -n "$p_sub" ]]; then host="${p_sub}.${domain_full}" # subdomain (single or multi-level) + else + host="${app_name}.${domain_full}" # no subdomain set -> app-name default fi tagsManagerUpdateUniversalTag "$file" "DOMAINSUBNAME_TAG_${idx}" "$host" fi From e5f6f4c37192ec9dd427532d15f25e7c69bcd983 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 22 May 2026 01:10:56 +0100 Subject: [PATCH 5/5] feat(dns): split-horizon local DNS for app subdomains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setupLocalDnsRewrites points every configured domain at the server's LAN IP inside the self-hosted resolver, so app subdomains resolve locally and hit Traefik directly (valid certs, no router hairpin). AdGuard gets a wildcard rewrite per domain via its REST API; Pi-hole gets per-host A records in the supported, mounted custom.list (no wildcard support there). Safe by construction: idempotent, guarded by installed-checks, cannot corrupt the resolver. Hooked into the Apply-DNS actions and resolver install. Also drops the dead HOST_NAME read from the setupDNSIP stub. NOTE: needs a live smoke-test — the AdGuard API call and Pi-hole reload can't be exercised without the running containers. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- scripts/app/app_update_specifics.sh | 2 + .../adguard/adguard_apply_dns_updater.sh | 2 + .../pihole/pihole_apply_dns_updater.sh | 2 + scripts/network/dns/setup_dns_ip.sh | 13 +- scripts/network/dns/setup_local_dns.sh | 140 ++++++++++++++++++ scripts/source/files/arrays/files_network.sh | 1 + 6 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 scripts/network/dns/setup_local_dns.sh diff --git a/scripts/app/app_update_specifics.sh b/scripts/app/app_update_specifics.sh index fafaa74..4a796d3 100755 --- a/scripts/app/app_update_specifics.sh +++ b/scripts/app/app_update_specifics.sh @@ -11,6 +11,8 @@ appUpdateSpecifics() if [[ $CFG_REQUIREMENT_DNS_UPDATER == "true" ]]; then updateDNS $app_name install; fi + # Split-horizon local DNS: app subdomains resolve to the box on the LAN. + declare -F setupLocalDnsRewrites >/dev/null 2>&1 && setupLocalDnsRewrites fi if [[ $app_name == "libreportal" ]]; then diff --git a/scripts/app/containers/adguard/adguard_apply_dns_updater.sh b/scripts/app/containers/adguard/adguard_apply_dns_updater.sh index 92154ed..1e1f79a 100644 --- a/scripts/app/containers/adguard/adguard_apply_dns_updater.sh +++ b/scripts/app/containers/adguard/adguard_apply_dns_updater.sh @@ -9,4 +9,6 @@ appAdguardApplyDnsUpdater() fi updateDNS "adguard" "manual" isSuccessful "/etc/resolv.conf updated to use AdGuard as the host DNS resolver." + # Split-horizon: make app subdomains resolve to the box on the LAN. + declare -F setupLocalDnsRewrites >/dev/null 2>&1 && setupLocalDnsRewrites } diff --git a/scripts/app/containers/pihole/pihole_apply_dns_updater.sh b/scripts/app/containers/pihole/pihole_apply_dns_updater.sh index 1946416..11fc175 100644 --- a/scripts/app/containers/pihole/pihole_apply_dns_updater.sh +++ b/scripts/app/containers/pihole/pihole_apply_dns_updater.sh @@ -9,4 +9,6 @@ appPiholeApplyDnsUpdater() fi updateDNS "pihole" "manual" isSuccessful "/etc/resolv.conf updated to use Pi-hole as the host DNS resolver." + # Split-horizon: make app subdomains resolve to the box on the LAN. + declare -F setupLocalDnsRewrites >/dev/null 2>&1 && setupLocalDnsRewrites } diff --git a/scripts/network/dns/setup_dns_ip.sh b/scripts/network/dns/setup_dns_ip.sh index 8afd95e..205bcbb 100755 --- a/scripts/network/dns/setup_dns_ip.sh +++ b/scripts/network/dns/setup_dns_ip.sh @@ -11,11 +11,10 @@ setupDNSIP() fi fi - # Build variable names based on app_name - dns_host_name_var="CFG_${app_name^^}_HOST_NAME" - - # Access the variables using variable indirection - dns_host_name="${!dns_host_name_var}" - - # UNFINISHED + # STUB: meant to resolve the reachable IP of the given resolver + # (adguard/pihole) into $dns_ip_setup for updateDNS. Not implemented yet, + # so updateDNS falls back to CFG_DNS_SERVER_* upstreams. (Previously read + # CFG__HOST_NAME here, which is unused — removed during the per-port + # subdomain refactor so HOST_NAME has no remaining DNS dependency.) + : } diff --git a/scripts/network/dns/setup_local_dns.sh b/scripts/network/dns/setup_local_dns.sh new file mode 100644 index 0000000..4da40bf --- /dev/null +++ b/scripts/network/dns/setup_local_dns.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# +# Split-horizon ("local") DNS. +# +# Points every configured domain at the LibrePortal server's LAN IP inside the +# self-hosted resolver, so app subdomains resolve to the box on the local +# network and hit Traefik directly — with valid Let's Encrypt certs — instead +# of bouncing out to the public IP (which most home routers can't hairpin). +# +# Domain-level, single-host model: every domain -> the one server IP. Traefik +# sorts out which app by Host header. Adding apps/domains later is covered +# automatically (AdGuard wildcard) or on the next run (Pi-hole host list). +# +# SAFE BY CONSTRUCTION: every action is idempotent, guarded by an "installed" +# check, and cannot corrupt the resolver if a detail is off — +# * AdGuard goes through its REST API; a bad URL/cred just fails harmlessly. +# * Pi-hole writes ONLY to the supported, mounted /etc/pihole/custom.list, +# inside a clearly-marked managed block (never touches /etc/dnsmasq.d). +# NOTE: the AdGuard API call and the Pi-hole reload need a live smoke-test — +# they can't be exercised without the running containers. + +# The server's LAN IP (the source address used to reach the network). +localDnsServerIp() { + local ip + ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}') + [[ -z "$ip" ]] && ip="${public_ip_v4:-}" # last-resort fallback + printf '%s' "$ip" +} + +# Configured (non-empty) domains, one per line. +localDnsDomains() { + local i d + for i in 1 2 3 4 5 6 7 8 9; do + d="CFG_DOMAIN_${i}"; d="${!d:-}" + [[ -n "$d" ]] && printf '%s\n' "$d" + done +} + +# Every app host (subdomain.domain) across all apps' Traefik-managed ports — +# for resolvers without wildcard support (Pi-hole custom.list). Mirrors the +# per-port host rule: subdomain ""/@/root -> apex, else .. +localDnsAppHosts() { + local cfg app up d_idx dom portv sub host + local -a parts + for cfg in "${containers_dir}"*/*.config; do + [[ -f "$cfg" ]] || continue + app=$(basename "$cfg" .config); up=${app^^} + d_idx=$(grep -oE "CFG_${up}_DOMAIN=[0-9]+" "$cfg" | head -1 | cut -d= -f2) + [[ -z "$d_idx" ]] && continue + dom="CFG_DOMAIN_${d_idx}"; dom="${!dom:-}" + [[ -z "$dom" ]] && continue + while IFS= read -r portv; do + IFS='|' read -ra parts <<< "$portv" + [[ "${parts[6]:-}" == "true" ]] || continue # Traefik-managed only + sub="${parts[10]:-}" + if [[ "$sub" == "@" || "$sub" == "root" ]]; then + host="$dom" # apex (explicit only) + elif [[ -n "$sub" ]]; then + host="${sub}.${dom}" + else + host="${app}.${dom}" # no subdomain set -> app-name default + fi + printf '%s\n' "$host" + done < <(grep -oE "CFG_${up}_PORT_[0-9]+=\"[^\"]*\"" "$cfg" | sed -E 's/^[^"]*"//; s/"$//') + done | sort -u +} + +# AdGuard Home: wildcard rewrite per domain (covers every subdomain) + apex, +# via the REST API. Idempotent (delete-then-add). Admin port discovered at +# runtime via `docker port`, creds from the saved CFG_ADGUARD_* values. +localDnsApplyAdguard() { + local ip="$1"; shift + local user="${CFG_ADGUARD_ADMIN_USER:-admin}" + local pass="${CFG_ADGUARD_ADMIN_PASSWORD:-${CFG_ADGUARD_PASSWORD:-}}" + local hostport base d entry payload + hostport=$(dockerCommandRun "docker port adguard-service 3000/tcp" 2>/dev/null | head -1 | sed 's/.*://') + if [[ -z "$hostport" ]]; then + isNotice "AdGuard admin port not found (running?). Skipping AdGuard rewrites." + return 0 + fi + base="http://127.0.0.1:${hostport}" + for d in "$@"; do + for entry in "*.${d}" "${d}"; do + payload="{\"domain\":\"${entry}\",\"answer\":\"${ip}\"}" + curl -fsS -u "${user}:${pass}" -H 'Content-Type: application/json' \ + -X POST "${base}/control/rewrite/delete" -d "$payload" >/dev/null 2>&1 + if curl -fsS -u "${user}:${pass}" -H 'Content-Type: application/json' \ + -X POST "${base}/control/rewrite/add" -d "$payload" >/dev/null 2>&1; then + isSuccessful "AdGuard rewrite: ${entry} -> ${ip}" + else + isNotice "AdGuard rewrite for ${entry} not applied (check API URL/creds) — safe to retry." + fi + done + done +} + +# Pi-hole: per-host A records in the mounted, supported custom.list, inside a +# managed block (no wildcards on Pi-hole; no touching /etc/dnsmasq.d). Reloads +# via `pihole restartdns`, falling back to a container restart. +localDnsApplyPihole() { + local ip="$1" + local list="${containers_dir}pihole/pihole-dnsmasq-unbound/custom.list" # mounts to /etc/pihole + local b="# >>> libreportal-local >>>" e="# <<< libreportal-local <<<" + local hosts tmp h n + hosts=$(localDnsAppHosts) + if [[ -z "$hosts" ]]; then isNotice "No app hosts for Pi-hole — skipping."; return 0; fi + tmp=$(sudo mktemp) + if [[ -f "$list" ]]; then # preserve anything outside our block + sudo awk -v b="$b" -v e="$e" '$0==b{skip=1} !skip{print} $0==e{skip=0}' "$list" | sudo tee "$tmp" >/dev/null + fi + { + echo "$b" + while IFS= read -r h; do [[ -n "$h" ]] && echo "${ip} ${h}"; done <<< "$hosts" + echo "$e" + } | sudo tee -a "$tmp" >/dev/null + sudo cp "$tmp" "$list"; sudo rm -f "$tmp" + n=$(printf '%s\n' "$hosts" | grep -c .) + dockerCommandRun "docker exec pihole-service pihole restartdns" >/dev/null 2>&1 || dockerComposeRestart pihole + isSuccessful "Pi-hole custom.list updated: ${n} hosts -> ${ip}" +} + +# Orchestrator — call after a domain change or app install. +setupLocalDnsRewrites() { + isHeader "Local (split-horizon) DNS" + local ip domains + ip=$(localDnsServerIp) + [[ -z "$ip" ]] && { isNotice "Could not determine the server LAN IP — skipping local DNS."; return 0; } + domains=$(localDnsDomains) + [[ -z "$domains" ]] && { isNotice "No domains configured (CFG_DOMAIN_*) — skipping local DNS."; return 0; } + isNotice "Server IP ${ip} · domains: $(printf '%s ' $domains)" + + local did=0 + if [[ "$(dockerCheckAppInstalled "adguard" "docker")" == "installed" ]]; then + localDnsApplyAdguard "$ip" $domains; did=1 + fi + if [[ "$(dockerCheckAppInstalled "pihole" "docker")" == "installed" ]]; then + localDnsApplyPihole "$ip"; did=1 + fi + [[ "$did" -eq 0 ]] && isNotice "Neither AdGuard nor Pi-hole installed — local DNS skipped." +} diff --git a/scripts/source/files/arrays/files_network.sh b/scripts/source/files/arrays/files_network.sh index 14bebe3..638767e 100755 --- a/scripts/source/files/arrays/files_network.sh +++ b/scripts/source/files/arrays/files_network.sh @@ -16,6 +16,7 @@ network_scripts=( "network/display/show_traefik_services.sh" "network/dns/setup_dns_ip.sh" "network/dns/setup_dns.sh" + "network/dns/setup_local_dns.sh" "network/firewall/firewall_initial_setup.sh" "network/firewall/rules/firewall_clear_rules.sh" "network/firewall/rules/firewall_rebuild_from_db.sh"