From 17ab7c4929f38f994840b46dd6085180e7fc2f3d Mon Sep 17 00:00:00 2001
From: Jan Eitzinger <jan@moebiusband.org>
Date: Mon, 2 Jun 2025 08:44:10 +0200
Subject: [PATCH] Add auth, rest api, svelte frontend, build structure

---
 .air.toml                                     |   81 +-
 .sqlfluff                                     |    7 +
 Makefile                                      |   40 +-
 go.mod                                        |    5 +
 go.sum                                        |   10 +
 internal/api/rest.go                          |  131 ++
 internal/auth/auth.go                         |  248 +++
 internal/auth/local.go                        |   47 +
 internal/handlers/admin.go                    |   33 +
 internal/handlers/root.go                     |   19 +-
 internal/middleware/recover.go                |    2 +-
 internal/middleware/securedCheck.go           |   27 +
 internal/repository/db.go                     |   31 +
 internal/repository/dbConn.go                 |   11 +-
 internal/repository/migration.go              |   12 +-
 .../repository/migrations/0000_schema.up.sql  |   11 -
 ...000_schema.down.sql => 01_schema.down.sql} |    0
 .../repository/migrations/01_schema.up.sql    |   23 +
 internal/repository/models.go                 |   33 +
 internal/repository/query.sql.go              |  386 +++++
 internal/repository/sql/query.sql             |   86 +-
 main.go                                       |  117 +-
 web/frontend/.gitignore                       |   24 +
 web/frontend/README.md                        |   47 +
 web/frontend/index.html                       |   13 +
 web/frontend/package-lock.json                | 1412 +++++++++++++++++
 web/frontend/package.json                     |   20 +
 web/{static => frontend/public}/css/app.css   |   19 +
 .../public}/css/bootstrap-grid.css            |    0
 .../public}/css/bootstrap-grid.css.map        |    0
 .../public}/css/bootstrap-grid.min.css        |    0
 .../public}/css/bootstrap-grid.min.css.map    |    0
 .../public}/css/bootstrap-grid.rtl.css        |    0
 .../public}/css/bootstrap-grid.rtl.css.map    |    0
 .../public}/css/bootstrap-grid.rtl.min.css    |    0
 .../css/bootstrap-grid.rtl.min.css.map        |    0
 .../public}/css/bootstrap-icons.css           |    0
 .../public}/css/bootstrap-icons.json          |    0
 .../public}/css/bootstrap-icons.min.css       |    0
 .../public}/css/bootstrap-reboot.css          |    0
 .../public}/css/bootstrap-reboot.css.map      |    0
 .../public}/css/bootstrap-reboot.min.css      |    0
 .../public}/css/bootstrap-reboot.min.css.map  |    0
 .../public}/css/bootstrap-reboot.rtl.css      |    0
 .../public}/css/bootstrap-reboot.rtl.css.map  |    0
 .../public}/css/bootstrap-reboot.rtl.min.css  |    0
 .../css/bootstrap-reboot.rtl.min.css.map      |    0
 .../public}/css/bootstrap-utilities.css       |    0
 .../public}/css/bootstrap-utilities.css.map   |    0
 .../public}/css/bootstrap-utilities.min.css   |    0
 .../css/bootstrap-utilities.min.css.map       |    0
 .../public}/css/bootstrap-utilities.rtl.css   |    0
 .../css/bootstrap-utilities.rtl.css.map       |    0
 .../css/bootstrap-utilities.rtl.min.css       |    0
 .../css/bootstrap-utilities.rtl.min.css.map   |    0
 .../public}/css/bootstrap.css                 |    0
 .../public}/css/bootstrap.css.map             |    0
 .../public}/css/bootstrap.min.css             |    0
 .../public}/css/bootstrap.min.css.map         |    0
 .../public}/css/bootstrap.rtl.css             |    0
 .../public}/css/bootstrap.rtl.css.map         |    0
 .../public}/css/bootstrap.rtl.min.css         |    0
 .../public}/css/bootstrap.rtl.min.css.map     |    0
 .../public}/css/fonts/bootstrap-icons.woff    |  Bin
 .../public}/css/fonts/bootstrap-icons.woff2   |  Bin
 web/{static => frontend/public}/favicon.ico   |  Bin
 .../public}/img/Bromeliad.jpg                 |  Bin
 .../public}/img/RookieStripes.jpg             |  Bin
 .../public}/img/TreeOfLight.jpg               |  Bin
 .../public}/img/action.jpg                    |  Bin
 .../public}/img/anniversary/image-1.jpg       |  Bin
 .../public}/img/anniversary/image-10.jpg      |  Bin
 .../public}/img/anniversary/image-11.jpg      |  Bin
 .../public}/img/anniversary/image-12.png      |  Bin
 .../public}/img/anniversary/image-13.jpg      |  Bin
 .../public}/img/anniversary/image-14.jpg      |  Bin
 .../public}/img/anniversary/image-15.png      |  Bin
 .../public}/img/anniversary/image-16.jpg      |  Bin
 .../public}/img/anniversary/image-17.png      |  Bin
 .../public}/img/anniversary/image-18.jpg      |  Bin
 .../public}/img/anniversary/image-19.jpg      |  Bin
 .../public}/img/anniversary/image-2.jpg       |  Bin
 .../public}/img/anniversary/image-20.jpg      |  Bin
 .../public}/img/anniversary/image-21.jpg      |  Bin
 .../public}/img/anniversary/image-22.jpg      |  Bin
 .../public}/img/anniversary/image-23.jpg      |  Bin
 .../public}/img/anniversary/image-24.jpg      |  Bin
 .../public}/img/anniversary/image-25.jpg      |  Bin
 .../public}/img/anniversary/image-26.jpg      |  Bin
 .../public}/img/anniversary/image-27.jpg      |  Bin
 .../public}/img/anniversary/image-28.jpg      |  Bin
 .../public}/img/anniversary/image-29.jpg      |  Bin
 .../public}/img/anniversary/image-3.jpg       |  Bin
 .../public}/img/anniversary/image-30.jpg      |  Bin
 .../public}/img/anniversary/image-31.jpg      |  Bin
 .../public}/img/anniversary/image-32.jpg      |  Bin
 .../public}/img/anniversary/image-33.jpg      |  Bin
 .../public}/img/anniversary/image-34.jpg      |  Bin
 .../public}/img/anniversary/image-35.jpg      |  Bin
 .../public}/img/anniversary/image-36.jpg      |  Bin
 .../public}/img/anniversary/image-37.jpg      |  Bin
 .../public}/img/anniversary/image-38.jpg      |  Bin
 .../public}/img/anniversary/image-39.jpg      |  Bin
 .../public}/img/anniversary/image-4.jpg       |  Bin
 .../public}/img/anniversary/image-40.jpg      |  Bin
 .../public}/img/anniversary/image-41.jpg      |  Bin
 .../public}/img/anniversary/image-42.jpg      |  Bin
 .../public}/img/anniversary/image-43.jpg      |  Bin
 .../public}/img/anniversary/image-44.jpg      |  Bin
 .../public}/img/anniversary/image-45.jpg      |  Bin
 .../public}/img/anniversary/image-46.jpg      |  Bin
 .../public}/img/anniversary/image-47.jpg      |  Bin
 .../public}/img/anniversary/image-48.jpg      |  Bin
 .../public}/img/anniversary/image-49.jpg      |  Bin
 .../public}/img/anniversary/image-5.jpg       |  Bin
 .../public}/img/anniversary/image-50.jpg      |  Bin
 .../public}/img/anniversary/image-6.jpg       |  Bin
 .../public}/img/anniversary/image-7.jpg       |  Bin
 .../public}/img/anniversary/image-8.jpg       |  Bin
 .../public}/img/anniversary/image-9.jpg       |  Bin
 .../public}/img/cordula.jpg                   |  Bin
 .../public}/img/downloads-bg.jpg              |  Bin
 .../public}/img/farben.jpg                    |  Bin
 .../public}/img/flags/Argentina.png           |  Bin
 .../public}/img/flags/Australia.png           |  Bin
 .../public}/img/flags/Austria.png             |  Bin
 .../public}/img/flags/Belgium.png             |  Bin
 .../public}/img/flags/Brazil.png              |  Bin
 .../public}/img/flags/Bulgaria.png            |  Bin
 .../public}/img/flags/Canada.png              |  Bin
 .../public}/img/flags/Chile.png               |  Bin
 .../public}/img/flags/China.png               |  Bin
 .../public}/img/flags/Croatia.png             |  Bin
 .../public}/img/flags/Czech-Republic.png      |  Bin
 .../public}/img/flags/Denmark.png             |  Bin
 .../public}/img/flags/England.png             |  Bin
 .../public}/img/flags/Finland.png             |  Bin
 .../public}/img/flags/France.png              |  Bin
 .../public}/img/flags/Germany.png             |  Bin
 .../public}/img/flags/Greece.png              |  Bin
 .../public}/img/flags/Hong-Kong.png           |  Bin
 .../public}/img/flags/Hungary.png             |  Bin
 .../public}/img/flags/Iceland.png             |  Bin
 .../public}/img/flags/India.png               |  Bin
 .../public}/img/flags/Indonesia.png           |  Bin
 .../public}/img/flags/Iran.png                |  Bin
 .../public}/img/flags/Iraq.png                |  Bin
 .../public}/img/flags/Ireland.png             |  Bin
 .../public}/img/flags/Israel.png              |  Bin
 .../public}/img/flags/Italy.png               |  Bin
 .../public}/img/flags/Jamaica.png             |  Bin
 .../public}/img/flags/Japan.png               |  Bin
 .../public}/img/flags/Kenya.png               |  Bin
 .../public}/img/flags/Kuwait.png              |  Bin
 .../public}/img/flags/Luxembourg.png          |  Bin
 .../public}/img/flags/Malaysia.png            |  Bin
 .../public}/img/flags/Namibia.png             |  Bin
 .../public}/img/flags/Netherlands.png         |  Bin
 .../public}/img/flags/Norway.png              |  Bin
 .../public}/img/flags/Pakistan.png            |  Bin
 .../public}/img/flags/Poland.png              |  Bin
 .../public}/img/flags/Portugal.png            |  Bin
 .../public}/img/flags/Romania.png             |  Bin
 .../public}/img/flags/Russia.png              |  Bin
 .../public}/img/flags/Scotland.png            |  Bin
 .../public}/img/flags/Senegal.png             |  Bin
 .../public}/img/flags/Serbia.png              |  Bin
 .../public}/img/flags/Singapore.png           |  Bin
 .../public}/img/flags/Slovakia.png            |  Bin
 .../public}/img/flags/Slovenia.png            |  Bin
 .../public}/img/flags/South-Africa.png        |  Bin
 .../public}/img/flags/South-Korea.png         |  Bin
 .../public}/img/flags/Spain.png               |  Bin
 .../public}/img/flags/Sweden.png              |  Bin
 .../public}/img/flags/Switzerland.png         |  Bin
 .../public}/img/flags/Taiwan.png              |  Bin
 .../public}/img/flags/Thailand.png            |  Bin
 .../public}/img/flags/Turkey.png              |  Bin
 .../public}/img/flags/United-Kingdom.png      |  Bin
 .../public}/img/flags/United-Nations.png      |  Bin
 .../public}/img/flags/United-States.png       |  Bin
 .../public}/img/intro-bg.jpg                  |  Bin
 web/{static => frontend/public}/img/laden.jpg |  Bin
 .../public}/img/loading.gif                   |  Bin
 .../public}/img/logo-small.png                |  Bin
 web/{static => frontend/public}/img/logo.png  |  Bin
 .../public}/img/map-marker.png                |  Bin
 .../public}/img/montage.jpg                   |  Bin
 web/{static => frontend/public}/img/niggi.jpg |  Bin
 .../public}/img/overview.jpg                  |  Bin
 .../public}/img/progressbar.gif               |  Bin
 web/{static => frontend/public}/img/waage.jpg |  Bin
 .../public}/img/wickeln.jpg                   |  Bin
 web/{static => frontend/public}/img/wolle.jpg |  Bin
 .../public}/js/bootstrap.bundle.js            |    0
 .../public}/js/bootstrap.bundle.js.map        |    0
 .../public}/js/bootstrap.bundle.min.js        |    0
 .../public}/js/bootstrap.bundle.min.js.map    |    0
 .../public}/js/bootstrap.esm.js               |    0
 .../public}/js/bootstrap.esm.js.map           |    0
 .../public}/js/bootstrap.esm.min.js           |    0
 .../public}/js/bootstrap.esm.min.js.map       |    0
 .../public}/js/bootstrap.js                   |    0
 .../public}/js/bootstrap.js.map               |    0
 .../public}/js/bootstrap.min.js               |    0
 .../public}/js/bootstrap.min.js.map           |    0
 web/{static => frontend/public}/robots.txt    |    0
 web/frontend/src/App.svelte                   |   45 +
 web/frontend/src/app.css                      |   86 +
 web/frontend/src/assets/svelte.svg            |    1 +
 web/frontend/src/lib/Counter.svelte           |   10 +
 web/frontend/src/main.ts                      |    9 +
 web/frontend/src/vite-env.d.ts                |    2 +
 web/frontend/svelte.config.js                 |    7 +
 web/frontend/tsconfig.app.json                |   20 +
 web/frontend/tsconfig.json                    |    7 +
 web/frontend/tsconfig.node.json               |   25 +
 web/frontend/vite.config.ts                   |    7 +
 web/templates.go                              |   20 +
 web/templates/admin.html                      |    3 +
 web/templates/base.html                       |    5 +-
 web/templates/login.html                      |   51 +
 222 files changed, 3057 insertions(+), 136 deletions(-)
 create mode 100644 .sqlfluff
 create mode 100644 internal/api/rest.go
 create mode 100644 internal/auth/auth.go
 create mode 100644 internal/auth/local.go
 create mode 100644 internal/handlers/admin.go
 create mode 100644 internal/middleware/securedCheck.go
 create mode 100644 internal/repository/db.go
 delete mode 100644 internal/repository/migrations/0000_schema.up.sql
 rename internal/repository/migrations/{0000_schema.down.sql => 01_schema.down.sql} (100%)
 create mode 100644 internal/repository/migrations/01_schema.up.sql
 create mode 100644 internal/repository/models.go
 create mode 100644 internal/repository/query.sql.go
 create mode 100644 web/frontend/.gitignore
 create mode 100644 web/frontend/README.md
 create mode 100644 web/frontend/index.html
 create mode 100644 web/frontend/package-lock.json
 create mode 100644 web/frontend/package.json
 rename web/{static => frontend/public}/css/app.css (94%)
 rename web/{static => frontend/public}/css/bootstrap-grid.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.rtl.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.rtl.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.rtl.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-grid.rtl.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-icons.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-icons.json (100%)
 rename web/{static => frontend/public}/css/bootstrap-icons.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.rtl.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.rtl.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.rtl.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-reboot.rtl.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.rtl.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.rtl.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.rtl.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap-utilities.rtl.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap.css (100%)
 rename web/{static => frontend/public}/css/bootstrap.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap.min.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap.rtl.css (100%)
 rename web/{static => frontend/public}/css/bootstrap.rtl.css.map (100%)
 rename web/{static => frontend/public}/css/bootstrap.rtl.min.css (100%)
 rename web/{static => frontend/public}/css/bootstrap.rtl.min.css.map (100%)
 rename web/{static => frontend/public}/css/fonts/bootstrap-icons.woff (100%)
 rename web/{static => frontend/public}/css/fonts/bootstrap-icons.woff2 (100%)
 rename web/{static => frontend/public}/favicon.ico (100%)
 rename web/{static => frontend/public}/img/Bromeliad.jpg (100%)
 rename web/{static => frontend/public}/img/RookieStripes.jpg (100%)
 rename web/{static => frontend/public}/img/TreeOfLight.jpg (100%)
 rename web/{static => frontend/public}/img/action.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-1.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-10.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-11.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-12.png (100%)
 rename web/{static => frontend/public}/img/anniversary/image-13.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-14.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-15.png (100%)
 rename web/{static => frontend/public}/img/anniversary/image-16.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-17.png (100%)
 rename web/{static => frontend/public}/img/anniversary/image-18.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-19.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-2.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-20.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-21.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-22.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-23.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-24.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-25.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-26.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-27.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-28.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-29.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-3.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-30.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-31.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-32.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-33.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-34.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-35.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-36.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-37.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-38.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-39.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-4.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-40.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-41.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-42.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-43.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-44.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-45.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-46.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-47.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-48.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-49.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-5.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-50.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-6.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-7.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-8.jpg (100%)
 rename web/{static => frontend/public}/img/anniversary/image-9.jpg (100%)
 rename web/{static => frontend/public}/img/cordula.jpg (100%)
 rename web/{static => frontend/public}/img/downloads-bg.jpg (100%)
 rename web/{static => frontend/public}/img/farben.jpg (100%)
 rename web/{static => frontend/public}/img/flags/Argentina.png (100%)
 rename web/{static => frontend/public}/img/flags/Australia.png (100%)
 rename web/{static => frontend/public}/img/flags/Austria.png (100%)
 rename web/{static => frontend/public}/img/flags/Belgium.png (100%)
 rename web/{static => frontend/public}/img/flags/Brazil.png (100%)
 rename web/{static => frontend/public}/img/flags/Bulgaria.png (100%)
 rename web/{static => frontend/public}/img/flags/Canada.png (100%)
 rename web/{static => frontend/public}/img/flags/Chile.png (100%)
 rename web/{static => frontend/public}/img/flags/China.png (100%)
 rename web/{static => frontend/public}/img/flags/Croatia.png (100%)
 rename web/{static => frontend/public}/img/flags/Czech-Republic.png (100%)
 rename web/{static => frontend/public}/img/flags/Denmark.png (100%)
 rename web/{static => frontend/public}/img/flags/England.png (100%)
 rename web/{static => frontend/public}/img/flags/Finland.png (100%)
 rename web/{static => frontend/public}/img/flags/France.png (100%)
 rename web/{static => frontend/public}/img/flags/Germany.png (100%)
 rename web/{static => frontend/public}/img/flags/Greece.png (100%)
 rename web/{static => frontend/public}/img/flags/Hong-Kong.png (100%)
 rename web/{static => frontend/public}/img/flags/Hungary.png (100%)
 rename web/{static => frontend/public}/img/flags/Iceland.png (100%)
 rename web/{static => frontend/public}/img/flags/India.png (100%)
 rename web/{static => frontend/public}/img/flags/Indonesia.png (100%)
 rename web/{static => frontend/public}/img/flags/Iran.png (100%)
 rename web/{static => frontend/public}/img/flags/Iraq.png (100%)
 rename web/{static => frontend/public}/img/flags/Ireland.png (100%)
 rename web/{static => frontend/public}/img/flags/Israel.png (100%)
 rename web/{static => frontend/public}/img/flags/Italy.png (100%)
 rename web/{static => frontend/public}/img/flags/Jamaica.png (100%)
 rename web/{static => frontend/public}/img/flags/Japan.png (100%)
 rename web/{static => frontend/public}/img/flags/Kenya.png (100%)
 rename web/{static => frontend/public}/img/flags/Kuwait.png (100%)
 rename web/{static => frontend/public}/img/flags/Luxembourg.png (100%)
 rename web/{static => frontend/public}/img/flags/Malaysia.png (100%)
 rename web/{static => frontend/public}/img/flags/Namibia.png (100%)
 rename web/{static => frontend/public}/img/flags/Netherlands.png (100%)
 rename web/{static => frontend/public}/img/flags/Norway.png (100%)
 rename web/{static => frontend/public}/img/flags/Pakistan.png (100%)
 rename web/{static => frontend/public}/img/flags/Poland.png (100%)
 rename web/{static => frontend/public}/img/flags/Portugal.png (100%)
 rename web/{static => frontend/public}/img/flags/Romania.png (100%)
 rename web/{static => frontend/public}/img/flags/Russia.png (100%)
 rename web/{static => frontend/public}/img/flags/Scotland.png (100%)
 rename web/{static => frontend/public}/img/flags/Senegal.png (100%)
 rename web/{static => frontend/public}/img/flags/Serbia.png (100%)
 rename web/{static => frontend/public}/img/flags/Singapore.png (100%)
 rename web/{static => frontend/public}/img/flags/Slovakia.png (100%)
 rename web/{static => frontend/public}/img/flags/Slovenia.png (100%)
 rename web/{static => frontend/public}/img/flags/South-Africa.png (100%)
 rename web/{static => frontend/public}/img/flags/South-Korea.png (100%)
 rename web/{static => frontend/public}/img/flags/Spain.png (100%)
 rename web/{static => frontend/public}/img/flags/Sweden.png (100%)
 rename web/{static => frontend/public}/img/flags/Switzerland.png (100%)
 rename web/{static => frontend/public}/img/flags/Taiwan.png (100%)
 rename web/{static => frontend/public}/img/flags/Thailand.png (100%)
 rename web/{static => frontend/public}/img/flags/Turkey.png (100%)
 rename web/{static => frontend/public}/img/flags/United-Kingdom.png (100%)
 rename web/{static => frontend/public}/img/flags/United-Nations.png (100%)
 rename web/{static => frontend/public}/img/flags/United-States.png (100%)
 rename web/{static => frontend/public}/img/intro-bg.jpg (100%)
 rename web/{static => frontend/public}/img/laden.jpg (100%)
 rename web/{static => frontend/public}/img/loading.gif (100%)
 rename web/{static => frontend/public}/img/logo-small.png (100%)
 rename web/{static => frontend/public}/img/logo.png (100%)
 rename web/{static => frontend/public}/img/map-marker.png (100%)
 rename web/{static => frontend/public}/img/montage.jpg (100%)
 rename web/{static => frontend/public}/img/niggi.jpg (100%)
 rename web/{static => frontend/public}/img/overview.jpg (100%)
 rename web/{static => frontend/public}/img/progressbar.gif (100%)
 rename web/{static => frontend/public}/img/waage.jpg (100%)
 rename web/{static => frontend/public}/img/wickeln.jpg (100%)
 rename web/{static => frontend/public}/img/wolle.jpg (100%)
 rename web/{static => frontend/public}/js/bootstrap.bundle.js (100%)
 rename web/{static => frontend/public}/js/bootstrap.bundle.js.map (100%)
 rename web/{static => frontend/public}/js/bootstrap.bundle.min.js (100%)
 rename web/{static => frontend/public}/js/bootstrap.bundle.min.js.map (100%)
 rename web/{static => frontend/public}/js/bootstrap.esm.js (100%)
 rename web/{static => frontend/public}/js/bootstrap.esm.js.map (100%)
 rename web/{static => frontend/public}/js/bootstrap.esm.min.js (100%)
 rename web/{static => frontend/public}/js/bootstrap.esm.min.js.map (100%)
 rename web/{static => frontend/public}/js/bootstrap.js (100%)
 rename web/{static => frontend/public}/js/bootstrap.js.map (100%)
 rename web/{static => frontend/public}/js/bootstrap.min.js (100%)
 rename web/{static => frontend/public}/js/bootstrap.min.js.map (100%)
 rename web/{static => frontend/public}/robots.txt (100%)
 create mode 100644 web/frontend/src/App.svelte
 create mode 100644 web/frontend/src/app.css
 create mode 100644 web/frontend/src/assets/svelte.svg
 create mode 100644 web/frontend/src/lib/Counter.svelte
 create mode 100644 web/frontend/src/main.ts
 create mode 100644 web/frontend/src/vite-env.d.ts
 create mode 100644 web/frontend/svelte.config.js
 create mode 100644 web/frontend/tsconfig.app.json
 create mode 100644 web/frontend/tsconfig.json
 create mode 100644 web/frontend/tsconfig.node.json
 create mode 100644 web/frontend/vite.config.ts
 create mode 100644 web/templates/admin.html
 create mode 100644 web/templates/login.html

diff --git a/.air.toml b/.air.toml
index 498951f..e58a42a 100644
--- a/.air.toml
+++ b/.air.toml
@@ -3,50 +3,57 @@ testdata_dir = "testdata"
 tmp_dir = "tmp"
 
 [build]
-  args_bin = []
-  bin = "./tmp/main"
-  cmd = "go build -o ./tmp/main ."
-  delay = 1000
-  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
-  exclude_file = []
-  exclude_regex = ["_test.go"]
-  exclude_unchanged = false
-  follow_symlink = false
-  full_bin = ""
-  include_dir = []
-  include_ext = ["go", "tpl", "tmpl", "html"]
-  include_file = []
-  kill_delay = "0s"
-  log = "build-errors.log"
-  poll = false
-  poll_interval = 0
-  post_cmd = []
-  pre_cmd = []
-  rerun = false
-  rerun_delay = 500
-  send_interrupt = false
-  stop_on_error = false
+args_bin = []
+bin = "./tmp/main"
+cmd = "go build -o ./tmp/main ."
+delay = 1000
+exclude_dir = [
+  "assets",
+  "tmp",
+  "vendor",
+  "testdata",
+  "web/frontend/node_modules",
+  "web/static",
+]
+exclude_file = []
+exclude_regex = ["_test.go"]
+exclude_unchanged = false
+follow_symlink = false
+full_bin = ""
+include_dir = []
+include_ext = ["go", "ts", "svelte", "html"]
+include_file = ["main.go", "vite.config.ts"]
+kill_delay = "0s"
+log = "build-errors.log"
+poll = false
+poll_interval = 0
+post_cmd = []
+pre_cmd = []
+rerun = false
+rerun_delay = 500
+send_interrupt = false
+stop_on_error = false
 
 [color]
-  app = ""
-  build = "yellow"
-  main = "magenta"
-  runner = "green"
-  watcher = "cyan"
+app = ""
+build = "yellow"
+main = "magenta"
+runner = "green"
+watcher = "cyan"
 
 [log]
-  main_only = false
-  silent = false
-  time = false
+main_only = false
+silent = false
+time = false
 
 [misc]
-  clean_on_exit = false
+clean_on_exit = false
 
 [proxy]
-  app_port = 0
-  enabled = false
-  proxy_port = 0
+app_port = 0
+enabled = false
+proxy_port = 0
 
 [screen]
-  clear_on_rebuild = false
-  keep_scroll = true
+clear_on_rebuild = false
+keep_scroll = true
diff --git a/.sqlfluff b/.sqlfluff
new file mode 100644
index 0000000..bc52c80
--- /dev/null
+++ b/.sqlfluff
@@ -0,0 +1,7 @@
+[sqlfluff]
+dialect = sqlite
+templater = placeholder
+processes = -1
+
+[sqlfluff:templater:placeholder]
+param_style = question_mark
diff --git a/Makefile b/Makefile
index 8cf33ea..e5f387b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,27 +1,17 @@
-.PHONY: dev build
+TARGET = ./tmp/server
+FRONTEND = ./web/frontend
 
-dev:
-	@if command -v $(HOME)/go/bin/air > /dev/null; then \
-		AIR_CMD="$(HOME)/go/bin/air"; \
-	elif command -v air > /dev/null; then \
-		AIR_CMD="air"; \
-	else \
-		read -p "air is not installed. Install it? [Y/n] " choice; \
-		if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \
-			echo "Installing..."; \
-			go install github.com/air-verse/air@latest; \
-			AIR_CMD="$(HOME)/go/bin/air"; \
-		else \
-			echo "Exiting..."; \
-			exit 1; \
-		fi; \
-	fi; \
-	echo "Starting Air..."; \
-	$$AIR_CMD
+SVELTE_COMPONENTS = status
 
-build:
-	@echo "Generate Tailwind CSS..."
-	go generate
-	@echo "Building Go server..."
-	go build -o tmp/server main.go
-	@echo "Build complete."
+SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .ts, $(SVELTE_COMPONENTS)))
+
+.PHONY: $(TARGET)
+.NOTPARALLEL:
+
+$(TARGET): $(SVELTE_TARGETS)
+	$(info ===>  BUILD Go server)
+	@go build -o $(TARGET) main.go
+
+$(SVELTE_TARGETS): $(SVELTE_SRC)
+	$(info ===>  BUILD frontend)
+	cd $(FRONTEND) && npm install && npm run build
diff --git a/go.mod b/go.mod
index 64d11ff..3bc30f3 100644
--- a/go.mod
+++ b/go.mod
@@ -12,15 +12,20 @@ require (
 require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/securecookie v1.1.2 // indirect
+	github.com/gorilla/sessions v1.4.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-sqlite3 v1.14.24 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/olivere/vite v0.1.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	go.uber.org/atomic v1.11.0 // indirect
+	golang.org/x/crypto v0.38.0 // indirect
 	golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/time v0.11.0 // indirect
 	modernc.org/libc v1.65.7 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.11.0 // indirect
diff --git a/go.sum b/go.sum
index a123efb..2797b53 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,10 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -23,6 +27,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
 github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/olivere/vite v0.1.0 h1:Wi5zTtS3BbnOrfG+oRT7KZOI9lp48gRv59VptSBmPO4=
+github.com/olivere/vite v0.1.0/go.mod h1:ef1SWmGSWAYJxSuY2Bu90YLQ7hUBxYmejIVuFGsIIe8=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -31,6 +37,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
 golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
@@ -40,6 +48,8 @@ golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
 golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/internal/api/rest.go b/internal/api/rest.go
new file mode 100644
index 0000000..aa71d3d
--- /dev/null
+++ b/internal/api/rest.go
@@ -0,0 +1,131 @@
+package api
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log/slog"
+	"net/http"
+	"strconv"
+
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
+)
+
+type ErrorResponse struct {
+	// Statustext of Errorcode
+	Status string `json:"status"`
+	Error  string `json:"error"` // Error Message
+}
+
+func MountApiEndpoints(r *http.ServeMux) {
+	r.HandleFunc("POST /api/news/", createNewsItem)
+	r.HandleFunc("GET /api/news/", readNewsItems)
+	r.HandleFunc("GET /api/news/{id}", readNewsItem)
+	r.HandleFunc("PATCH /api/news/", updateNewsItem)
+	r.HandleFunc("DELETE /api/news/{id}", deleteNewsItem)
+}
+
+func handleError(err error, statusCode int, rw http.ResponseWriter) {
+	slog.Warn("rest error", "error", err)
+	rw.Header().Add("Content-Type", "application/json")
+	rw.WriteHeader(statusCode)
+	json.NewEncoder(rw).Encode(ErrorResponse{
+		Status: http.StatusText(statusCode),
+		Error:  err.Error(),
+	})
+}
+
+func decode(r io.Reader, val any) error {
+	dec := json.NewDecoder(r)
+	dec.DisallowUnknownFields()
+	return dec.Decode(val)
+}
+
+func createNewsItem(rw http.ResponseWriter, r *http.Request) {
+	req := repository.CreateNewsEntryParams{}
+	if err := decode(r.Body, &req); err != nil {
+		handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
+		return
+	}
+
+	repo := repository.GetRepository()
+	err := repo.CreateNewsEntry(r.Context(), req)
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+}
+
+func readNewsItems(rw http.ResponseWriter, r *http.Request) {
+	repo := repository.GetRepository()
+	items, err := repo.ListNews(r.Context())
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+
+	slog.Debug("/api/news returned", "newscount", len(items))
+	rw.Header().Add("Content-Type", "application/json")
+	bw := bufio.NewWriter(rw)
+	defer bw.Flush()
+
+	if err := json.NewEncoder(bw).Encode(items); err != nil {
+		handleError(err, http.StatusInternalServerError, rw)
+		return
+	}
+}
+
+func readNewsItem(rw http.ResponseWriter, r *http.Request) {
+	repo := repository.GetRepository()
+	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+
+	item, err := repo.GetNewsEntry(r.Context(), id)
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+
+	rw.Header().Add("Content-Type", "application/json")
+	bw := bufio.NewWriter(rw)
+	defer bw.Flush()
+
+	if err := json.NewEncoder(bw).Encode(item); err != nil {
+		handleError(err, http.StatusInternalServerError, rw)
+		return
+	}
+}
+
+func updateNewsItem(rw http.ResponseWriter, r *http.Request) {
+	req := repository.UpdateNewsEntryParams{}
+	if err := decode(r.Body, &req); err != nil {
+		handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
+		return
+	}
+
+	repo := repository.GetRepository()
+	err := repo.UpdateNewsEntry(r.Context(), req)
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+}
+
+func deleteNewsItem(rw http.ResponseWriter, r *http.Request) {
+	repo := repository.GetRepository()
+	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+
+	err = repo.DeleteNewsEntry(r.Context(), id)
+	if err != nil {
+		handleError(err, http.StatusBadRequest, rw)
+		return
+	}
+}
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
new file mode 100644
index 0000000..e42ca49
--- /dev/null
+++ b/internal/auth/auth.go
@@ -0,0 +1,248 @@
+// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
+// All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+package auth
+
+import (
+	"context"
+	"crypto/rand"
+	"database/sql"
+	"encoding/base64"
+	"errors"
+	"log/slog"
+	"net"
+	"net/http"
+	"os"
+	"sync"
+	"time"
+
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
+	"github.com/gorilla/sessions"
+	"golang.org/x/time/rate"
+)
+
+type Authenticator interface {
+	CanLogin(user *repository.AppUser, username string, rw http.ResponseWriter, r *http.Request) (*repository.AppUser, bool)
+	Login(user *repository.AppUser, rw http.ResponseWriter, r *http.Request) (*repository.AppUser, error)
+}
+
+var (
+	initOnce     sync.Once
+	authInstance *Authentication
+)
+
+var ipUserLimiters sync.Map
+
+func getIPUserLimiter(ip, username string) *rate.Limiter {
+	key := ip + ":" + username
+	limiter, ok := ipUserLimiters.Load(key)
+	if !ok {
+		newLimiter := rate.NewLimiter(rate.Every(time.Hour/10), 10)
+		ipUserLimiters.Store(key, newLimiter)
+		return newLimiter
+	}
+	return limiter.(*rate.Limiter)
+}
+
+type Authentication struct {
+	sessionStore   *sessions.CookieStore
+	LocalAuth      *LocalAuthenticator
+	authenticators []Authenticator
+	SessionMaxAge  time.Duration
+}
+
+func (auth *Authentication) AuthViaSession(
+	rw http.ResponseWriter,
+	r *http.Request,
+) (*repository.AppUser, error) {
+	session, err := auth.sessionStore.Get(r, "session")
+	if err != nil {
+		slog.Error("Error while getting session store")
+		return nil, err
+	}
+
+	if session.IsNew {
+		return nil, nil
+	}
+
+	// TODO: Check if session keys exist
+	username, _ := session.Values["username"].(string)
+	return &repository.AppUser{
+		UserName: username,
+	}, nil
+}
+
+func Init() {
+	initOnce.Do(func() {
+		authInstance = &Authentication{}
+
+		sessKey := os.Getenv("SESSION_KEY")
+		if sessKey == "" {
+			slog.Warn("environment variable 'SESSION_KEY' not set (will use non-persistent random key)")
+			bytes := make([]byte, 32)
+			if _, err := rand.Read(bytes); err != nil {
+				slog.Error("Error while initializing authentication -> failed to generate random bytes for session key")
+				os.Exit(1)
+			}
+			authInstance.sessionStore = sessions.NewCookieStore(bytes)
+		} else {
+			bytes, err := base64.StdEncoding.DecodeString(sessKey)
+			if err != nil {
+				slog.Error("Error while initializing authentication -> decoding session key failed")
+				os.Exit(1)
+			}
+			authInstance.sessionStore = sessions.NewCookieStore(bytes)
+		}
+
+		if d, err := time.ParseDuration("24h"); err == nil {
+			authInstance.SessionMaxAge = d
+		}
+
+		authInstance.LocalAuth = &LocalAuthenticator{}
+		if err := authInstance.LocalAuth.Init(); err != nil {
+			slog.Error("Error while initializing authentication -> localAuth init failed")
+			os.Exit(1)
+		}
+		authInstance.authenticators = append(authInstance.authenticators, authInstance.LocalAuth)
+	})
+}
+
+func GetAuthInstance() *Authentication {
+	if authInstance == nil {
+		slog.Error("Authentication module not initialized!")
+	}
+
+	return authInstance
+}
+
+func (auth *Authentication) SaveSession(rw http.ResponseWriter,
+	r *http.Request, user *repository.AppUser,
+) error {
+	session, err := auth.sessionStore.New(r, "session")
+	if err != nil {
+		slog.Error("session creation failed", "error", err.Error())
+		http.Error(rw, err.Error(), http.StatusInternalServerError)
+		return err
+	}
+
+	if auth.SessionMaxAge != 0 {
+		session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
+	}
+	session.Options.Secure = false
+	session.Options.SameSite = http.SameSiteStrictMode
+	session.Values["username"] = user.UserName
+	if err := auth.sessionStore.Save(r, rw, session); err != nil {
+		slog.Warn("session save failed", "error", err.Error())
+		http.Error(rw, err.Error(), http.StatusInternalServerError)
+		return err
+	}
+
+	return nil
+}
+
+func (auth *Authentication) Login(
+	onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
+) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		ip, _, err := net.SplitHostPort(r.RemoteAddr)
+		if err != nil {
+			ip = r.RemoteAddr
+		}
+
+		username := r.FormValue("username")
+
+		limiter := getIPUserLimiter(ip, username)
+		if !limiter.Allow() {
+			slog.Warn("AUTH/RATE > Too many login attempts for combination", "ip", ip, "username", username)
+			onfailure(rw, r, errors.New("too many login attempts, try again in a few minutes"))
+			return
+		}
+
+		var dbUser repository.AppUser
+		if username != "" {
+			var err error
+			dbUser, err = repository.GetRepository().GetUser(r.Context(), username)
+			if err != nil && err != sql.ErrNoRows {
+				slog.Error("Error while loading user", "username", username)
+			}
+		}
+
+		for _, authenticator := range auth.authenticators {
+			var ok bool
+			var user *repository.AppUser
+			if user, ok = authenticator.CanLogin(&dbUser, username, rw, r); !ok {
+				continue
+			} else {
+				slog.Debug("Can login with user", "username", user.UserName)
+			}
+
+			user, err := authenticator.Login(user, rw, r)
+			if err != nil {
+				slog.Warn("user login failed", "error", err.Error())
+				onfailure(rw, r, err)
+				return
+			}
+
+			if err := auth.SaveSession(rw, r, user); err != nil {
+				return
+			}
+
+			slog.Info("login successfull", "user", user.UserName)
+			ctx := context.WithValue(r.Context(), "user", user)
+
+			if r.FormValue("redirect") != "" {
+				http.RedirectHandler(r.FormValue("redirect"), http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
+				return
+			}
+
+			http.RedirectHandler("/", http.StatusFound).ServeHTTP(rw, r.WithContext(ctx))
+			return
+		}
+
+		slog.Debug("login failed: no authenticator applied")
+		onfailure(rw, r, errors.New("no authenticator applied"))
+	})
+}
+
+func (auth *Authentication) Auth(
+	onsuccess http.Handler,
+	onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
+) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		user, err := auth.AuthViaSession(rw, r)
+		if err != nil {
+			slog.Info("auth -> authentication failed", "error", err.Error())
+			http.Error(rw, err.Error(), http.StatusUnauthorized)
+			return
+		}
+		if user != nil {
+			ctx := context.WithValue(r.Context(), "user", user)
+			onsuccess.ServeHTTP(rw, r.WithContext(ctx))
+			return
+		}
+
+		slog.Info("auth -> authentication failed")
+		onfailure(rw, r, errors.New("unauthorized (please login first)"))
+	})
+}
+
+func (auth *Authentication) Logout(onsuccess http.Handler) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		session, err := auth.sessionStore.Get(r, "session")
+		if err != nil {
+			http.Error(rw, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		if !session.IsNew {
+			session.Options.MaxAge = -1
+			if err := auth.sessionStore.Save(r, rw, session); err != nil {
+				http.Error(rw, err.Error(), http.StatusInternalServerError)
+				return
+			}
+		}
+
+		onsuccess.ServeHTTP(rw, r)
+	})
+}
diff --git a/internal/auth/local.go b/internal/auth/local.go
new file mode 100644
index 0000000..a92303b
--- /dev/null
+++ b/internal/auth/local.go
@@ -0,0 +1,47 @@
+// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
+// All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+package auth
+
+import (
+	"fmt"
+	"log/slog"
+	"net/http"
+
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
+	"golang.org/x/crypto/bcrypt"
+)
+
+type LocalAuthenticator struct {
+	auth *Authentication
+}
+
+var _ Authenticator = (*LocalAuthenticator)(nil)
+
+func (la *LocalAuthenticator) Init() error {
+	return nil
+}
+
+func (la *LocalAuthenticator) CanLogin(
+	user *repository.AppUser,
+	username string,
+	rw http.ResponseWriter,
+	r *http.Request,
+) (*repository.AppUser, bool) {
+	return user, user != nil
+}
+
+func (la *LocalAuthenticator) Login(
+	user *repository.AppUser,
+	rw http.ResponseWriter,
+	r *http.Request,
+) (*repository.AppUser, error) {
+	if e := bcrypt.CompareHashAndPassword([]byte(*user.UserPass),
+		[]byte(r.FormValue("password"))); e != nil {
+		slog.Error("AUTH/LOCAL > Authentication for user failed!", "user", user.UserName)
+		return nil, fmt.Errorf("Authentication failed")
+	}
+
+	return user, nil
+}
diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go
new file mode 100644
index 0000000..c2b9d12
--- /dev/null
+++ b/internal/handlers/admin.go
@@ -0,0 +1,33 @@
+package handlers
+
+import (
+	"html/template"
+	"log/slog"
+	"net/http"
+
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
+	"github.com/olivere/vite"
+)
+
+func AdminHandler() http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		tpl := template.Must(template.ParseFS(web.Templates, "templates/admin.html", "templates/base.html"))
+		viteFragment, err := vite.HTMLFragment(vite.Config{
+			FS:    web.StaticAssets,
+			IsDev: false,
+		})
+		if err != nil {
+			http.Error(w, "Error instantiating vite fragment", http.StatusInternalServerError)
+			return
+		}
+
+		data := web.PageData{
+			Title: "Admin",
+			Vite:  viteFragment,
+		}
+		if err := tpl.ExecuteTemplate(w, "base", data); err != nil {
+			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+			slog.Error("Error executing template", "error", err)
+		}
+	}
+}
diff --git a/internal/handlers/root.go b/internal/handlers/root.go
index a9628c7..d3809e5 100644
--- a/internal/handlers/root.go
+++ b/internal/handlers/root.go
@@ -8,29 +8,12 @@ import (
 	"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
 )
 
-type PageData struct {
-	Title string
-}
-
 func RootHandler() http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		slog.Info("Render root handler")
 		tpl := template.Must(template.ParseFS(web.Templates, "templates/index.html", "templates/base.html"))
 
-		// basefile, err := web.Templates.ReadFile("templates/base.html")
-		// if err != nil {
-		// 	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-		// 	slog.Error("Error reading template", "error", err)
-		// 	return
-		// }
-		// file, err := web.Templates.ReadFile("templates/index.html")
-		// if err != nil {
-		// 	http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-		// 	slog.Error("Error reading template", "error", err)
-		// 	return
-		// }
-
-		data := PageData{
+		data := web.PageData{
 			Title: "DyeForYarn",
 		}
 		if err := tpl.ExecuteTemplate(w, "base", data); err != nil {
diff --git a/internal/middleware/recover.go b/internal/middleware/recover.go
index e52a927..bb54a15 100644
--- a/internal/middleware/recover.go
+++ b/internal/middleware/recover.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 )
 
-func RecoverMiddleware(next http.Handler) http.Handler {
+func Recover(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		defer func() {
 			if err := recover(); err != nil {
diff --git a/internal/middleware/securedCheck.go b/internal/middleware/securedCheck.go
new file mode 100644
index 0000000..f65e017
--- /dev/null
+++ b/internal/middleware/securedCheck.go
@@ -0,0 +1,27 @@
+package middleware
+
+import (
+	"net/http"
+
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth"
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
+)
+
+func SecuredCheck(next http.Handler) http.Handler {
+	authHandle := auth.GetAuthInstance()
+
+	return authHandle.Auth(
+		// On success;
+		next,
+
+		// On failure:
+		func(rw http.ResponseWriter, r *http.Request, err error) {
+			rw.WriteHeader(http.StatusUnauthorized)
+			web.RenderTemplate(rw, "login", web.PageData{
+				Title:    "Authentication failed - ClusterCockpit",
+				MsgType:  "alert-danger",
+				Message:  err.Error(),
+				Redirect: r.RequestURI,
+			})
+		})
+}
diff --git a/internal/repository/db.go b/internal/repository/db.go
new file mode 100644
index 0000000..e77f620
--- /dev/null
+++ b/internal/repository/db.go
@@ -0,0 +1,31 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+
+package repository
+
+import (
+	"context"
+	"database/sql"
+)
+
+type DBTX interface {
+	ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
+	PrepareContext(context.Context, string) (*sql.Stmt, error)
+	QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
+	QueryRowContext(context.Context, string, ...interface{}) *sql.Row
+}
+
+func New(db DBTX) *Queries {
+	return &Queries{db: db}
+}
+
+type Queries struct {
+	db DBTX
+}
+
+func (q *Queries) WithTx(tx *sql.Tx) *Queries {
+	return &Queries{
+		db: tx,
+	}
+}
diff --git a/internal/repository/dbConn.go b/internal/repository/dbConn.go
index 3056b0d..7ff5e14 100644
--- a/internal/repository/dbConn.go
+++ b/internal/repository/dbConn.go
@@ -3,6 +3,7 @@ package repository
 import (
 	"database/sql"
 	"log/slog"
+	"os"
 	"sync"
 )
 
@@ -21,6 +22,11 @@ func Connect(dsnURI string) {
 		}
 
 		repo = New(dbConn)
+		err = checkDBVersion(dbConn)
+		if err != nil {
+			slog.Error("DB Connection: Failed DB version check", "error", err)
+			os.Exit(1)
+		}
 	})
 }
 
@@ -32,10 +38,11 @@ func GetConnection() (*sql.DB, error) {
 	return dbConn, nil
 }
 
-func GetRepository() (*Queries, error) {
+func GetRepository() *Queries {
 	if repo == nil {
 		slog.Error("Database connection not initialized!")
+		os.Exit(1)
 	}
 
-	return repo, nil
+	return repo
 }
diff --git a/internal/repository/migration.go b/internal/repository/migration.go
index 91e98ad..8d37323 100644
--- a/internal/repository/migration.go
+++ b/internal/repository/migration.go
@@ -36,14 +36,14 @@ func checkDBVersion(db *sql.DB) error {
 	v, dirty, err := m.Version()
 	if err != nil {
 		if err == migrate.ErrNilVersion {
-			slog.Warn("Legacy database without version or missing database file!")
+			slog.Error("Legacy database without version or missing database file!")
 		} else {
 			return err
 		}
 	}
 
 	if v < Version {
-		return fmt.Errorf("unsupported database version %d, need %d.\nPlease backup your database file and run cc-backend -migrate-db", v, Version)
+		return fmt.Errorf("unsupported database version %d, need %d.\nPlease backup your database file and run server -migrate-db", v, Version)
 	} else if v > Version {
 		return fmt.Errorf("unsupported database version %d, need %d.\nPlease refer to documentation how to downgrade db with external migrate tool", v, Version)
 	}
@@ -61,7 +61,7 @@ func getMigrateInstance(dsnURI string) (m *migrate.Migrate, err error) {
 		slog.Error("failed to get instance", "Error", err)
 	}
 
-	m, err = migrate.NewWithSourceInstance("iofs", d, dsnURI)
+	m, err = migrate.NewWithSourceInstance("iofs", d, "sqlite3://"+dsnURI)
 	if err != nil {
 		return m, err
 	}
@@ -75,7 +75,7 @@ func MigrateDB(db string) error {
 		return err
 	}
 
-	v, dirty, err := m.Version()
+	_, dirty, err := m.Version()
 	if err != nil {
 		if err == migrate.ErrNilVersion {
 			slog.Warn("Legacy database without version or missing database file!")
@@ -84,10 +84,6 @@ func MigrateDB(db string) error {
 		}
 	}
 
-	if v < Version {
-		slog.Info("unsupported database version %d, need %d.\nPlease backup your database file and run cc-backend -migrate-db", v, Version)
-	}
-
 	if dirty {
 		return fmt.Errorf("last migration to version %d has failed, please fix the db manually and force version with -force-db flag", Version)
 	}
diff --git a/internal/repository/migrations/0000_schema.up.sql b/internal/repository/migrations/0000_schema.up.sql
deleted file mode 100644
index 4ba339c..0000000
--- a/internal/repository/migrations/0000_schema.up.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TABLE news (
-    id INTEGER PRIMARY KEY,
-    name TEXT NOT NULL,
-    bio TEXT
-);
-
-CREATE TABLE retailer (
-    id INTEGER PRIMARY KEY,
-    name TEXT NOT NULL,
-    bio TEXT
-);
diff --git a/internal/repository/migrations/0000_schema.down.sql b/internal/repository/migrations/01_schema.down.sql
similarity index 100%
rename from internal/repository/migrations/0000_schema.down.sql
rename to internal/repository/migrations/01_schema.down.sql
diff --git a/internal/repository/migrations/01_schema.up.sql b/internal/repository/migrations/01_schema.up.sql
new file mode 100644
index 0000000..0513bfe
--- /dev/null
+++ b/internal/repository/migrations/01_schema.up.sql
@@ -0,0 +1,23 @@
+CREATE TABLE IF NOT EXISTS news (
+    id INTEGER PRIMARY KEY,
+    news_title TEXT NOT NULL,
+    news_text TEXT NOT NULL,
+    news_date DATETIME,
+    news_publish DATETIME,
+    display TINYINT NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS retailer (
+    id INTEGER PRIMARY KEY,
+    shopname TEXT NOT NULL,
+    url TEXT NOT NULL,
+    country TEXT NOT NULL,
+    display TINYINT NOT NULL DEFAULT 1
+);
+
+CREATE TABLE IF NOT EXISTS app_user (
+    user_name TEXT PRIMARY KEY,
+    user_pass TEXT DEFAULT NULL,
+    realname TEXT DEFAULT NULL,
+    email TEXT DEFAULT NULL
+);
diff --git a/internal/repository/models.go b/internal/repository/models.go
new file mode 100644
index 0000000..7706f4e
--- /dev/null
+++ b/internal/repository/models.go
@@ -0,0 +1,33 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+
+package repository
+
+import (
+	"time"
+)
+
+type AppUser struct {
+	UserName string  `db:"user_name" json:"userName"`
+	UserPass *string `db:"user_pass" json:"userPass"`
+	Realname *string `db:"realname" json:"realname"`
+	Email    *string `db:"email" json:"email"`
+}
+
+type News struct {
+	ID          int64      `db:"id" json:"id"`
+	NewsTitle   string     `db:"news_title" json:"newsTitle"`
+	NewsText    string     `db:"news_text" json:"newsText"`
+	NewsDate    *time.Time `db:"news_date" json:"newsDate"`
+	NewsPublish *time.Time `db:"news_publish" json:"newsPublish"`
+	Display     int64      `db:"display" json:"display"`
+}
+
+type Retailer struct {
+	ID       int64  `db:"id" json:"id"`
+	Shopname string `db:"shopname" json:"shopname"`
+	Url      string `db:"url" json:"url"`
+	Country  string `db:"country" json:"country"`
+	Display  int64  `db:"display" json:"display"`
+}
diff --git a/internal/repository/query.sql.go b/internal/repository/query.sql.go
new file mode 100644
index 0000000..7b8da4b
--- /dev/null
+++ b/internal/repository/query.sql.go
@@ -0,0 +1,386 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: query.sql
+
+package repository
+
+import (
+	"context"
+	"time"
+)
+
+const createNewsEntry = `-- name: CreateNewsEntry :exec
+INSERT INTO news (
+    news_title, news_text, news_date,
+    news_publish, display
+) VALUES (?, ?, ?, ?, ?)
+`
+
+type CreateNewsEntryParams struct {
+	NewsTitle   string     `db:"news_title" json:"newsTitle"`
+	NewsText    string     `db:"news_text" json:"newsText"`
+	NewsDate    *time.Time `db:"news_date" json:"newsDate"`
+	NewsPublish *time.Time `db:"news_publish" json:"newsPublish"`
+	Display     int64      `db:"display" json:"display"`
+}
+
+func (q *Queries) CreateNewsEntry(ctx context.Context, arg CreateNewsEntryParams) error {
+	_, err := q.db.ExecContext(ctx, createNewsEntry,
+		arg.NewsTitle,
+		arg.NewsText,
+		arg.NewsDate,
+		arg.NewsPublish,
+		arg.Display,
+	)
+	return err
+}
+
+const createRetailer = `-- name: CreateRetailer :exec
+INSERT INTO retailer (
+    shopname, url, country, display
+) VALUES (?, ?, ?, ?)
+`
+
+type CreateRetailerParams struct {
+	Shopname string `db:"shopname" json:"shopname"`
+	Url      string `db:"url" json:"url"`
+	Country  string `db:"country" json:"country"`
+	Display  int64  `db:"display" json:"display"`
+}
+
+func (q *Queries) CreateRetailer(ctx context.Context, arg CreateRetailerParams) error {
+	_, err := q.db.ExecContext(ctx, createRetailer,
+		arg.Shopname,
+		arg.Url,
+		arg.Country,
+		arg.Display,
+	)
+	return err
+}
+
+const createUser = `-- name: CreateUser :exec
+INSERT INTO app_user (
+    user_name, user_pass
+)
+VALUES (?, ?)
+`
+
+type CreateUserParams struct {
+	UserName string  `db:"user_name" json:"userName"`
+	UserPass *string `db:"user_pass" json:"userPass"`
+}
+
+func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
+	_, err := q.db.ExecContext(ctx, createUser, arg.UserName, arg.UserPass)
+	return err
+}
+
+const deleteNewsEntry = `-- name: DeleteNewsEntry :exec
+DELETE FROM news
+WHERE id = ?
+`
+
+func (q *Queries) DeleteNewsEntry(ctx context.Context, id int64) error {
+	_, err := q.db.ExecContext(ctx, deleteNewsEntry, id)
+	return err
+}
+
+const deleteRetailer = `-- name: DeleteRetailer :exec
+DELETE FROM news
+WHERE id = ?
+`
+
+func (q *Queries) DeleteRetailer(ctx context.Context, id int64) error {
+	_, err := q.db.ExecContext(ctx, deleteRetailer, id)
+	return err
+}
+
+const deleteUser = `-- name: DeleteUser :exec
+DELETE FROM app_user
+WHERE user_name = ?
+`
+
+func (q *Queries) DeleteUser(ctx context.Context, userName string) error {
+	_, err := q.db.ExecContext(ctx, deleteUser, userName)
+	return err
+}
+
+const getNewsEntry = `-- name: GetNewsEntry :one
+SELECT id, news_title, news_text, news_date, news_publish, display FROM news
+WHERE id = ? LIMIT 1
+`
+
+func (q *Queries) GetNewsEntry(ctx context.Context, id int64) (News, error) {
+	row := q.db.QueryRowContext(ctx, getNewsEntry, id)
+	var i News
+	err := row.Scan(
+		&i.ID,
+		&i.NewsTitle,
+		&i.NewsText,
+		&i.NewsDate,
+		&i.NewsPublish,
+		&i.Display,
+	)
+	return i, err
+}
+
+const getUser = `-- name: GetUser :one
+SELECT user_name, user_pass, realname, email FROM app_user
+WHERE user_name = ? LIMIT 1
+`
+
+func (q *Queries) GetUser(ctx context.Context, userName string) (AppUser, error) {
+	row := q.db.QueryRowContext(ctx, getUser, userName)
+	var i AppUser
+	err := row.Scan(
+		&i.UserName,
+		&i.UserPass,
+		&i.Realname,
+		&i.Email,
+	)
+	return i, err
+}
+
+const listActiveNews = `-- name: ListActiveNews :many
+SELECT id, news_title, news_text, news_date, news_publish, display FROM news
+WHERE display = 1
+ORDER BY news_date
+`
+
+func (q *Queries) ListActiveNews(ctx context.Context) ([]News, error) {
+	rows, err := q.db.QueryContext(ctx, listActiveNews)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []News
+	for rows.Next() {
+		var i News
+		if err := rows.Scan(
+			&i.ID,
+			&i.NewsTitle,
+			&i.NewsText,
+			&i.NewsDate,
+			&i.NewsPublish,
+			&i.Display,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const listActiveRetailers = `-- name: ListActiveRetailers :many
+SELECT id, shopname, url, country, display FROM retailer
+WHERE display = 1
+ORDER BY shopname
+`
+
+func (q *Queries) ListActiveRetailers(ctx context.Context) ([]Retailer, error) {
+	rows, err := q.db.QueryContext(ctx, listActiveRetailers)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []Retailer
+	for rows.Next() {
+		var i Retailer
+		if err := rows.Scan(
+			&i.ID,
+			&i.Shopname,
+			&i.Url,
+			&i.Country,
+			&i.Display,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const listNews = `-- name: ListNews :many
+SELECT id, news_title, news_text, news_date, news_publish, display FROM news
+ORDER BY news_date
+`
+
+func (q *Queries) ListNews(ctx context.Context) ([]News, error) {
+	rows, err := q.db.QueryContext(ctx, listNews)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []News
+	for rows.Next() {
+		var i News
+		if err := rows.Scan(
+			&i.ID,
+			&i.NewsTitle,
+			&i.NewsText,
+			&i.NewsDate,
+			&i.NewsPublish,
+			&i.Display,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const listRetailers = `-- name: ListRetailers :many
+SELECT id, shopname, url, country, display FROM retailer
+ORDER BY shopname
+`
+
+func (q *Queries) ListRetailers(ctx context.Context) ([]Retailer, error) {
+	rows, err := q.db.QueryContext(ctx, listRetailers)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []Retailer
+	for rows.Next() {
+		var i Retailer
+		if err := rows.Scan(
+			&i.ID,
+			&i.Shopname,
+			&i.Url,
+			&i.Country,
+			&i.Display,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const listUsers = `-- name: ListUsers :many
+SELECT user_name, user_pass, realname, email FROM app_user
+ORDER BY user_name
+`
+
+func (q *Queries) ListUsers(ctx context.Context) ([]AppUser, error) {
+	rows, err := q.db.QueryContext(ctx, listUsers)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []AppUser
+	for rows.Next() {
+		var i AppUser
+		if err := rows.Scan(
+			&i.UserName,
+			&i.UserPass,
+			&i.Realname,
+			&i.Email,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const updateNewsEntry = `-- name: UpdateNewsEntry :exec
+UPDATE news
+SET
+    news_title = ?, news_text = ?,
+    news_date = ?, news_publish = ?, display = ?
+WHERE id = ?
+`
+
+type UpdateNewsEntryParams struct {
+	NewsTitle   string     `db:"news_title" json:"newsTitle"`
+	NewsText    string     `db:"news_text" json:"newsText"`
+	NewsDate    *time.Time `db:"news_date" json:"newsDate"`
+	NewsPublish *time.Time `db:"news_publish" json:"newsPublish"`
+	Display     int64      `db:"display" json:"display"`
+	ID          int64      `db:"id" json:"id"`
+}
+
+func (q *Queries) UpdateNewsEntry(ctx context.Context, arg UpdateNewsEntryParams) error {
+	_, err := q.db.ExecContext(ctx, updateNewsEntry,
+		arg.NewsTitle,
+		arg.NewsText,
+		arg.NewsDate,
+		arg.NewsPublish,
+		arg.Display,
+		arg.ID,
+	)
+	return err
+}
+
+const updateRetailer = `-- name: UpdateRetailer :exec
+UPDATE retailer
+SET shopname = ?, url = ?, country = ?, display = ?
+WHERE id = ?
+`
+
+type UpdateRetailerParams struct {
+	Shopname string `db:"shopname" json:"shopname"`
+	Url      string `db:"url" json:"url"`
+	Country  string `db:"country" json:"country"`
+	Display  int64  `db:"display" json:"display"`
+	ID       int64  `db:"id" json:"id"`
+}
+
+func (q *Queries) UpdateRetailer(ctx context.Context, arg UpdateRetailerParams) error {
+	_, err := q.db.ExecContext(ctx, updateRetailer,
+		arg.Shopname,
+		arg.Url,
+		arg.Country,
+		arg.Display,
+		arg.ID,
+	)
+	return err
+}
+
+const updateUser = `-- name: UpdateUser :exec
+UPDATE app_user
+SET user_pass = ?
+WHERE user_name = ?
+`
+
+type UpdateUserParams struct {
+	UserPass *string `db:"user_pass" json:"userPass"`
+	UserName string  `db:"user_name" json:"userName"`
+}
+
+func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
+	_, err := q.db.ExecContext(ctx, updateUser, arg.UserPass, arg.UserName)
+	return err
+}
diff --git a/internal/repository/sql/query.sql b/internal/repository/sql/query.sql
index 212a226..3641ff0 100644
--- a/internal/repository/sql/query.sql
+++ b/internal/repository/sql/query.sql
@@ -1,25 +1,75 @@
--- name: GetAuthor :one
-SELECT * FROM authors
+-- name: GetUser :one
+SELECT * FROM app_user
+WHERE user_name = ? LIMIT 1;
+--
+-- name: ListUsers :many
+SELECT * FROM app_user
+ORDER BY user_name;
+
+-- name: CreateUser :exec
+INSERT INTO app_user (
+    user_name, user_pass
+)
+VALUES (?, ?);
+
+-- name: UpdateUser :exec
+UPDATE app_user
+SET user_pass = ?
+WHERE user_name = ?;
+
+-- name: DeleteUser :exec
+DELETE FROM app_user
+WHERE user_name = ?;
+--
+-- name: GetNewsEntry :one
+SELECT * FROM news
 WHERE id = ? LIMIT 1;
 
--- name: ListAuthors :many
-SELECT * FROM authors
-ORDER BY name;
+-- name: ListNews :many
+SELECT * FROM news
+ORDER BY news_date;
 
--- name: CreateAuthor :one
-INSERT INTO authors (
-  name, bio
-) VALUES (
-  ?, ?
-)
-RETURNING *;
+-- name: ListActiveNews :many
+SELECT * FROM news
+WHERE display = 1
+ORDER BY news_date;
 
--- name: UpdateAuthor :exec
-UPDATE authors
-set name = ?,
-bio = ?
+-- name: CreateNewsEntry :exec
+INSERT INTO news (
+    news_title, news_text, news_date,
+    news_publish, display
+) VALUES (?, ?, ?, ?, ?);
+
+-- name: UpdateNewsEntry :exec
+UPDATE news
+SET
+    news_title = ?, news_text = ?,
+    news_date = ?, news_publish = ?, display = ?
 WHERE id = ?;
 
--- name: DeleteAuthor :exec
-DELETE FROM authors
+-- name: DeleteNewsEntry :exec
+DELETE FROM news
+WHERE id = ?;
+--
+-- name: ListRetailers :many
+SELECT * FROM retailer
+ORDER BY shopname;
+
+-- name: ListActiveRetailers :many
+SELECT * FROM retailer
+WHERE display = 1
+ORDER BY shopname;
+
+-- name: CreateRetailer :exec
+INSERT INTO retailer (
+    shopname, url, country, display
+) VALUES (?, ?, ?, ?);
+
+-- name: UpdateRetailer :exec
+UPDATE retailer
+SET shopname = ?, url = ?, country = ?, display = ?
+WHERE id = ?;
+
+-- name: DeleteRetailer :exec
+DELETE FROM news
 WHERE id = ?;
diff --git a/main.go b/main.go
index a9f00a7..7262715 100644
--- a/main.go
+++ b/main.go
@@ -1,26 +1,26 @@
 package main
 
 import (
-	"embed"
+	"context"
 	"flag"
 	"fmt"
 	"io/fs"
 	"log/slog"
 	"net/http"
 	"os"
+	"strings"
 	"time"
 
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth"
 	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers"
 	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware"
 	"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
+	"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
 	"github.com/joho/godotenv"
 
 	_ "modernc.org/sqlite"
 )
 
-//go:embed web/static/*
-var static embed.FS
-
 func init() {
 	_, jsonLogger := os.LookupEnv("JSON_LOGGER")
 	_, debug := os.LookupEnv("DEBUG")
@@ -48,10 +48,14 @@ func init() {
 
 func main() {
 	var flagMigrateDB, flagRevertDB, flagForceDB bool
+	var flagNewUser, flagDelUser string
 
 	flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit")
 	flag.BoolVar(&flagRevertDB, "revert-db", false, "Migrate database to previous version and exit")
 	flag.BoolVar(&flagForceDB, "force-db", false, "Force database version, clear dirty flag and exit")
+	flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: <username>:<password>")
+	flag.StringVar(&flagDelUser, "del-user", "", "Remove a existing user. Argument format: <username>")
+	flag.Parse()
 
 	err := godotenv.Load()
 	if err != nil {
@@ -60,7 +64,7 @@ func main() {
 
 	dbURL := os.Getenv("DB")
 	if dbURL == "" {
-		dbURL = "file:app.db"
+		dbURL = "app.db"
 	}
 
 	if flagMigrateDB {
@@ -70,6 +74,7 @@ func main() {
 			os.Exit(1)
 		}
 		slog.Info("MigrateDB Success: Migrated database at location.\n", "version", repository.Version)
+		os.Exit(0)
 	}
 
 	if flagRevertDB {
@@ -79,6 +84,7 @@ func main() {
 			os.Exit(1)
 		}
 		slog.Info("RevertDB Success: Reverted database", "version", (repository.Version - 1))
+		os.Exit(0)
 	}
 
 	if flagForceDB {
@@ -87,7 +93,8 @@ func main() {
 			slog.Error("ForceDB Failed: Could not force database version", "version", repository.Version, "error", err)
 			os.Exit(1)
 		}
-		slog.Error("ForceDB Success: Forced database version", "version", repository.Version)
+		slog.Info("ForceDB Success: Forced database version", "version", repository.Version)
+		os.Exit(0)
 	}
 
 	repository.Connect(dbURL)
@@ -98,20 +105,58 @@ func main() {
 	}
 	addr := ":" + port
 
+	auth.Init()
+
+	if flagNewUser != "" {
+		parts := strings.SplitN(flagNewUser, ":", 2)
+		if len(parts) != 2 || len(parts[0]) == 0 {
+			slog.Error("Add User: Could not parse supplied argument format: No changes.\n"+
+				"Want: <username>::<password>\n", "have", flagNewUser)
+		}
+
+		ctx := context.Background()
+		q := repository.GetRepository()
+		if err := q.CreateUser(ctx, repository.CreateUserParams{
+			UserName: parts[0], UserPass: &parts[1],
+		}); err != nil {
+			slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error())
+			os.Exit(1)
+		} else {
+			slog.Info("add new user", "username", parts[0])
+		}
+		os.Exit(0)
+	}
+
+	if flagDelUser != "" {
+		ctx := context.Background()
+		q := repository.GetRepository()
+		if err := q.DeleteUser(ctx, flagDelUser); err != nil {
+			slog.Error("Delete User: Could not delete user", "username", flagDelUser, "error", err)
+			os.Exit(1)
+		} else {
+			slog.Info("deleted user from DB", "username", flagDelUser)
+		}
+		os.Exit(0)
+	}
+
 	mux := http.NewServeMux()
 
-	// Use an embedded filesystem rooted at "web/static"
-	fs, err := fs.Sub(static, "web/static")
+	sfs, err := fs.Sub(web.StaticAssets, "frontend/dist")
 	if err != nil {
 		slog.Error("Failed to create sub filesystem", "error", err)
 		return
 	}
-	// Serve files from the embedded /web/static directory at /static
-	fileServer := http.FileServer(http.FS(fs))
-	mux.Handle("GET /static/", http.StripPrefix("/static/", fileServer))
+	mux.Handle("GET /static/", http.StripPrefix("/static", http.FileServer(http.FS(sfs))))
+
+	afs, err := fs.Sub(web.StaticAssets, "frontend/dist/assets")
+	if err != nil {
+		slog.Error("Failed to create sub filesystem", "error", err)
+		return
+	}
+	mux.Handle("GET /assets/", http.StripPrefix("/assets", http.FileServer(http.FS(afs))))
 
 	mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {
-		data, err := static.ReadFile("web/static/favicon.ico")
+		data, err := web.StaticAssets.ReadFile("frontend/dist/favicon.ico")
 		if err != nil {
 			http.NotFound(w, r)
 			return
@@ -120,7 +165,7 @@ func main() {
 		w.Write(data)
 	})
 	mux.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) {
-		data, err := static.ReadFile("web/static/robots.txt")
+		data, err := web.StaticAssets.ReadFile("frontend/dist/robots.txt")
 		if err != nil {
 			http.NotFound(w, r)
 			return
@@ -134,10 +179,54 @@ func main() {
 		w.Write([]byte(`OK`))
 	})
 
+	mux.HandleFunc("GET /login", func(rw http.ResponseWriter, r *http.Request) {
+		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
+		web.RenderTemplate(rw, "login", web.PageData{Title: "Login"})
+	})
+	mux.HandleFunc("GET /imprint", func(rw http.ResponseWriter, r *http.Request) {
+		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
+		web.RenderTemplate(rw, "imprint", web.PageData{Title: "Imprint"})
+	})
+	mux.HandleFunc("GET /privacy", func(rw http.ResponseWriter, r *http.Request) {
+		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
+		web.RenderTemplate(rw, "privacy", web.PageData{Title: "Privacy"})
+	})
+
+	authHandle := auth.GetAuthInstance()
+	mux.Handle("POST /login", authHandle.Login(
+		func(rw http.ResponseWriter, r *http.Request, err error) {
+			rw.Header().Add("Content-Type", "text/html; charset=utf-8")
+			rw.WriteHeader(http.StatusUnauthorized)
+			web.RenderTemplate(rw, "login", web.PageData{
+				Title:   "Login failed - ClusterCockpit",
+				MsgType: "alert-warning",
+				Message: err.Error(),
+			})
+		}))
+
+	mux.Handle("POST /logout", authHandle.Logout(
+		http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+			rw.Header().Add("Content-Type", "text/html; charset=utf-8")
+			rw.WriteHeader(http.StatusOK)
+			web.RenderTemplate(rw, "login", web.PageData{
+				Title:   "Bye - ClusterCockpit",
+				MsgType: "alert-info",
+				Message: "Logout successful",
+			})
+		})))
+
 	mux.HandleFunc("GET /", handlers.RootHandler())
 
+	securedMux := http.NewServeMux()
+	securedMux.HandleFunc("GET /", handlers.AdminHandler())
+
+	securedChain := &middleware.Chain{}
+	securedChain.Use(middleware.SecuredCheck)
+
+	mux.Handle("GET /admin/", http.StripPrefix("/admin", securedChain.Then(securedMux)))
+
 	chain := &middleware.Chain{}
-	chain.Use(middleware.RecoverMiddleware)
+	chain.Use(middleware.Recover)
 	wrappedMux := chain.Then(mux)
 
 	server := &http.Server{
diff --git a/web/frontend/.gitignore b/web/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/web/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/web/frontend/README.md b/web/frontend/README.md
new file mode 100644
index 0000000..e6cd94f
--- /dev/null
+++ b/web/frontend/README.md
@@ -0,0 +1,47 @@
+# Svelte + TS + Vite
+
+This template should help get you started developing with Svelte and TypeScript in Vite.
+
+## Recommended IDE Setup
+
+[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
+
+## Need an official Svelte framework?
+
+Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
+
+## Technical considerations
+
+**Why use this over SvelteKit?**
+
+- It brings its own routing solution which might not be preferable for some users.
+- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
+
+This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
+
+Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
+
+**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
+
+Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
+
+**Why include `.vscode/extensions.json`?**
+
+Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
+
+**Why enable `allowJs` in the TS template?**
+
+While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
+
+**Why is HMR not preserving my local component state?**
+
+HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
+
+If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
+
+```ts
+// store.ts
+// An extremely simple external store
+import { writable } from 'svelte/store'
+export default writable(0)
+```
diff --git a/web/frontend/index.html b/web/frontend/index.html
new file mode 100644
index 0000000..b6c5f0a
--- /dev/null
+++ b/web/frontend/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite + Svelte + TS</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>
diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json
new file mode 100644
index 0000000..baaf567
--- /dev/null
+++ b/web/frontend/package-lock.json
@@ -0,0 +1,1412 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "frontend",
+      "version": "0.0.0",
+      "devDependencies": {
+        "@sveltejs/vite-plugin-svelte": "^5.0.3",
+        "@tsconfig/svelte": "^5.0.4",
+        "svelte": "^5.28.1",
+        "svelte-check": "^4.1.6",
+        "typescript": "~5.8.3",
+        "vite": "^6.3.5"
+      }
+    },
+    "node_modules/@ampproject/remapping": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+      "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+      "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+      "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+      "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+      "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+      "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+      "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+      "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+      "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+      "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+      "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+      "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+      "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+      "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+      "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+      "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+      "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+      "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+      "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+      "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+      "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+      "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+      "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+      "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+      "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
+      "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
+      "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
+      "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
+      "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
+      "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
+      "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
+      "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
+      "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
+      "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
+      "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
+      "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
+      "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
+      "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
+      "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
+      "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
+      "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
+      "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
+      "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
+      "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
+      "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@sveltejs/acorn-typescript": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
+      "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^8.9.0"
+      }
+    },
+    "node_modules/@sveltejs/vite-plugin-svelte": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz",
+      "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
+        "debug": "^4.4.0",
+        "deepmerge": "^4.3.1",
+        "kleur": "^4.1.5",
+        "magic-string": "^0.30.15",
+        "vitefu": "^1.0.4"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22"
+      },
+      "peerDependencies": {
+        "svelte": "^5.0.0",
+        "vite": "^6.0.0"
+      }
+    },
+    "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
+      "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.7"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22"
+      },
+      "peerDependencies": {
+        "@sveltejs/vite-plugin-svelte": "^5.0.0",
+        "svelte": "^5.0.0",
+        "vite": "^6.0.0"
+      }
+    },
+    "node_modules/@tsconfig/svelte": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
+      "integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+      "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/acorn": {
+      "version": "8.14.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+      "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/aria-query": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+      "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/axobject-query": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+      "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+      "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.5",
+        "@esbuild/android-arm": "0.25.5",
+        "@esbuild/android-arm64": "0.25.5",
+        "@esbuild/android-x64": "0.25.5",
+        "@esbuild/darwin-arm64": "0.25.5",
+        "@esbuild/darwin-x64": "0.25.5",
+        "@esbuild/freebsd-arm64": "0.25.5",
+        "@esbuild/freebsd-x64": "0.25.5",
+        "@esbuild/linux-arm": "0.25.5",
+        "@esbuild/linux-arm64": "0.25.5",
+        "@esbuild/linux-ia32": "0.25.5",
+        "@esbuild/linux-loong64": "0.25.5",
+        "@esbuild/linux-mips64el": "0.25.5",
+        "@esbuild/linux-ppc64": "0.25.5",
+        "@esbuild/linux-riscv64": "0.25.5",
+        "@esbuild/linux-s390x": "0.25.5",
+        "@esbuild/linux-x64": "0.25.5",
+        "@esbuild/netbsd-arm64": "0.25.5",
+        "@esbuild/netbsd-x64": "0.25.5",
+        "@esbuild/openbsd-arm64": "0.25.5",
+        "@esbuild/openbsd-x64": "0.25.5",
+        "@esbuild/sunos-x64": "0.25.5",
+        "@esbuild/win32-arm64": "0.25.5",
+        "@esbuild/win32-ia32": "0.25.5",
+        "@esbuild/win32-x64": "0.25.5"
+      }
+    },
+    "node_modules/esm-env": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+      "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esrap": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz",
+      "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.4.15"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.4.5",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
+      "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "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,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/is-reference": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+      "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.6"
+      }
+    },
+    "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,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/locate-character": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+      "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.17",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+      "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/mri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "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,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.4",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
+      "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.41.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
+      "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.7"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.41.1",
+        "@rollup/rollup-android-arm64": "4.41.1",
+        "@rollup/rollup-darwin-arm64": "4.41.1",
+        "@rollup/rollup-darwin-x64": "4.41.1",
+        "@rollup/rollup-freebsd-arm64": "4.41.1",
+        "@rollup/rollup-freebsd-x64": "4.41.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.41.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.41.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.41.1",
+        "@rollup/rollup-linux-arm64-musl": "4.41.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.41.1",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.41.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.41.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.41.1",
+        "@rollup/rollup-linux-x64-gnu": "4.41.1",
+        "@rollup/rollup-linux-x64-musl": "4.41.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.41.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.41.1",
+        "@rollup/rollup-win32-x64-msvc": "4.41.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/sade": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mri": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/svelte": {
+      "version": "5.33.11",
+      "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.11.tgz",
+      "integrity": "sha512-BVnvd6T3OShNvsRwYPXdseSO5rnQ4SljmhLVCCpBX1nEQI+e2TopOlazo4z+1+aUukyHZxlIVg3hpZ5TMugrMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ampproject/remapping": "^2.3.0",
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@sveltejs/acorn-typescript": "^1.0.5",
+        "@types/estree": "^1.0.5",
+        "acorn": "^8.12.1",
+        "aria-query": "^5.3.1",
+        "axobject-query": "^4.1.0",
+        "clsx": "^2.1.1",
+        "esm-env": "^1.2.1",
+        "esrap": "^1.4.6",
+        "is-reference": "^3.0.3",
+        "locate-character": "^3.0.0",
+        "magic-string": "^0.30.11",
+        "zimmerframe": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/svelte-check": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.1.tgz",
+      "integrity": "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.25",
+        "chokidar": "^4.0.1",
+        "fdir": "^6.2.0",
+        "picocolors": "^1.0.0",
+        "sade": "^1.7.4"
+      },
+      "bin": {
+        "svelte-check": "bin/svelte-check"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "peerDependencies": {
+        "svelte": "^4.0.0 || ^5.0.0-next.0",
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+      "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/vite": {
+      "version": "6.3.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+      "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitefu": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
+      "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
+      "dev": true,
+      "license": "MIT",
+      "workspaces": [
+        "tests/deps/*",
+        "tests/projects/*"
+      ],
+      "peerDependencies": {
+        "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+      },
+      "peerDependenciesMeta": {
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/zimmerframe": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
+      "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
+      "dev": true,
+      "license": "MIT"
+    }
+  }
+}
diff --git a/web/frontend/package.json b/web/frontend/package.json
new file mode 100644
index 0000000..76ee115
--- /dev/null
+++ b/web/frontend/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "frontend",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
+  },
+  "devDependencies": {
+    "@sveltejs/vite-plugin-svelte": "^5.0.3",
+    "@tsconfig/svelte": "^5.0.4",
+    "svelte": "^5.28.1",
+    "svelte-check": "^4.1.6",
+    "typescript": "~5.8.3",
+    "vite": "^6.3.5"
+  }
+}
diff --git a/web/static/css/app.css b/web/frontend/public/css/app.css
similarity index 94%
rename from web/static/css/app.css
rename to web/frontend/public/css/app.css
index e85a61c..9e084ae 100644
--- a/web/static/css/app.css
+++ b/web/frontend/public/css/app.css
@@ -115,6 +115,25 @@ p {
   transition: background 0.3s ease-in-out;
 }
 
+.container {
+  max-width: 100vw;
+}
+
+.site {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.site-content {
+  flex: 1 0 auto;
+  margin-top: 80px;
+}
+
+.site-footer {
+  flex: none;
+}
+
 .content-section {
   padding-top: 200px;
 }
diff --git a/web/static/css/bootstrap-grid.css b/web/frontend/public/css/bootstrap-grid.css
similarity index 100%
rename from web/static/css/bootstrap-grid.css
rename to web/frontend/public/css/bootstrap-grid.css
diff --git a/web/static/css/bootstrap-grid.css.map b/web/frontend/public/css/bootstrap-grid.css.map
similarity index 100%
rename from web/static/css/bootstrap-grid.css.map
rename to web/frontend/public/css/bootstrap-grid.css.map
diff --git a/web/static/css/bootstrap-grid.min.css b/web/frontend/public/css/bootstrap-grid.min.css
similarity index 100%
rename from web/static/css/bootstrap-grid.min.css
rename to web/frontend/public/css/bootstrap-grid.min.css
diff --git a/web/static/css/bootstrap-grid.min.css.map b/web/frontend/public/css/bootstrap-grid.min.css.map
similarity index 100%
rename from web/static/css/bootstrap-grid.min.css.map
rename to web/frontend/public/css/bootstrap-grid.min.css.map
diff --git a/web/static/css/bootstrap-grid.rtl.css b/web/frontend/public/css/bootstrap-grid.rtl.css
similarity index 100%
rename from web/static/css/bootstrap-grid.rtl.css
rename to web/frontend/public/css/bootstrap-grid.rtl.css
diff --git a/web/static/css/bootstrap-grid.rtl.css.map b/web/frontend/public/css/bootstrap-grid.rtl.css.map
similarity index 100%
rename from web/static/css/bootstrap-grid.rtl.css.map
rename to web/frontend/public/css/bootstrap-grid.rtl.css.map
diff --git a/web/static/css/bootstrap-grid.rtl.min.css b/web/frontend/public/css/bootstrap-grid.rtl.min.css
similarity index 100%
rename from web/static/css/bootstrap-grid.rtl.min.css
rename to web/frontend/public/css/bootstrap-grid.rtl.min.css
diff --git a/web/static/css/bootstrap-grid.rtl.min.css.map b/web/frontend/public/css/bootstrap-grid.rtl.min.css.map
similarity index 100%
rename from web/static/css/bootstrap-grid.rtl.min.css.map
rename to web/frontend/public/css/bootstrap-grid.rtl.min.css.map
diff --git a/web/static/css/bootstrap-icons.css b/web/frontend/public/css/bootstrap-icons.css
similarity index 100%
rename from web/static/css/bootstrap-icons.css
rename to web/frontend/public/css/bootstrap-icons.css
diff --git a/web/static/css/bootstrap-icons.json b/web/frontend/public/css/bootstrap-icons.json
similarity index 100%
rename from web/static/css/bootstrap-icons.json
rename to web/frontend/public/css/bootstrap-icons.json
diff --git a/web/static/css/bootstrap-icons.min.css b/web/frontend/public/css/bootstrap-icons.min.css
similarity index 100%
rename from web/static/css/bootstrap-icons.min.css
rename to web/frontend/public/css/bootstrap-icons.min.css
diff --git a/web/static/css/bootstrap-reboot.css b/web/frontend/public/css/bootstrap-reboot.css
similarity index 100%
rename from web/static/css/bootstrap-reboot.css
rename to web/frontend/public/css/bootstrap-reboot.css
diff --git a/web/static/css/bootstrap-reboot.css.map b/web/frontend/public/css/bootstrap-reboot.css.map
similarity index 100%
rename from web/static/css/bootstrap-reboot.css.map
rename to web/frontend/public/css/bootstrap-reboot.css.map
diff --git a/web/static/css/bootstrap-reboot.min.css b/web/frontend/public/css/bootstrap-reboot.min.css
similarity index 100%
rename from web/static/css/bootstrap-reboot.min.css
rename to web/frontend/public/css/bootstrap-reboot.min.css
diff --git a/web/static/css/bootstrap-reboot.min.css.map b/web/frontend/public/css/bootstrap-reboot.min.css.map
similarity index 100%
rename from web/static/css/bootstrap-reboot.min.css.map
rename to web/frontend/public/css/bootstrap-reboot.min.css.map
diff --git a/web/static/css/bootstrap-reboot.rtl.css b/web/frontend/public/css/bootstrap-reboot.rtl.css
similarity index 100%
rename from web/static/css/bootstrap-reboot.rtl.css
rename to web/frontend/public/css/bootstrap-reboot.rtl.css
diff --git a/web/static/css/bootstrap-reboot.rtl.css.map b/web/frontend/public/css/bootstrap-reboot.rtl.css.map
similarity index 100%
rename from web/static/css/bootstrap-reboot.rtl.css.map
rename to web/frontend/public/css/bootstrap-reboot.rtl.css.map
diff --git a/web/static/css/bootstrap-reboot.rtl.min.css b/web/frontend/public/css/bootstrap-reboot.rtl.min.css
similarity index 100%
rename from web/static/css/bootstrap-reboot.rtl.min.css
rename to web/frontend/public/css/bootstrap-reboot.rtl.min.css
diff --git a/web/static/css/bootstrap-reboot.rtl.min.css.map b/web/frontend/public/css/bootstrap-reboot.rtl.min.css.map
similarity index 100%
rename from web/static/css/bootstrap-reboot.rtl.min.css.map
rename to web/frontend/public/css/bootstrap-reboot.rtl.min.css.map
diff --git a/web/static/css/bootstrap-utilities.css b/web/frontend/public/css/bootstrap-utilities.css
similarity index 100%
rename from web/static/css/bootstrap-utilities.css
rename to web/frontend/public/css/bootstrap-utilities.css
diff --git a/web/static/css/bootstrap-utilities.css.map b/web/frontend/public/css/bootstrap-utilities.css.map
similarity index 100%
rename from web/static/css/bootstrap-utilities.css.map
rename to web/frontend/public/css/bootstrap-utilities.css.map
diff --git a/web/static/css/bootstrap-utilities.min.css b/web/frontend/public/css/bootstrap-utilities.min.css
similarity index 100%
rename from web/static/css/bootstrap-utilities.min.css
rename to web/frontend/public/css/bootstrap-utilities.min.css
diff --git a/web/static/css/bootstrap-utilities.min.css.map b/web/frontend/public/css/bootstrap-utilities.min.css.map
similarity index 100%
rename from web/static/css/bootstrap-utilities.min.css.map
rename to web/frontend/public/css/bootstrap-utilities.min.css.map
diff --git a/web/static/css/bootstrap-utilities.rtl.css b/web/frontend/public/css/bootstrap-utilities.rtl.css
similarity index 100%
rename from web/static/css/bootstrap-utilities.rtl.css
rename to web/frontend/public/css/bootstrap-utilities.rtl.css
diff --git a/web/static/css/bootstrap-utilities.rtl.css.map b/web/frontend/public/css/bootstrap-utilities.rtl.css.map
similarity index 100%
rename from web/static/css/bootstrap-utilities.rtl.css.map
rename to web/frontend/public/css/bootstrap-utilities.rtl.css.map
diff --git a/web/static/css/bootstrap-utilities.rtl.min.css b/web/frontend/public/css/bootstrap-utilities.rtl.min.css
similarity index 100%
rename from web/static/css/bootstrap-utilities.rtl.min.css
rename to web/frontend/public/css/bootstrap-utilities.rtl.min.css
diff --git a/web/static/css/bootstrap-utilities.rtl.min.css.map b/web/frontend/public/css/bootstrap-utilities.rtl.min.css.map
similarity index 100%
rename from web/static/css/bootstrap-utilities.rtl.min.css.map
rename to web/frontend/public/css/bootstrap-utilities.rtl.min.css.map
diff --git a/web/static/css/bootstrap.css b/web/frontend/public/css/bootstrap.css
similarity index 100%
rename from web/static/css/bootstrap.css
rename to web/frontend/public/css/bootstrap.css
diff --git a/web/static/css/bootstrap.css.map b/web/frontend/public/css/bootstrap.css.map
similarity index 100%
rename from web/static/css/bootstrap.css.map
rename to web/frontend/public/css/bootstrap.css.map
diff --git a/web/static/css/bootstrap.min.css b/web/frontend/public/css/bootstrap.min.css
similarity index 100%
rename from web/static/css/bootstrap.min.css
rename to web/frontend/public/css/bootstrap.min.css
diff --git a/web/static/css/bootstrap.min.css.map b/web/frontend/public/css/bootstrap.min.css.map
similarity index 100%
rename from web/static/css/bootstrap.min.css.map
rename to web/frontend/public/css/bootstrap.min.css.map
diff --git a/web/static/css/bootstrap.rtl.css b/web/frontend/public/css/bootstrap.rtl.css
similarity index 100%
rename from web/static/css/bootstrap.rtl.css
rename to web/frontend/public/css/bootstrap.rtl.css
diff --git a/web/static/css/bootstrap.rtl.css.map b/web/frontend/public/css/bootstrap.rtl.css.map
similarity index 100%
rename from web/static/css/bootstrap.rtl.css.map
rename to web/frontend/public/css/bootstrap.rtl.css.map
diff --git a/web/static/css/bootstrap.rtl.min.css b/web/frontend/public/css/bootstrap.rtl.min.css
similarity index 100%
rename from web/static/css/bootstrap.rtl.min.css
rename to web/frontend/public/css/bootstrap.rtl.min.css
diff --git a/web/static/css/bootstrap.rtl.min.css.map b/web/frontend/public/css/bootstrap.rtl.min.css.map
similarity index 100%
rename from web/static/css/bootstrap.rtl.min.css.map
rename to web/frontend/public/css/bootstrap.rtl.min.css.map
diff --git a/web/static/css/fonts/bootstrap-icons.woff b/web/frontend/public/css/fonts/bootstrap-icons.woff
similarity index 100%
rename from web/static/css/fonts/bootstrap-icons.woff
rename to web/frontend/public/css/fonts/bootstrap-icons.woff
diff --git a/web/static/css/fonts/bootstrap-icons.woff2 b/web/frontend/public/css/fonts/bootstrap-icons.woff2
similarity index 100%
rename from web/static/css/fonts/bootstrap-icons.woff2
rename to web/frontend/public/css/fonts/bootstrap-icons.woff2
diff --git a/web/static/favicon.ico b/web/frontend/public/favicon.ico
similarity index 100%
rename from web/static/favicon.ico
rename to web/frontend/public/favicon.ico
diff --git a/web/static/img/Bromeliad.jpg b/web/frontend/public/img/Bromeliad.jpg
similarity index 100%
rename from web/static/img/Bromeliad.jpg
rename to web/frontend/public/img/Bromeliad.jpg
diff --git a/web/static/img/RookieStripes.jpg b/web/frontend/public/img/RookieStripes.jpg
similarity index 100%
rename from web/static/img/RookieStripes.jpg
rename to web/frontend/public/img/RookieStripes.jpg
diff --git a/web/static/img/TreeOfLight.jpg b/web/frontend/public/img/TreeOfLight.jpg
similarity index 100%
rename from web/static/img/TreeOfLight.jpg
rename to web/frontend/public/img/TreeOfLight.jpg
diff --git a/web/static/img/action.jpg b/web/frontend/public/img/action.jpg
similarity index 100%
rename from web/static/img/action.jpg
rename to web/frontend/public/img/action.jpg
diff --git a/web/static/img/anniversary/image-1.jpg b/web/frontend/public/img/anniversary/image-1.jpg
similarity index 100%
rename from web/static/img/anniversary/image-1.jpg
rename to web/frontend/public/img/anniversary/image-1.jpg
diff --git a/web/static/img/anniversary/image-10.jpg b/web/frontend/public/img/anniversary/image-10.jpg
similarity index 100%
rename from web/static/img/anniversary/image-10.jpg
rename to web/frontend/public/img/anniversary/image-10.jpg
diff --git a/web/static/img/anniversary/image-11.jpg b/web/frontend/public/img/anniversary/image-11.jpg
similarity index 100%
rename from web/static/img/anniversary/image-11.jpg
rename to web/frontend/public/img/anniversary/image-11.jpg
diff --git a/web/static/img/anniversary/image-12.png b/web/frontend/public/img/anniversary/image-12.png
similarity index 100%
rename from web/static/img/anniversary/image-12.png
rename to web/frontend/public/img/anniversary/image-12.png
diff --git a/web/static/img/anniversary/image-13.jpg b/web/frontend/public/img/anniversary/image-13.jpg
similarity index 100%
rename from web/static/img/anniversary/image-13.jpg
rename to web/frontend/public/img/anniversary/image-13.jpg
diff --git a/web/static/img/anniversary/image-14.jpg b/web/frontend/public/img/anniversary/image-14.jpg
similarity index 100%
rename from web/static/img/anniversary/image-14.jpg
rename to web/frontend/public/img/anniversary/image-14.jpg
diff --git a/web/static/img/anniversary/image-15.png b/web/frontend/public/img/anniversary/image-15.png
similarity index 100%
rename from web/static/img/anniversary/image-15.png
rename to web/frontend/public/img/anniversary/image-15.png
diff --git a/web/static/img/anniversary/image-16.jpg b/web/frontend/public/img/anniversary/image-16.jpg
similarity index 100%
rename from web/static/img/anniversary/image-16.jpg
rename to web/frontend/public/img/anniversary/image-16.jpg
diff --git a/web/static/img/anniversary/image-17.png b/web/frontend/public/img/anniversary/image-17.png
similarity index 100%
rename from web/static/img/anniversary/image-17.png
rename to web/frontend/public/img/anniversary/image-17.png
diff --git a/web/static/img/anniversary/image-18.jpg b/web/frontend/public/img/anniversary/image-18.jpg
similarity index 100%
rename from web/static/img/anniversary/image-18.jpg
rename to web/frontend/public/img/anniversary/image-18.jpg
diff --git a/web/static/img/anniversary/image-19.jpg b/web/frontend/public/img/anniversary/image-19.jpg
similarity index 100%
rename from web/static/img/anniversary/image-19.jpg
rename to web/frontend/public/img/anniversary/image-19.jpg
diff --git a/web/static/img/anniversary/image-2.jpg b/web/frontend/public/img/anniversary/image-2.jpg
similarity index 100%
rename from web/static/img/anniversary/image-2.jpg
rename to web/frontend/public/img/anniversary/image-2.jpg
diff --git a/web/static/img/anniversary/image-20.jpg b/web/frontend/public/img/anniversary/image-20.jpg
similarity index 100%
rename from web/static/img/anniversary/image-20.jpg
rename to web/frontend/public/img/anniversary/image-20.jpg
diff --git a/web/static/img/anniversary/image-21.jpg b/web/frontend/public/img/anniversary/image-21.jpg
similarity index 100%
rename from web/static/img/anniversary/image-21.jpg
rename to web/frontend/public/img/anniversary/image-21.jpg
diff --git a/web/static/img/anniversary/image-22.jpg b/web/frontend/public/img/anniversary/image-22.jpg
similarity index 100%
rename from web/static/img/anniversary/image-22.jpg
rename to web/frontend/public/img/anniversary/image-22.jpg
diff --git a/web/static/img/anniversary/image-23.jpg b/web/frontend/public/img/anniversary/image-23.jpg
similarity index 100%
rename from web/static/img/anniversary/image-23.jpg
rename to web/frontend/public/img/anniversary/image-23.jpg
diff --git a/web/static/img/anniversary/image-24.jpg b/web/frontend/public/img/anniversary/image-24.jpg
similarity index 100%
rename from web/static/img/anniversary/image-24.jpg
rename to web/frontend/public/img/anniversary/image-24.jpg
diff --git a/web/static/img/anniversary/image-25.jpg b/web/frontend/public/img/anniversary/image-25.jpg
similarity index 100%
rename from web/static/img/anniversary/image-25.jpg
rename to web/frontend/public/img/anniversary/image-25.jpg
diff --git a/web/static/img/anniversary/image-26.jpg b/web/frontend/public/img/anniversary/image-26.jpg
similarity index 100%
rename from web/static/img/anniversary/image-26.jpg
rename to web/frontend/public/img/anniversary/image-26.jpg
diff --git a/web/static/img/anniversary/image-27.jpg b/web/frontend/public/img/anniversary/image-27.jpg
similarity index 100%
rename from web/static/img/anniversary/image-27.jpg
rename to web/frontend/public/img/anniversary/image-27.jpg
diff --git a/web/static/img/anniversary/image-28.jpg b/web/frontend/public/img/anniversary/image-28.jpg
similarity index 100%
rename from web/static/img/anniversary/image-28.jpg
rename to web/frontend/public/img/anniversary/image-28.jpg
diff --git a/web/static/img/anniversary/image-29.jpg b/web/frontend/public/img/anniversary/image-29.jpg
similarity index 100%
rename from web/static/img/anniversary/image-29.jpg
rename to web/frontend/public/img/anniversary/image-29.jpg
diff --git a/web/static/img/anniversary/image-3.jpg b/web/frontend/public/img/anniversary/image-3.jpg
similarity index 100%
rename from web/static/img/anniversary/image-3.jpg
rename to web/frontend/public/img/anniversary/image-3.jpg
diff --git a/web/static/img/anniversary/image-30.jpg b/web/frontend/public/img/anniversary/image-30.jpg
similarity index 100%
rename from web/static/img/anniversary/image-30.jpg
rename to web/frontend/public/img/anniversary/image-30.jpg
diff --git a/web/static/img/anniversary/image-31.jpg b/web/frontend/public/img/anniversary/image-31.jpg
similarity index 100%
rename from web/static/img/anniversary/image-31.jpg
rename to web/frontend/public/img/anniversary/image-31.jpg
diff --git a/web/static/img/anniversary/image-32.jpg b/web/frontend/public/img/anniversary/image-32.jpg
similarity index 100%
rename from web/static/img/anniversary/image-32.jpg
rename to web/frontend/public/img/anniversary/image-32.jpg
diff --git a/web/static/img/anniversary/image-33.jpg b/web/frontend/public/img/anniversary/image-33.jpg
similarity index 100%
rename from web/static/img/anniversary/image-33.jpg
rename to web/frontend/public/img/anniversary/image-33.jpg
diff --git a/web/static/img/anniversary/image-34.jpg b/web/frontend/public/img/anniversary/image-34.jpg
similarity index 100%
rename from web/static/img/anniversary/image-34.jpg
rename to web/frontend/public/img/anniversary/image-34.jpg
diff --git a/web/static/img/anniversary/image-35.jpg b/web/frontend/public/img/anniversary/image-35.jpg
similarity index 100%
rename from web/static/img/anniversary/image-35.jpg
rename to web/frontend/public/img/anniversary/image-35.jpg
diff --git a/web/static/img/anniversary/image-36.jpg b/web/frontend/public/img/anniversary/image-36.jpg
similarity index 100%
rename from web/static/img/anniversary/image-36.jpg
rename to web/frontend/public/img/anniversary/image-36.jpg
diff --git a/web/static/img/anniversary/image-37.jpg b/web/frontend/public/img/anniversary/image-37.jpg
similarity index 100%
rename from web/static/img/anniversary/image-37.jpg
rename to web/frontend/public/img/anniversary/image-37.jpg
diff --git a/web/static/img/anniversary/image-38.jpg b/web/frontend/public/img/anniversary/image-38.jpg
similarity index 100%
rename from web/static/img/anniversary/image-38.jpg
rename to web/frontend/public/img/anniversary/image-38.jpg
diff --git a/web/static/img/anniversary/image-39.jpg b/web/frontend/public/img/anniversary/image-39.jpg
similarity index 100%
rename from web/static/img/anniversary/image-39.jpg
rename to web/frontend/public/img/anniversary/image-39.jpg
diff --git a/web/static/img/anniversary/image-4.jpg b/web/frontend/public/img/anniversary/image-4.jpg
similarity index 100%
rename from web/static/img/anniversary/image-4.jpg
rename to web/frontend/public/img/anniversary/image-4.jpg
diff --git a/web/static/img/anniversary/image-40.jpg b/web/frontend/public/img/anniversary/image-40.jpg
similarity index 100%
rename from web/static/img/anniversary/image-40.jpg
rename to web/frontend/public/img/anniversary/image-40.jpg
diff --git a/web/static/img/anniversary/image-41.jpg b/web/frontend/public/img/anniversary/image-41.jpg
similarity index 100%
rename from web/static/img/anniversary/image-41.jpg
rename to web/frontend/public/img/anniversary/image-41.jpg
diff --git a/web/static/img/anniversary/image-42.jpg b/web/frontend/public/img/anniversary/image-42.jpg
similarity index 100%
rename from web/static/img/anniversary/image-42.jpg
rename to web/frontend/public/img/anniversary/image-42.jpg
diff --git a/web/static/img/anniversary/image-43.jpg b/web/frontend/public/img/anniversary/image-43.jpg
similarity index 100%
rename from web/static/img/anniversary/image-43.jpg
rename to web/frontend/public/img/anniversary/image-43.jpg
diff --git a/web/static/img/anniversary/image-44.jpg b/web/frontend/public/img/anniversary/image-44.jpg
similarity index 100%
rename from web/static/img/anniversary/image-44.jpg
rename to web/frontend/public/img/anniversary/image-44.jpg
diff --git a/web/static/img/anniversary/image-45.jpg b/web/frontend/public/img/anniversary/image-45.jpg
similarity index 100%
rename from web/static/img/anniversary/image-45.jpg
rename to web/frontend/public/img/anniversary/image-45.jpg
diff --git a/web/static/img/anniversary/image-46.jpg b/web/frontend/public/img/anniversary/image-46.jpg
similarity index 100%
rename from web/static/img/anniversary/image-46.jpg
rename to web/frontend/public/img/anniversary/image-46.jpg
diff --git a/web/static/img/anniversary/image-47.jpg b/web/frontend/public/img/anniversary/image-47.jpg
similarity index 100%
rename from web/static/img/anniversary/image-47.jpg
rename to web/frontend/public/img/anniversary/image-47.jpg
diff --git a/web/static/img/anniversary/image-48.jpg b/web/frontend/public/img/anniversary/image-48.jpg
similarity index 100%
rename from web/static/img/anniversary/image-48.jpg
rename to web/frontend/public/img/anniversary/image-48.jpg
diff --git a/web/static/img/anniversary/image-49.jpg b/web/frontend/public/img/anniversary/image-49.jpg
similarity index 100%
rename from web/static/img/anniversary/image-49.jpg
rename to web/frontend/public/img/anniversary/image-49.jpg
diff --git a/web/static/img/anniversary/image-5.jpg b/web/frontend/public/img/anniversary/image-5.jpg
similarity index 100%
rename from web/static/img/anniversary/image-5.jpg
rename to web/frontend/public/img/anniversary/image-5.jpg
diff --git a/web/static/img/anniversary/image-50.jpg b/web/frontend/public/img/anniversary/image-50.jpg
similarity index 100%
rename from web/static/img/anniversary/image-50.jpg
rename to web/frontend/public/img/anniversary/image-50.jpg
diff --git a/web/static/img/anniversary/image-6.jpg b/web/frontend/public/img/anniversary/image-6.jpg
similarity index 100%
rename from web/static/img/anniversary/image-6.jpg
rename to web/frontend/public/img/anniversary/image-6.jpg
diff --git a/web/static/img/anniversary/image-7.jpg b/web/frontend/public/img/anniversary/image-7.jpg
similarity index 100%
rename from web/static/img/anniversary/image-7.jpg
rename to web/frontend/public/img/anniversary/image-7.jpg
diff --git a/web/static/img/anniversary/image-8.jpg b/web/frontend/public/img/anniversary/image-8.jpg
similarity index 100%
rename from web/static/img/anniversary/image-8.jpg
rename to web/frontend/public/img/anniversary/image-8.jpg
diff --git a/web/static/img/anniversary/image-9.jpg b/web/frontend/public/img/anniversary/image-9.jpg
similarity index 100%
rename from web/static/img/anniversary/image-9.jpg
rename to web/frontend/public/img/anniversary/image-9.jpg
diff --git a/web/static/img/cordula.jpg b/web/frontend/public/img/cordula.jpg
similarity index 100%
rename from web/static/img/cordula.jpg
rename to web/frontend/public/img/cordula.jpg
diff --git a/web/static/img/downloads-bg.jpg b/web/frontend/public/img/downloads-bg.jpg
similarity index 100%
rename from web/static/img/downloads-bg.jpg
rename to web/frontend/public/img/downloads-bg.jpg
diff --git a/web/static/img/farben.jpg b/web/frontend/public/img/farben.jpg
similarity index 100%
rename from web/static/img/farben.jpg
rename to web/frontend/public/img/farben.jpg
diff --git a/web/static/img/flags/Argentina.png b/web/frontend/public/img/flags/Argentina.png
similarity index 100%
rename from web/static/img/flags/Argentina.png
rename to web/frontend/public/img/flags/Argentina.png
diff --git a/web/static/img/flags/Australia.png b/web/frontend/public/img/flags/Australia.png
similarity index 100%
rename from web/static/img/flags/Australia.png
rename to web/frontend/public/img/flags/Australia.png
diff --git a/web/static/img/flags/Austria.png b/web/frontend/public/img/flags/Austria.png
similarity index 100%
rename from web/static/img/flags/Austria.png
rename to web/frontend/public/img/flags/Austria.png
diff --git a/web/static/img/flags/Belgium.png b/web/frontend/public/img/flags/Belgium.png
similarity index 100%
rename from web/static/img/flags/Belgium.png
rename to web/frontend/public/img/flags/Belgium.png
diff --git a/web/static/img/flags/Brazil.png b/web/frontend/public/img/flags/Brazil.png
similarity index 100%
rename from web/static/img/flags/Brazil.png
rename to web/frontend/public/img/flags/Brazil.png
diff --git a/web/static/img/flags/Bulgaria.png b/web/frontend/public/img/flags/Bulgaria.png
similarity index 100%
rename from web/static/img/flags/Bulgaria.png
rename to web/frontend/public/img/flags/Bulgaria.png
diff --git a/web/static/img/flags/Canada.png b/web/frontend/public/img/flags/Canada.png
similarity index 100%
rename from web/static/img/flags/Canada.png
rename to web/frontend/public/img/flags/Canada.png
diff --git a/web/static/img/flags/Chile.png b/web/frontend/public/img/flags/Chile.png
similarity index 100%
rename from web/static/img/flags/Chile.png
rename to web/frontend/public/img/flags/Chile.png
diff --git a/web/static/img/flags/China.png b/web/frontend/public/img/flags/China.png
similarity index 100%
rename from web/static/img/flags/China.png
rename to web/frontend/public/img/flags/China.png
diff --git a/web/static/img/flags/Croatia.png b/web/frontend/public/img/flags/Croatia.png
similarity index 100%
rename from web/static/img/flags/Croatia.png
rename to web/frontend/public/img/flags/Croatia.png
diff --git a/web/static/img/flags/Czech-Republic.png b/web/frontend/public/img/flags/Czech-Republic.png
similarity index 100%
rename from web/static/img/flags/Czech-Republic.png
rename to web/frontend/public/img/flags/Czech-Republic.png
diff --git a/web/static/img/flags/Denmark.png b/web/frontend/public/img/flags/Denmark.png
similarity index 100%
rename from web/static/img/flags/Denmark.png
rename to web/frontend/public/img/flags/Denmark.png
diff --git a/web/static/img/flags/England.png b/web/frontend/public/img/flags/England.png
similarity index 100%
rename from web/static/img/flags/England.png
rename to web/frontend/public/img/flags/England.png
diff --git a/web/static/img/flags/Finland.png b/web/frontend/public/img/flags/Finland.png
similarity index 100%
rename from web/static/img/flags/Finland.png
rename to web/frontend/public/img/flags/Finland.png
diff --git a/web/static/img/flags/France.png b/web/frontend/public/img/flags/France.png
similarity index 100%
rename from web/static/img/flags/France.png
rename to web/frontend/public/img/flags/France.png
diff --git a/web/static/img/flags/Germany.png b/web/frontend/public/img/flags/Germany.png
similarity index 100%
rename from web/static/img/flags/Germany.png
rename to web/frontend/public/img/flags/Germany.png
diff --git a/web/static/img/flags/Greece.png b/web/frontend/public/img/flags/Greece.png
similarity index 100%
rename from web/static/img/flags/Greece.png
rename to web/frontend/public/img/flags/Greece.png
diff --git a/web/static/img/flags/Hong-Kong.png b/web/frontend/public/img/flags/Hong-Kong.png
similarity index 100%
rename from web/static/img/flags/Hong-Kong.png
rename to web/frontend/public/img/flags/Hong-Kong.png
diff --git a/web/static/img/flags/Hungary.png b/web/frontend/public/img/flags/Hungary.png
similarity index 100%
rename from web/static/img/flags/Hungary.png
rename to web/frontend/public/img/flags/Hungary.png
diff --git a/web/static/img/flags/Iceland.png b/web/frontend/public/img/flags/Iceland.png
similarity index 100%
rename from web/static/img/flags/Iceland.png
rename to web/frontend/public/img/flags/Iceland.png
diff --git a/web/static/img/flags/India.png b/web/frontend/public/img/flags/India.png
similarity index 100%
rename from web/static/img/flags/India.png
rename to web/frontend/public/img/flags/India.png
diff --git a/web/static/img/flags/Indonesia.png b/web/frontend/public/img/flags/Indonesia.png
similarity index 100%
rename from web/static/img/flags/Indonesia.png
rename to web/frontend/public/img/flags/Indonesia.png
diff --git a/web/static/img/flags/Iran.png b/web/frontend/public/img/flags/Iran.png
similarity index 100%
rename from web/static/img/flags/Iran.png
rename to web/frontend/public/img/flags/Iran.png
diff --git a/web/static/img/flags/Iraq.png b/web/frontend/public/img/flags/Iraq.png
similarity index 100%
rename from web/static/img/flags/Iraq.png
rename to web/frontend/public/img/flags/Iraq.png
diff --git a/web/static/img/flags/Ireland.png b/web/frontend/public/img/flags/Ireland.png
similarity index 100%
rename from web/static/img/flags/Ireland.png
rename to web/frontend/public/img/flags/Ireland.png
diff --git a/web/static/img/flags/Israel.png b/web/frontend/public/img/flags/Israel.png
similarity index 100%
rename from web/static/img/flags/Israel.png
rename to web/frontend/public/img/flags/Israel.png
diff --git a/web/static/img/flags/Italy.png b/web/frontend/public/img/flags/Italy.png
similarity index 100%
rename from web/static/img/flags/Italy.png
rename to web/frontend/public/img/flags/Italy.png
diff --git a/web/static/img/flags/Jamaica.png b/web/frontend/public/img/flags/Jamaica.png
similarity index 100%
rename from web/static/img/flags/Jamaica.png
rename to web/frontend/public/img/flags/Jamaica.png
diff --git a/web/static/img/flags/Japan.png b/web/frontend/public/img/flags/Japan.png
similarity index 100%
rename from web/static/img/flags/Japan.png
rename to web/frontend/public/img/flags/Japan.png
diff --git a/web/static/img/flags/Kenya.png b/web/frontend/public/img/flags/Kenya.png
similarity index 100%
rename from web/static/img/flags/Kenya.png
rename to web/frontend/public/img/flags/Kenya.png
diff --git a/web/static/img/flags/Kuwait.png b/web/frontend/public/img/flags/Kuwait.png
similarity index 100%
rename from web/static/img/flags/Kuwait.png
rename to web/frontend/public/img/flags/Kuwait.png
diff --git a/web/static/img/flags/Luxembourg.png b/web/frontend/public/img/flags/Luxembourg.png
similarity index 100%
rename from web/static/img/flags/Luxembourg.png
rename to web/frontend/public/img/flags/Luxembourg.png
diff --git a/web/static/img/flags/Malaysia.png b/web/frontend/public/img/flags/Malaysia.png
similarity index 100%
rename from web/static/img/flags/Malaysia.png
rename to web/frontend/public/img/flags/Malaysia.png
diff --git a/web/static/img/flags/Namibia.png b/web/frontend/public/img/flags/Namibia.png
similarity index 100%
rename from web/static/img/flags/Namibia.png
rename to web/frontend/public/img/flags/Namibia.png
diff --git a/web/static/img/flags/Netherlands.png b/web/frontend/public/img/flags/Netherlands.png
similarity index 100%
rename from web/static/img/flags/Netherlands.png
rename to web/frontend/public/img/flags/Netherlands.png
diff --git a/web/static/img/flags/Norway.png b/web/frontend/public/img/flags/Norway.png
similarity index 100%
rename from web/static/img/flags/Norway.png
rename to web/frontend/public/img/flags/Norway.png
diff --git a/web/static/img/flags/Pakistan.png b/web/frontend/public/img/flags/Pakistan.png
similarity index 100%
rename from web/static/img/flags/Pakistan.png
rename to web/frontend/public/img/flags/Pakistan.png
diff --git a/web/static/img/flags/Poland.png b/web/frontend/public/img/flags/Poland.png
similarity index 100%
rename from web/static/img/flags/Poland.png
rename to web/frontend/public/img/flags/Poland.png
diff --git a/web/static/img/flags/Portugal.png b/web/frontend/public/img/flags/Portugal.png
similarity index 100%
rename from web/static/img/flags/Portugal.png
rename to web/frontend/public/img/flags/Portugal.png
diff --git a/web/static/img/flags/Romania.png b/web/frontend/public/img/flags/Romania.png
similarity index 100%
rename from web/static/img/flags/Romania.png
rename to web/frontend/public/img/flags/Romania.png
diff --git a/web/static/img/flags/Russia.png b/web/frontend/public/img/flags/Russia.png
similarity index 100%
rename from web/static/img/flags/Russia.png
rename to web/frontend/public/img/flags/Russia.png
diff --git a/web/static/img/flags/Scotland.png b/web/frontend/public/img/flags/Scotland.png
similarity index 100%
rename from web/static/img/flags/Scotland.png
rename to web/frontend/public/img/flags/Scotland.png
diff --git a/web/static/img/flags/Senegal.png b/web/frontend/public/img/flags/Senegal.png
similarity index 100%
rename from web/static/img/flags/Senegal.png
rename to web/frontend/public/img/flags/Senegal.png
diff --git a/web/static/img/flags/Serbia.png b/web/frontend/public/img/flags/Serbia.png
similarity index 100%
rename from web/static/img/flags/Serbia.png
rename to web/frontend/public/img/flags/Serbia.png
diff --git a/web/static/img/flags/Singapore.png b/web/frontend/public/img/flags/Singapore.png
similarity index 100%
rename from web/static/img/flags/Singapore.png
rename to web/frontend/public/img/flags/Singapore.png
diff --git a/web/static/img/flags/Slovakia.png b/web/frontend/public/img/flags/Slovakia.png
similarity index 100%
rename from web/static/img/flags/Slovakia.png
rename to web/frontend/public/img/flags/Slovakia.png
diff --git a/web/static/img/flags/Slovenia.png b/web/frontend/public/img/flags/Slovenia.png
similarity index 100%
rename from web/static/img/flags/Slovenia.png
rename to web/frontend/public/img/flags/Slovenia.png
diff --git a/web/static/img/flags/South-Africa.png b/web/frontend/public/img/flags/South-Africa.png
similarity index 100%
rename from web/static/img/flags/South-Africa.png
rename to web/frontend/public/img/flags/South-Africa.png
diff --git a/web/static/img/flags/South-Korea.png b/web/frontend/public/img/flags/South-Korea.png
similarity index 100%
rename from web/static/img/flags/South-Korea.png
rename to web/frontend/public/img/flags/South-Korea.png
diff --git a/web/static/img/flags/Spain.png b/web/frontend/public/img/flags/Spain.png
similarity index 100%
rename from web/static/img/flags/Spain.png
rename to web/frontend/public/img/flags/Spain.png
diff --git a/web/static/img/flags/Sweden.png b/web/frontend/public/img/flags/Sweden.png
similarity index 100%
rename from web/static/img/flags/Sweden.png
rename to web/frontend/public/img/flags/Sweden.png
diff --git a/web/static/img/flags/Switzerland.png b/web/frontend/public/img/flags/Switzerland.png
similarity index 100%
rename from web/static/img/flags/Switzerland.png
rename to web/frontend/public/img/flags/Switzerland.png
diff --git a/web/static/img/flags/Taiwan.png b/web/frontend/public/img/flags/Taiwan.png
similarity index 100%
rename from web/static/img/flags/Taiwan.png
rename to web/frontend/public/img/flags/Taiwan.png
diff --git a/web/static/img/flags/Thailand.png b/web/frontend/public/img/flags/Thailand.png
similarity index 100%
rename from web/static/img/flags/Thailand.png
rename to web/frontend/public/img/flags/Thailand.png
diff --git a/web/static/img/flags/Turkey.png b/web/frontend/public/img/flags/Turkey.png
similarity index 100%
rename from web/static/img/flags/Turkey.png
rename to web/frontend/public/img/flags/Turkey.png
diff --git a/web/static/img/flags/United-Kingdom.png b/web/frontend/public/img/flags/United-Kingdom.png
similarity index 100%
rename from web/static/img/flags/United-Kingdom.png
rename to web/frontend/public/img/flags/United-Kingdom.png
diff --git a/web/static/img/flags/United-Nations.png b/web/frontend/public/img/flags/United-Nations.png
similarity index 100%
rename from web/static/img/flags/United-Nations.png
rename to web/frontend/public/img/flags/United-Nations.png
diff --git a/web/static/img/flags/United-States.png b/web/frontend/public/img/flags/United-States.png
similarity index 100%
rename from web/static/img/flags/United-States.png
rename to web/frontend/public/img/flags/United-States.png
diff --git a/web/static/img/intro-bg.jpg b/web/frontend/public/img/intro-bg.jpg
similarity index 100%
rename from web/static/img/intro-bg.jpg
rename to web/frontend/public/img/intro-bg.jpg
diff --git a/web/static/img/laden.jpg b/web/frontend/public/img/laden.jpg
similarity index 100%
rename from web/static/img/laden.jpg
rename to web/frontend/public/img/laden.jpg
diff --git a/web/static/img/loading.gif b/web/frontend/public/img/loading.gif
similarity index 100%
rename from web/static/img/loading.gif
rename to web/frontend/public/img/loading.gif
diff --git a/web/static/img/logo-small.png b/web/frontend/public/img/logo-small.png
similarity index 100%
rename from web/static/img/logo-small.png
rename to web/frontend/public/img/logo-small.png
diff --git a/web/static/img/logo.png b/web/frontend/public/img/logo.png
similarity index 100%
rename from web/static/img/logo.png
rename to web/frontend/public/img/logo.png
diff --git a/web/static/img/map-marker.png b/web/frontend/public/img/map-marker.png
similarity index 100%
rename from web/static/img/map-marker.png
rename to web/frontend/public/img/map-marker.png
diff --git a/web/static/img/montage.jpg b/web/frontend/public/img/montage.jpg
similarity index 100%
rename from web/static/img/montage.jpg
rename to web/frontend/public/img/montage.jpg
diff --git a/web/static/img/niggi.jpg b/web/frontend/public/img/niggi.jpg
similarity index 100%
rename from web/static/img/niggi.jpg
rename to web/frontend/public/img/niggi.jpg
diff --git a/web/static/img/overview.jpg b/web/frontend/public/img/overview.jpg
similarity index 100%
rename from web/static/img/overview.jpg
rename to web/frontend/public/img/overview.jpg
diff --git a/web/static/img/progressbar.gif b/web/frontend/public/img/progressbar.gif
similarity index 100%
rename from web/static/img/progressbar.gif
rename to web/frontend/public/img/progressbar.gif
diff --git a/web/static/img/waage.jpg b/web/frontend/public/img/waage.jpg
similarity index 100%
rename from web/static/img/waage.jpg
rename to web/frontend/public/img/waage.jpg
diff --git a/web/static/img/wickeln.jpg b/web/frontend/public/img/wickeln.jpg
similarity index 100%
rename from web/static/img/wickeln.jpg
rename to web/frontend/public/img/wickeln.jpg
diff --git a/web/static/img/wolle.jpg b/web/frontend/public/img/wolle.jpg
similarity index 100%
rename from web/static/img/wolle.jpg
rename to web/frontend/public/img/wolle.jpg
diff --git a/web/static/js/bootstrap.bundle.js b/web/frontend/public/js/bootstrap.bundle.js
similarity index 100%
rename from web/static/js/bootstrap.bundle.js
rename to web/frontend/public/js/bootstrap.bundle.js
diff --git a/web/static/js/bootstrap.bundle.js.map b/web/frontend/public/js/bootstrap.bundle.js.map
similarity index 100%
rename from web/static/js/bootstrap.bundle.js.map
rename to web/frontend/public/js/bootstrap.bundle.js.map
diff --git a/web/static/js/bootstrap.bundle.min.js b/web/frontend/public/js/bootstrap.bundle.min.js
similarity index 100%
rename from web/static/js/bootstrap.bundle.min.js
rename to web/frontend/public/js/bootstrap.bundle.min.js
diff --git a/web/static/js/bootstrap.bundle.min.js.map b/web/frontend/public/js/bootstrap.bundle.min.js.map
similarity index 100%
rename from web/static/js/bootstrap.bundle.min.js.map
rename to web/frontend/public/js/bootstrap.bundle.min.js.map
diff --git a/web/static/js/bootstrap.esm.js b/web/frontend/public/js/bootstrap.esm.js
similarity index 100%
rename from web/static/js/bootstrap.esm.js
rename to web/frontend/public/js/bootstrap.esm.js
diff --git a/web/static/js/bootstrap.esm.js.map b/web/frontend/public/js/bootstrap.esm.js.map
similarity index 100%
rename from web/static/js/bootstrap.esm.js.map
rename to web/frontend/public/js/bootstrap.esm.js.map
diff --git a/web/static/js/bootstrap.esm.min.js b/web/frontend/public/js/bootstrap.esm.min.js
similarity index 100%
rename from web/static/js/bootstrap.esm.min.js
rename to web/frontend/public/js/bootstrap.esm.min.js
diff --git a/web/static/js/bootstrap.esm.min.js.map b/web/frontend/public/js/bootstrap.esm.min.js.map
similarity index 100%
rename from web/static/js/bootstrap.esm.min.js.map
rename to web/frontend/public/js/bootstrap.esm.min.js.map
diff --git a/web/static/js/bootstrap.js b/web/frontend/public/js/bootstrap.js
similarity index 100%
rename from web/static/js/bootstrap.js
rename to web/frontend/public/js/bootstrap.js
diff --git a/web/static/js/bootstrap.js.map b/web/frontend/public/js/bootstrap.js.map
similarity index 100%
rename from web/static/js/bootstrap.js.map
rename to web/frontend/public/js/bootstrap.js.map
diff --git a/web/static/js/bootstrap.min.js b/web/frontend/public/js/bootstrap.min.js
similarity index 100%
rename from web/static/js/bootstrap.min.js
rename to web/frontend/public/js/bootstrap.min.js
diff --git a/web/static/js/bootstrap.min.js.map b/web/frontend/public/js/bootstrap.min.js.map
similarity index 100%
rename from web/static/js/bootstrap.min.js.map
rename to web/frontend/public/js/bootstrap.min.js.map
diff --git a/web/static/robots.txt b/web/frontend/public/robots.txt
similarity index 100%
rename from web/static/robots.txt
rename to web/frontend/public/robots.txt
diff --git a/web/frontend/src/App.svelte b/web/frontend/src/App.svelte
new file mode 100644
index 0000000..9e85973
--- /dev/null
+++ b/web/frontend/src/App.svelte
@@ -0,0 +1,45 @@
+<script lang="ts">
+  import svelteLogo from "./assets/svelte.svg";
+  import Counter from "./lib/Counter.svelte";
+</script>
+
+<main>
+  <div>
+    <a href="https://svelte.dev" target="_blank" rel="noreferrer">
+      <img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
+    </a>
+  </div>
+  <h1>Vite + Svelte</h1>
+
+  <div class="card">
+    <Counter />
+  </div>
+
+  <p>
+    Check out <a
+      href="https://github.com/sveltejs/kit#readme"
+      target="_blank"
+      rel="noreferrer">SvelteKit</a
+    >, the official Svelte app framework powered by Vite!
+  </p>
+
+  <p class="read-the-docs">Click on the Vite and Svelte logos to learn more</p>
+</main>
+
+<style>
+  .logo {
+    height: 6em;
+    padding: 1.5em;
+    will-change: filter;
+    transition: filter 300ms;
+  }
+  .logo:hover {
+    filter: drop-shadow(0 0 2em #646cffaa);
+  }
+  .logo.svelte:hover {
+    filter: drop-shadow(0 0 2em #ff3e00aa);
+  }
+  .read-the-docs {
+    color: #888;
+  }
+</style>
diff --git a/web/frontend/src/app.css b/web/frontend/src/app.css
new file mode 100644
index 0000000..6a278fd
--- /dev/null
+++ b/web/frontend/src/app.css
@@ -0,0 +1,86 @@
+:root {
+  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+html,
+body {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+/* body { */
+/*   margin: 0; */
+/*   display: flex; */
+/*   place-items: center; */
+/*   min-width: 320px; */
+/*   min-height: 100vh; */
+/* } */
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+.card {
+  padding: 2em;
+}
+
+#app {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 2rem;
+  text-align: center;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}
diff --git a/web/frontend/src/assets/svelte.svg b/web/frontend/src/assets/svelte.svg
new file mode 100644
index 0000000..c5e0848
--- /dev/null
+++ b/web/frontend/src/assets/svelte.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
\ No newline at end of file
diff --git a/web/frontend/src/lib/Counter.svelte b/web/frontend/src/lib/Counter.svelte
new file mode 100644
index 0000000..37d75ce
--- /dev/null
+++ b/web/frontend/src/lib/Counter.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+  let count: number = $state(0)
+  const increment = () => {
+    count += 1
+  }
+</script>
+
+<button onclick={increment}>
+  count is {count}
+</button>
diff --git a/web/frontend/src/main.ts b/web/frontend/src/main.ts
new file mode 100644
index 0000000..664a057
--- /dev/null
+++ b/web/frontend/src/main.ts
@@ -0,0 +1,9 @@
+import { mount } from 'svelte'
+import './app.css'
+import App from './App.svelte'
+
+const app = mount(App, {
+  target: document.getElementById('app')!,
+})
+
+export default app
diff --git a/web/frontend/src/vite-env.d.ts b/web/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..4078e74
--- /dev/null
+++ b/web/frontend/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="svelte" />
+/// <reference types="vite/client" />
diff --git a/web/frontend/svelte.config.js b/web/frontend/svelte.config.js
new file mode 100644
index 0000000..b0683fd
--- /dev/null
+++ b/web/frontend/svelte.config.js
@@ -0,0 +1,7 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+
+export default {
+  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
+  // for more information about preprocessors
+  preprocess: vitePreprocess(),
+}
diff --git a/web/frontend/tsconfig.app.json b/web/frontend/tsconfig.app.json
new file mode 100644
index 0000000..55a2f9b
--- /dev/null
+++ b/web/frontend/tsconfig.app.json
@@ -0,0 +1,20 @@
+{
+  "extends": "@tsconfig/svelte/tsconfig.json",
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "resolveJsonModule": true,
+    /**
+     * Typecheck JS in `.svelte` and `.js` files by default.
+     * Disable checkJs if you'd like to use dynamic types in JS.
+     * Note that setting allowJs false does not prevent the use
+     * of JS in `.svelte` files.
+     */
+    "allowJs": true,
+    "checkJs": true,
+    "isolatedModules": true,
+    "moduleDetection": "force"
+  },
+  "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
+}
diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/web/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}
diff --git a/web/frontend/tsconfig.node.json b/web/frontend/tsconfig.node.json
new file mode 100644
index 0000000..9728af2
--- /dev/null
+++ b/web/frontend/tsconfig.node.json
@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2022",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}
diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts
new file mode 100644
index 0000000..d32eba1
--- /dev/null
+++ b/web/frontend/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import { svelte } from '@sveltejs/vite-plugin-svelte'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [svelte()],
+})
diff --git a/web/templates.go b/web/templates.go
index eea7d25..11dfec9 100644
--- a/web/templates.go
+++ b/web/templates.go
@@ -2,7 +2,27 @@ package web
 
 import (
 	"embed"
+	"net/http"
+	"text/template"
+
+	"github.com/olivere/vite"
 )
 
+type PageData struct {
+	Title    string
+	MsgType  string
+	Message  string
+	Redirect string
+	Vite     *vite.Fragment
+}
+
 //go:embed templates
 var Templates embed.FS
+
+//go:embed frontend/dist
+var StaticAssets embed.FS
+
+func RenderTemplate(w http.ResponseWriter, name string, data PageData) error {
+	tpl := template.Must(template.ParseFS(Templates, "templates/"+name+".html", "templates/base.html"))
+	return tpl.ExecuteTemplate(w, "base", data)
+}
diff --git a/web/templates/admin.html b/web/templates/admin.html
new file mode 100644
index 0000000..3f9a328
--- /dev/null
+++ b/web/templates/admin.html
@@ -0,0 +1,3 @@
+{{define "vite"}} {{ .Vite.Tags }} {{end}} {{define "content"}}
+<div id="root"></div>
+{{end}}
diff --git a/web/templates/base.html b/web/templates/base.html
index d5ca8e4..f22543f 100644
--- a/web/templates/base.html
+++ b/web/templates/base.html
@@ -3,6 +3,7 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
+    {{block "vite" .}} {{end}}
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta
       name="description"
@@ -17,7 +18,7 @@
     <link href="/static/css/app.css" rel="stylesheet" />
     {{block "stylesheets" .}} {{end}}
   </head>
-  <body>
+  <body class="site">
     <nav
       id="navbar-main"
       class="navbar navbar-expand-lg fixed-top navbar-light bg-white"
@@ -47,7 +48,7 @@
     {{block "header" .}} {{end}}
 
     <main>
-      <div class="container">{{block "content" .}} {{end}}</div>
+      <div class="container site-content">{{block "content" .}} {{end}}</div>
     </main>
 
     {{block "footer" .}}
diff --git a/web/templates/login.html b/web/templates/login.html
new file mode 100644
index 0000000..fa9ae13
--- /dev/null
+++ b/web/templates/login.html
@@ -0,0 +1,51 @@
+{{define "navigation"}} {{end}} {{define "content"}}
+<section class="content-section">
+  <div class="container">
+    <div class="row">
+      <div class="col-4 mx-auto">
+        {{if .MsgType}}
+        <div class="alert {{.MsgType}}" role="alert">{{.Message}}</div>
+        {{end}}
+
+        <div class="card">
+          <div class="card-header">
+            <h3>Login</h3>
+          </div>
+          <div class="card-body">
+            <form action="/login" method="post">
+              <div class="mb-3">
+                <label class="form-label" for="username">Username</label>
+                <input
+                  class="form-control"
+                  type="text"
+                  id="username"
+                  name="username"
+                  required
+                  autofocus
+                />
+              </div>
+              <div class="mb-3">
+                <label class="form-label" for="password">Password</label>
+                <input
+                  class="form-control"
+                  type="password"
+                  id="password"
+                  name="password"
+                  required
+                />
+              </div>
+              <button type="submit" class="btn btn-success">Submit</button>
+              <input
+                type="hidden"
+                id="redirect"
+                name="redirect"
+                value="{{ .Redirect }}"
+              />
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</section>
+{{end}}