Compare commits

...

27 commits

Author SHA1 Message Date
8140a16c03 Merge branch 'release/0.2.0' 2026-02-22 13:25:36 +00:00
848ec0e6c4 ⬆️ bump versions 2026-02-22 13:23:50 +00:00
b05891a209 🧹 general cleanup 2026-02-22 13:11:38 +00:00
8735260bc3 🩹 fix lint errors in unit tests 2026-02-22 13:09:56 +00:00
1eab05a6ea api: add SETTINGS.show_api_docs 2026-02-22 12:06:16 +01:00
30d1c5fba9 api "production" script: invoke granian without "click" hack 2026-02-22 05:00:11 +01:00
ca46a2cf5c 🔧 various small fixes
- add version to ui package.json
- "prettier" formatting on whole repo
- "webpack-bundle-analyzer" -> "vite-bundle-analyzer"
- add "vue-eslint-parser" to ui package.json
2026-02-22 03:05:05 +00:00
005bb31fca into shared compose project
- vite can proxy requests to the api in dev mode
- no more CORS needed in api
- static api_baseurl in axios config
2026-02-22 02:47:31 +00:00
c838f79e66 🐛 remove dependency on legacy "browserslist" 2026-02-22 02:41:16 +00:00
d9f4042bd3 Merge branch 'feature/ui-rescaffold' into develop 2026-02-22 01:32:18 +00:00
cbfc1d25c6 🚧 fix tasks and Dockerfile 2026-02-22 01:31:26 +00:00
cc333497b0 🚧 ui: re-scaffolding
- merge in remaining legacy code
2026-02-22 01:14:38 +00:00
b1c65f4d4a 🚧 ui: re-scaffolding
- fix lint errors
2026-02-21 23:48:18 +00:00
d816460efa 🧹 reformat using "prettier" 2026-02-21 19:24:09 +00:00
a1c4e408a9 🚧 ui: re-scaffolding
- move code into newly built project
2026-02-21 19:10:56 +00:00
022c9138bf 🚧 ui: re-scaffolding
- built a new bare vue 3.5 project with vite
- partly merged in old configs
2026-02-20 22:25:22 +00:00
8c231b5bf4 Merge branch 'feature/api-rescaffold' into develop 2026-02-20 17:11:14 +00:00
865c145b42 🔧 Dockerfile rework
- eliminate corepack dependency
- improved caching
- changed some comments and paths
2026-02-20 17:08:43 +00:00
1f6aeadae0 add some OMZ plugins to devcontainers 2026-02-20 12:14:36 +00:00
1179fa0f80 ⬆️ api: upgrade deps
- `uv-upx upgrade run`
2026-02-20 01:36:23 +01:00
e5e4bda66a final image now uses dhi base image 2026-02-20 00:08:42 +00:00
89403a8e30 🚧 api: re-scaffolding
- main Dockerfile readability
- use "granian" in production mode
- simpler "production.py" replacing "mini-tiangolo" (gunicorn+unicorn)
2026-02-19 23:45:59 +01:00
eb3985d6a9 🧰 api: add tool "uv-upx" (kin to poetry-plugin-up) 2026-02-19 03:11:11 +01:00
d0a7daf7be 🚧 api: re-scaffolding
- remove legacy "poetry.lock", "main.py"
- flesh out launch.json to use only "fastapi-cli"
- remove unneeded lines from "settings.json"
- rework main "Dockerfile" for "uv" compliance
- update ".dockerignore"
2026-02-19 03:10:03 +01:00
67eebecd39 🔧 minor refactoring
- use `type` keyword instead of `typing.TypeAlias`
- use `str` instead of `AnyStr`
2026-02-18 03:21:03 +01:00
c7679072aa 🚧 api: re-scaffolding
- uses astral.sh tooling: uv, ruff, ty
- removed legacy tools
- some minor python fixes
2026-02-18 03:19:22 +01:00
03df8bbb65 Merge tag '0.1.0' into develop
First tagged release

- python 3.14, node 24
- vue 3 with composition api
- next steps: re-scaffold both subprojects (uv + vite)
2026-02-16 01:12:45 +00:00
75 changed files with 4358 additions and 10042 deletions

View file

@ -0,0 +1,14 @@
name: advent22
services:
api:
image: mcr.microsoft.com/devcontainers/python:3-3.14-trixie
volumes:
- ../:/workspaces/advent22:cached
command: sleep infinity
ui:
image: mcr.microsoft.com/devcontainers/javascript-node:4-24-trixie
volumes:
- ../:/workspaces/advent22:cached
command: sleep infinity

View file

@ -11,9 +11,12 @@
**/.dockerignore **/.dockerignore
# found in python and JS dirs # found in python and JS dirs
**/__pycache__ **/__pycache__/
**/node_modules **/node_modules/
**/.pytest_cache **/.pytest_cache/
**/.ruff_cache/
**/.uv_cache/
**/.venv/
# env files # env files
**/.env **/.env
@ -25,3 +28,6 @@
**/yarn-debug.log* **/yarn-debug.log*
**/yarn-error.log* **/yarn-error.log*
**/pnpm-debug.log* **/pnpm-debug.log*
# custom files
api/api.conf

View file

@ -1,51 +1,6 @@
ARG NODE_VERSION=24 ARG NODE_VERSION=24
ARG PYTHON_VERSION=3.14-slim ARG PYTHON_VERSION=3.14
#############
# build api #
#############
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION} AS build-api
# env setup
WORKDIR /usr/local/src/advent22_api
ENV \
PATH="/root/.local/bin:${PATH}"
# install poetry with export plugin
RUN set -ex; \
\
python -m pip --no-cache-dir install --upgrade pip wheel; \
\
apt-get update; apt-get install --yes --no-install-recommends \
curl \
; rm -rf /var/lib/apt/lists/*; \
\
curl -sSL https://install.python-poetry.org | python3 -; \
poetry self add poetry-plugin-export;
# build dependency wheels
COPY api/pyproject.toml api/poetry.lock ./
RUN set -ex; \
\
# # buildtime dependencies
# apt-get update; apt-get install --yes --no-install-recommends \
# build-essential \
# ; rm -rf /var/lib/apt/lists/*; \
\
# generate requirements.txt
poetry export \
--format requirements.txt \
--output requirements.txt; \
\
python3 -m pip --no-cache-dir wheel \
--wheel-dir ./dist \
--requirement requirements.txt;
# build advent22_api wheel
COPY api ./
RUN poetry build --format wheel --output ./dist
############ ############
# build ui # # build ui #
@ -54,76 +9,90 @@ RUN poetry build --format wheel --output ./dist
ARG NODE_VERSION ARG NODE_VERSION
FROM node:${NODE_VERSION} AS build-ui FROM node:${NODE_VERSION} AS build-ui
# env setup # install ui dependencies
WORKDIR /usr/local/src/advent22_ui WORKDIR /usr/local/src/advent22_ui
RUN --mount=type=bind,source=ui/package.json,target=package.json \
--mount=type=bind,source=ui/yarn.lock,target=yarn.lock \
--mount=type=bind,source=ui/.yarn/releases,target=.yarn/releases \
--mount=type=bind,source=ui/.yarnrc.yml,target=.yarnrc.yml \
--mount=type=cache,id=ui,target=/root/.yarn \
\
yarn install --immutable --check-cache;
# install advent22_ui dependencies # copy and build ui
COPY ui/package*.json ui/yarn*.lock ./
RUN set -ex; \
corepack enable; \
yarn install;
# copy and build advent22_ui
COPY ui ./ COPY ui ./
RUN set -ex; \ RUN --mount=type=cache,id=ui,target=/root/.yarn \
yarn dlx update-browserslist-db@latest; \ set -ex; \
yarn build --dest /tmp/advent22_ui/html; \ \
# exclude webpack-bundle-analyzer output yarn build --outDir /opt/advent22/ui; \
rm -f /tmp/advent22_ui/html/report.html; # exclude vite-bundle-analyzer output
rm /opt/advent22/ui/stats.html;
######################
# python preparation # ###############
###################### # install app #
###############
ARG PYTHON_VERSION ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION} AS uvicorn-gunicorn FROM dhi.io/python:${PYTHON_VERSION}-dev AS install-app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# where credit is due ...
LABEL maintainer="Sebastián Ramirez <tiangolo@gmail.com>"
WORKDIR /usr/local/share/uvicorn-gunicorn
# install uvicorn-gunicorn
COPY ./scripts/mini-tiangolo ./
RUN set -ex; \
chmod +x start.sh; \
python3 -m pip --no-cache-dir install gunicorn;
CMD ["/usr/local/share/uvicorn-gunicorn/start.sh"]
###########
# web app #
###########
FROM uvicorn-gunicorn AS production
# env setup # env setup
ENV \
PRODUCTION_MODE="true" \
PORT="8000" \
MODULE_NAME="advent22_api.app"
EXPOSE 8000
WORKDIR /opt/advent22 WORKDIR /opt/advent22
VOLUME [ "/opt/advent22" ] ENV UV_COMPILE_BYTECODE=1 \
UV_NO_DEV=1 \
UV_LINK_MODE="copy"
COPY --from=build-api /usr/local/src/advent22_api/dist /usr/local/share/advent22_api.dist RUN --mount=type=bind,source=api/uv.lock,target=api/uv.lock \
RUN set -ex; \ --mount=type=bind,source=api/pyproject.toml,target=api/pyproject.toml \
# remove example app --mount=type=bind,source=api/.python-version,target=api/.python-version \
rm -rf /app; \ --mount=type=cache,id=api,target=/root/.cache/uv \
\ set -ex; \
# # runtime dependencies
# apt-get update; apt-get install --yes --no-install-recommends \
# ; rm -rf /var/lib/apt/lists/*; \
\
# install advent22_api wheels
python3 -m pip --no-cache-dir install --no-deps /usr/local/share/advent22_api.dist/*.whl; \
\ \
# prepare data directory # prepare data directory
chown nobody:nogroup ./ mkdir data; \
chown nobody:nobody data; \
chmod u=rwx,g=rx,o=rx data; \
\
# install api deps
uv sync \
--project api/ \
--locked \
--no-install-project \
--no-editable \
;
# add prepared advent22_ui # install api
COPY --from=build-ui /tmp/advent22_ui /usr/local/share/advent22_ui COPY api api/
RUN --mount=type=cache,id=api,target=/root/.cache/uv \
\
uv sync \
--project api/ \
--locked \
--no-editable \
;
# add prepared ui
COPY --from=build-ui /opt/advent22/ui ui/
####################
# production image #
####################
ARG PYTHON_VERSION
FROM dhi.io/python:${PYTHON_VERSION} AS production
ENV PATH="/opt/advent22/api/.venv/bin:$PATH"
EXPOSE 8000
CMD [ "advent22" ]
ARG PYTHON_VERSION
COPY --from=install-app /opt/python/lib/python${PYTHON_VERSION} /opt/python/lib/python${PYTHON_VERSION}/
COPY --from=install-app /opt/advent22 /opt/advent22/
WORKDIR /opt/advent22/data
VOLUME [ "/opt/advent22/data" ]
# run as unprivileged user # run as unprivileged user
USER nobody USER nobody

View file

@ -4,12 +4,18 @@
"name": "Advent22 API", "name": "Advent22 API",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie", "dockerComposeFile": "../../.devcontainer/docker_compose.yml",
"service": "api",
"workspaceFolder": "/workspaces/advent22/api",
"runServices": ["api"],
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
"features": { "features": {
"ghcr.io/devcontainers/features/git-lfs:1": {}, "ghcr.io/devcontainers/features/git-lfs:1": {},
"ghcr.io/devcontainers-extra/features/poetry:2": {}, "ghcr.io/devcontainers-extra/features/uv:1": {},
"ghcr.io/devcontainers-extra/features/zsh-plugins:0": {
"plugins": "git-flow uv"
},
"ghcr.io/devcontainers-extra/features/apt-get-packages:1": { "ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
"packages": "git-flow" "packages": "git-flow"
}, },
@ -17,7 +23,8 @@
}, },
"containerEnv": { "containerEnv": {
"TZ": "Europe/Berlin" "TZ": "Europe/Berlin",
"UV_CACHE_DIR": "/workspaces/advent22/.uv_cache"
}, },
// Configure tool-specific properties. // Configure tool-specific properties.
@ -31,22 +38,21 @@
}, },
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"astral-sh.ty",
"charliermarsh.ruff",
"be5invis.toml", "be5invis.toml",
"mhutchie.git-graph", "mhutchie.git-graph",
"ms-python.python", "ms-python.python",
"ms-python.black-formatter",
"ms-python.flake8",
"ms-python.isort",
"ms-python.vscode-pylance" "ms-python.vscode-pylance"
] ]
} }
}, },
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "sudo /usr/local/py-utils/bin/poetry self add poetry-plugin-up", "postCreateCommand": "uv tool install uv-upx",
// Use 'postStartCommand' to run commands after the container is started. // Use 'postStartCommand' to run commands after the container is started.
"postStartCommand": "poetry install" "postStartCommand": "uv tool upgrade uv-upx && uv sync"
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],

View file

@ -1,4 +0,0 @@
[flake8]
max-line-length = 80
extend-select = B950
extend-ignore = E203,E501

View file

@ -1,3 +0,0 @@
[settings]
profile = black
line_length = 80

1
api/.python-version Normal file
View file

@ -0,0 +1 @@
3.14

View file

@ -5,18 +5,25 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Main Module", "name": "FastAPI CLI (dev)",
"type": "python", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "advent22_api.main", "module": "fastapi",
"pythonArgs": [ "args": [
"-Xfrozen_modules=off", "dev",
"--host",
"0.0.0.0",
"--port",
"8000",
"--entrypoint",
"advent22_api.app:app",
"--reload-dir",
"${workspaceFolder}/advent22_api"
], ],
"env": { "env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1", "ADVENT22__WEBDAV__CACHE_TTL": "30"
"WEBDAV__CACHE_TTL": "30",
}, },
"justMyCode": true, "justMyCode": true
} }
] ]
} }

View file

@ -3,31 +3,16 @@
"[python]": { "[python]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": "explicit", "source.organizeImports": "explicit",
"source.fixAll": "explicit", "source.fixAll": "explicit"
}
}, },
},
"python.languageServer": "Pylance",
"python.analysis.autoImportCompletions": true,
"python.analysis.importFormat": "relative",
"python.analysis.fixAll": [
"source.convertImportFormat",
"source.unusedImports",
],
"python.analysis.typeCheckingMode": "basic",
"python.analysis.diagnosticMode": "workspace",
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.testing.pytestArgs": [
"--import-mode=importlib",
"test",
],
"black-formatter.importStrategy": "fromEnvironment", "ty.diagnosticMode": "workspace",
"flake8.importStrategy": "fromEnvironment", "ruff.nativeServer": "on"
"isort.importStrategy": "fromEnvironment",
} }

21
api/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Lenaisten e.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

0
api/README.md Normal file
View file

View file

View file

@ -1,5 +1,4 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from .core.settings import SETTINGS from .core.settings import SETTINGS
@ -33,14 +32,3 @@ if SETTINGS.production_mode:
), ),
name="frontend", name="frontend",
) )
else:
# Allow CORS in debug mode
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)

View file

@ -1,7 +1,7 @@
import colorsys import colorsys
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import AnyStr, Self, TypeAlias from typing import Self, cast
import numpy as np import numpy as np
from PIL import Image as PILImage from PIL import Image as PILImage
@ -11,9 +11,9 @@ from PIL.ImageFont import FreeTypeFont
from .config import Config from .config import Config
_RGB: TypeAlias = tuple[int, int, int] type _RGB = tuple[int, int, int]
_XY: TypeAlias = tuple[float, float] type _XY = tuple[float, float]
_Box: TypeAlias = tuple[int, int, int, int] type _Box = tuple[int, int, int, int]
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -56,7 +56,7 @@ class AdventImage:
async def get_text_box( async def get_text_box(
self, self,
xy: _XY, xy: _XY,
text: AnyStr, text: str,
font: FreeTypeFont, font: FreeTypeFont,
anchor: str | None = "mm", anchor: str | None = "mm",
**text_kwargs, **text_kwargs,
@ -95,12 +95,12 @@ class AdventImage:
pixel_data = np.asarray(self.img.crop(box)) pixel_data = np.asarray(self.img.crop(box))
mean_color: np.ndarray = np.mean(pixel_data, axis=(0, 1)) mean_color: np.ndarray = np.mean(pixel_data, axis=(0, 1))
return _RGB(mean_color.astype(int)) return cast(_RGB, mean_color.astype(int))
async def hide_text( async def hide_text(
self, self,
xy: _XY, xy: _XY,
text: AnyStr, text: str,
font: FreeTypeFont, font: FreeTypeFont,
anchor: str | None = "mm", anchor: str | None = "mm",
**text_kwargs, **text_kwargs,
@ -134,8 +134,10 @@ class AdventImage:
else: else:
tc_v -= 3 tc_v -= 3
text_color = colorsys.hsv_to_rgb(tc_h, tc_s, tc_v) text_color: tuple[int | float, int | float, int | float] = colorsys.hsv_to_rgb(
text_color = _RGB(int(val) for val in text_color) tc_h, tc_s, tc_v
)
text_color = cast(_RGB, tuple(int(val) for val in text_color))
# Buchstaben verstecken # Buchstaben verstecken
ImageDraw.Draw(self.img).text( ImageDraw.Draw(self.img).text(

View file

@ -1,5 +1,4 @@
import tomllib import tomllib
from typing import TypeAlias
import tomli_w import tomli_w
from fastapi import Depends from fastapi import Depends
@ -20,7 +19,7 @@ class DoorSaved(BaseModel):
y2: int y2: int
DoorsSaved: TypeAlias = list[DoorSaved] type DoorsSaved = list[DoorSaved]
class CalendarConfig(BaseModel): class CalendarConfig(BaseModel):

View file

@ -55,6 +55,6 @@ class RedisCache(__RedisCache):
try: try:
return super()._deserialize(s) return super()._deserialize(s)
except (UnicodeDecodeError, JSONDecodeError): except UnicodeDecodeError, JSONDecodeError:
assert isinstance(s, bytes) assert isinstance(s, bytes)
return s return s

View file

@ -219,7 +219,7 @@ async def get_day_image(
image = await AdventImage.from_img(img, cfg) image = await AdventImage.from_img(img, cfg)
return image.img return image.img
except (KeyError, RuntimeError): except KeyError, RuntimeError:
# Erstelle automatisch generiertes Bild # Erstelle automatisch generiertes Bild
return await gen_day_auto_image( return await gen_day_auto_image(
day=day, day=day,

View file

@ -1,10 +1,6 @@
from typing import TypeVar from pydantic import BaseModel, Field
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
T = TypeVar("T")
class Credentials(BaseModel): class Credentials(BaseModel):
username: str = "" username: str = ""
@ -55,6 +51,7 @@ class Settings(BaseSettings):
""" """
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_prefix="ADVENT22__",
env_file="api.conf", env_file="api.conf",
env_file_encoding="utf-8", env_file_encoding="utf-8",
env_nested_delimiter="__", env_nested_delimiter="__",
@ -65,29 +62,32 @@ class Settings(BaseSettings):
##### #####
production_mode: bool = False production_mode: bool = False
ui_directory: str = "/usr/local/share/advent22_ui/html" show_api_docs: bool = Field(
default_factory=lambda data: not data["production_mode"]
)
ui_directory: str = "/opt/advent22/ui"
##### #####
# openapi settings # openapi settings
##### #####
def __dev_value(self, value: T) -> T | None: def __api_docs[T](self, value: T) -> T | None:
if self.production_mode: if self.show_api_docs:
return None
return value return value
return None
@property @property
def openapi_url(self) -> str | None: def openapi_url(self) -> str | None:
return self.__dev_value("/api/openapi.json") return self.__api_docs("/api/openapi.json")
@property @property
def docs_url(self) -> str | None: def docs_url(self) -> str | None:
return self.__dev_value("/api/docs") return self.__api_docs("/api/docs")
@property @property
def redoc_url(self) -> str | None: def redoc_url(self) -> str | None:
return self.__dev_value("/api/redoc") return self.__api_docs("/api/redoc")
##### #####
# webdav settings # webdav settings

View file

@ -1,22 +0,0 @@
#!/usr/bin/python3
import uvicorn
from .core.settings import SETTINGS
def main() -> None:
"""
If the `main` script is run, `uvicorn` is used to run the app.
"""
uvicorn.run(
app="advent22_api.app:app",
host="0.0.0.0",
port=8000,
reload=not SETTINGS.production_mode,
)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,49 @@
import os
from granian import Granian
from granian.constants import Interfaces, Loops
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class WorkersSettings(BaseModel):
per_core: int = Field(1, ge=1)
max: int | None = Field(None, ge=1)
exact: int | None = Field(None, ge=1)
@property
def count(self) -> int:
# usage of "or" operator: values here are not allowed to be 0
base = self.exact or (self.per_core * (os.cpu_count() or 1))
return min(base, self.max or base)
class BindSettings(BaseModel):
host: str = "0.0.0.0"
port: int = 8000
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="ADVENT22__",
env_nested_delimiter="__",
)
workers: WorkersSettings = WorkersSettings()
bind: BindSettings = BindSettings()
def start():
os.environ["ADVENT22__PRODUCTION_MODE"] = "true"
settings = Settings()
server = Granian(
"advent22_api.app:app",
address=settings.bind.host,
port=settings.bind.port,
workers=settings.workers.count,
interface=Interfaces.ASGI,
loop=Loops.uvloop,
process_name="advent22",
)
server.serve()

1876
api/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,44 @@
[tool.poetry] [project]
authors = [ name = "advent22-api"
"Jörn-Michael Miehe <jmm@yavook.de>", version = "0.2.0"
"Penner42 <unbekannt42@web.de>",
]
description = "" description = ""
license = "MIT" license = {file = "LICENSE"}
name = "advent22_api" readme = "README.md"
version = "0.1.0" requires-python = ">=3.14"
authors = [
{name = "Jörn-Michael Miehe", email = "jmm@yavook.de"},
{name = "Penner42", email = "unbekannt42@web.de"},
]
dependencies = [
"asyncify>=0.12.1",
"cachetools>=7.0.1",
"cachetoolsutils>=11.0",
"fastapi>=0.129.2",
"granian[pname,uvloop]>=2.7.1",
"markdown>=3.10.2",
"numpy>=2.4.2",
"pillow>=12.1.1",
"pydantic-settings>=2.13.1",
"redis[hiredis]>=7.2.0",
"requests>=2.32.5",
"tomli-w>=1.2.0",
"webdavclient3>=3.14.7",
]
[tool.poetry.dependencies] [project.scripts]
Pillow = "^12.1.1" advent22 = "advent22_api.production:start"
asyncify = "^0.12.1"
cachetools = "^7.0.1"
cachetoolsutils = "^11.0"
fastapi = "^0.129.0"
markdown = "^3.10.2"
numpy = "^2.4.2"
pydantic-settings = "^2.13.0"
python = ">=3.11,<3.15"
redis = {extras = ["hiredis"], version = "^7.1.1"}
tomli-w = "^1.2.0"
uvicorn = {extras = ["standard"], version = "^0.40.0"}
webdavclient3 = "^3.14.7"
[tool.poetry.group.dev.dependencies] [dependency-groups]
black = "^26.1.0" dev = [
flake8 = "^7.3.0" "fastapi[standard]>=0.129.2",
pytest = "^9.0.2" "pytest>=9.0.2",
"ruff>=0.15.2",
]
[build-system] [build-system]
build-backend = "poetry.core.masonry.api" requires = ["uv_build>=0.10.4,<0.11.0", "packaging"]
requires = ["poetry-core>=1.0.0"] build-backend = "uv_build"
[tool.uv.build-backend]
# module-name = "advent22_api"
module-root = ""

1177
api/uv.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,67 +0,0 @@
import json
import multiprocessing
import os
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "80")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
if bind_env:
use_bind = bind_env
else:
use_bind = f"{host}:{port}"
cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
if web_concurrency_str:
web_concurrency = int(web_concurrency_str)
assert web_concurrency > 0
else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")
# Gunicorn config variables
loglevel = use_loglevel
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)
# For debugging and testing
log_data = {
"loglevel": loglevel,
"workers": workers,
"bind": bind,
"graceful_timeout": graceful_timeout,
"timeout": timeout,
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
}
print(json.dumps(log_data))

View file

@ -1,20 +0,0 @@
#!/bin/sh
set -e
MODULE_NAME=${MODULE_NAME:-"app.main"}
VARIABLE_NAME=${VARIABLE_NAME:-"app"}
export APP_MODULE="${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}"
export GUNICORN_CONF="${GUNICORN_CONF:-"/usr/local/share/uvicorn-gunicorn/gunicorn_conf.py"}"
export WORKER_CLASS="${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"}"
if [ -f "${PRE_START_PATH}" ] ; then
echo "Running script ${PRE_START_PATH}"
# shellcheck disable=SC1090
. "${PRE_START_PATH}"
fi
# Start Gunicorn
exec gunicorn \
-k "${WORKER_CLASS}" \
-c "${GUNICORN_CONF}" \
"${APP_MODULE}"

View file

@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11

View file

@ -4,11 +4,17 @@
"name": "Advent22 UI", "name": "Advent22 UI",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:4-24-trixie", "dockerComposeFile": "../../.devcontainer/docker_compose.yml",
"service": "ui",
"workspaceFolder": "/workspaces/advent22/ui",
"runServices": ["ui"],
// Features to add to the dev container. More info: https://containers.dev/features. // Features to add to the dev container. More info: https://containers.dev/features.
"features": { "features": {
"ghcr.io/devcontainers/features/git-lfs:1": {}, "ghcr.io/devcontainers/features/git-lfs:1": {},
"ghcr.io/devcontainers-extra/features/zsh-plugins:0": {
"plugins": "git-flow npm nvm yarn"
},
"ghcr.io/devcontainers-extra/features/apt-get-packages:1": { "ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
"packages": "git-flow" "packages": "git-flow"
}, },
@ -26,9 +32,12 @@
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mhutchie.git-graph", "mhutchie.git-graph",
"oxc.oxc-vscode",
"Syler.sass-indented", "Syler.sass-indented",
"vitest.explorer",
"Vue.volar" "Vue.volar"
] ]
} }
@ -38,7 +47,7 @@
// "postCreateCommand": "yarn install", // "postCreateCommand": "yarn install",
// Use 'postStartCommand' to run commands after the container is started. // Use 'postStartCommand' to run commands after the container is started.
"postStartCommand": "yarn dlx update-browserslist-db@latest && yarn install" "postStartCommand": "yarn install"
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [], // "forwardPorts": [],

8
ui/.editorconfig Normal file
View file

@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

View file

@ -1,38 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
"no-empty": "off",
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
overrides: [
{
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
env: {
mocha: true,
},
rules: {
"@typescript-eslint/no-unused-expressions": "off",
}
},
],
};

1
ui/.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

15
ui/.gitignore vendored
View file

@ -1,4 +1,19 @@
# from newly scaffolded vite project
.DS_Store .DS_Store
dist-ssr
coverage
*.local
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
# https://raw.githubusercontent.com/github/gitignore/refs/heads/main/Node.gitignore # https://raw.githubusercontent.com/github/gitignore/refs/heads/main/Node.gitignore

10
ui/.oxlintrc.json Normal file
View file

@ -0,0 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue", "vitest"],
"env": {
"browser": true
},
"categories": {
"correctness": "error"
}
}

7
ui/.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "all"
}

View file

@ -1,3 +1,12 @@
{ {
"recommendations": ["sdras.vue-vscode-snippets"] "recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"mhutchie.git-graph",
"oxc.oxc-vscode",
"Syler.sass-indented",
"vitest.explorer",
"Vue.volar"
]
} }

View file

@ -4,10 +4,11 @@
}, },
"[jsonc]": { "[jsonc]": {
"editor.formatOnSave": false, "editor.formatOnSave": false
}, },
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit" "source.organizeImports": "explicit"
}, },
@ -15,9 +16,14 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.tabSize": 2, "editor.tabSize": 2,
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts, typed-router.d.ts",
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .oxfmt*, .prettier*, prettier*, .editorconfig"
},
"sass.disableAutoIndent": true, "sass.disableAutoIndent": true,
"sass.format.convert": false, "sass.format.convert": false,
"sass.format.deleteWhitespace": true, "sass.format.deleteWhitespace": true
"prettier.trailingComma": "all",
} }

17
ui/.vscode/tasks.json vendored
View file

@ -3,10 +3,21 @@
"tasks": [ "tasks": [
{ {
"type": "npm", "type": "npm",
"script": "serve", "script": "dev",
"problemMatcher": [], "problemMatcher": [],
"label": "UI starten", "label": "UI starten"
"detail": "vue-cli-service serve" },
{
"type": "npm",
"script": "lint",
"problemMatcher": ["$eslint-compact"],
"label": "Linter"
},
{
"type": "npm",
"script": "format",
"problemMatcher": [],
"label": "Formatter"
} }
] ]
} }

View file

@ -1,29 +1,54 @@
# advent22_ui # advent22_ui
## Project setup This template should help get you started developing with Vue 3 in Vite.
``` ## Recommended IDE Setup
yarn install
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
yarn
``` ```
### Compiles and hot-reloads for development ### Compile and Hot-Reload for Development
``` ```sh
yarn serve yarn dev
``` ```
### Compiles and minifies for production ### Type-Check, Compile and Minify for Production
``` ```sh
yarn build yarn build
``` ```
### Lints and fixes files ### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
yarn test:unit
``` ```
### Lint with [ESLint](https://eslint.org/)
```sh
yarn lint yarn lint
``` ```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View file

@ -1,5 +0,0 @@
{
"presets": [
"@vue/cli-plugin-babel/preset"
]
}

1
ui/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

32
ui/eslint.config.ts Normal file
View file

@ -0,0 +1,32 @@
import { globalIgnores } from "eslint/config";
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
import pluginVue from "eslint-plugin-vue";
import pluginVitest from "@vitest/eslint-plugin";
import pluginOxlint from "eslint-plugin-oxlint";
import skipFormatting from "eslint-config-prettier/flat";
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: "app/files-to-lint",
files: ["**/*.{vue,ts,mts,tsx}"],
},
globalIgnores(["**/dist/**", "**/dist-ssr/**", "**/coverage/**"]),
...pluginVue.configs["flat/essential"],
vueTsConfigs.recommended,
{
...pluginVitest.configs.recommended,
files: ["src/**/__tests__/*"],
},
...pluginOxlint.buildFromOxlintConfigFile(".oxlintrc.json"),
skipFormatting,
);

View file

@ -1,11 +1,11 @@
<!DOCTYPE html> <!doctype html>
<html lang=""> <html lang="">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= title %></title>
<!-- Matomo --> <!-- Matomo -->
<script> <script>
let _paq = (window._paq = window._paq || []); let _paq = (window._paq = window._paq || []);
@ -29,12 +29,11 @@
<body> <body>
<noscript> <noscript>
<strong <strong
>Es tut uns leid, aber <%= htmlWebpackPlugin.options.title %> >Es tut uns leid, aber <%= title %> funktioniert nicht richtig ohne JavaScript. Bitte
funktioniert nicht richtig ohne JavaScript. Bitte aktivieren Sie es, um aktivieren Sie es, um fortzufahren.</strong
fortzufahren.</strong
> >
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View file

@ -1,47 +1,63 @@
{ {
"name": "advent22_ui", "name": "advent22_ui",
"version": "0.1.0", "version": "0.2.0",
"private": true, "private": true,
"type": "module",
"packageManager": "yarn@4.12.0", "packageManager": "yarn@4.12.0",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite --host",
"build": "vue-cli-service build", "build": "run-p type-check \"build-only {@}\" --",
"test:unit": "vue-cli-service test:unit", "preview": "vite preview --host",
"test:unit-watch": "vue-cli-service test:unit --watch", "test:unit": "vitest",
"lint": "vue-cli-service lint", "build-only": "vite build",
"ui": "vue ui --host 0.0.0.0 --headless" "type-check": "vue-tsc --build",
"lint": "run-s 'lint:*'",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
"pinia": "^3.0.4",
"vue": "^3.5.28"
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/vue-fontawesome": "^3.1.3", "@fortawesome/vue-fontawesome": "^3.1.3",
"@types/chai": "^5.2.3", "@tsconfig/node24": "^24.0.4",
"@types/jsdom": "^27.0.0",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/mocha": "^10.0.10", "@types/node": "^25.3.0",
"@typescript-eslint/eslint-plugin": "^8.55.0", "@vitejs/plugin-vue": "^6.0.4",
"@typescript-eslint/parser": "^8.55.0", "@vitest/eslint-plugin": "^1.6.9",
"@vue/cli-plugin-babel": "^5.0.9", "@vue/eslint-config-typescript": "^14.7.0",
"@vue/cli-plugin-eslint": "^5.0.9",
"@vue/cli-plugin-typescript": "^5.0.9",
"@vue/cli-plugin-unit-mocha": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.13.5", "axios": "^1.13.5",
"bulma": "^1.0.4", "bulma": "^1.0.4",
"bulma-toast": "2.4.3", "bulma-toast": "2.4.3",
"chai": "^6.2.2", "eslint": "^10.0.1",
"core-js": "^3.48.0", "eslint-config-prettier": "^10.1.8",
"eslint": "^8.57.1", "eslint-plugin-oxlint": "~1.49.0",
"eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue": "~10.8.0",
"jiti": "^2.6.1",
"jsdom": "^28.1.0",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"pinia": "^3.0.4", "npm-run-all2": "^8.0.4",
"sass": "~1.94.3", "oxlint": "~1.49.0",
"sass-loader": "^16.0.0", "prettier": "3.8.1",
"typescript": "^5.9.3", "sass-embedded": "^1.97.3",
"vue": "^3.5.25", "typescript": "~5.9.3",
"vue-cli-plugin-webpack-bundle-analyzer": "^4.0.0" "vite": "^7.3.1",
"vite-bundle-analyzer": "^1.3.6",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^8.0.6",
"vitest": "^4.0.18",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
} }
} }

View file

@ -7,15 +7,8 @@
</section> </section>
<section class="section px-3"> <section class="section px-3">
<progress <progress v-if="store.background_image === 'loading'" class="progress is-primary" max="100" />
v-if="store.background_image === 'loading'" <div v-else-if="store.background_image === 'error'" class="notification is-danger">
class="progress is-primary"
max="100"
/>
<div
v-else-if="store.background_image === 'error'"
class="notification is-danger"
>
Hintergrundbild konnte nicht geladen werden Hintergrundbild konnte nicht geladen werden
</div> </div>
<div v-else class="container"> <div v-else class="container">

View file

@ -1,4 +1,4 @@
import { expect } from "chai"; import { describe, expect, it } from "vitest";
import { Rectangle } from "@/lib/rects/rectangle"; import { Rectangle } from "@/lib/rects/rectangle";
import { Vector2D } from "@/lib/rects/vector2d"; import { Vector2D } from "@/lib/rects/vector2d";
@ -17,56 +17,60 @@ describe("Rectangle Tests", () => {
width: number, width: number,
height: number, height: number,
): void { ): void {
expect(r.left).to.equal(left); expect(r.left).toEqual(left);
expect(r.top).to.equal(top); expect(r.top).toEqual(top);
expect(r.width).to.equal(width); expect(r.width).toEqual(width);
expect(r.height).to.equal(height); expect(r.height).toEqual(height);
expect(r.area).to.equal(width * height); expect(r.area).toEqual(width * height);
expect(r.middle.x).to.equal(left + 0.5 * width); expect(r.middle.x).toEqual(left + 0.5 * width);
expect(r.middle.y).to.equal(top + 0.5 * height); expect(r.middle.y).toEqual(top + 0.5 * height);
} }
it("should create a default rectangle", () => { it("should create a default rectangle", () => {
expect.hasAssertions();
check_rectangle(new Rectangle(), 0, 0, 0, 0); check_rectangle(new Rectangle(), 0, 0, 0, 0);
}); });
it("should create a rectangle", () => { it("should create a rectangle", () => {
expect.hasAssertions();
check_rectangle(r1, 1, 2, 3, 4); check_rectangle(r1, 1, 2, 3, 4);
}); });
it("should create the same rectangle backwards", () => { it("should create the same rectangle backwards", () => {
expect.hasAssertions();
check_rectangle(r2, 1, 2, 3, 4); check_rectangle(r2, 1, 2, 3, 4);
}); });
it("should compare rectangles", () => { it("should compare rectangles", () => {
expect(r1.equals(r2)).to.be.true; expect(r1.equals(r2)).toBe(true);
expect(r1.equals(new Rectangle())).to.be.false; expect(r1.equals(new Rectangle())).toBe(false);
}); });
it("should create the same rectangle transposed", () => { it("should create the same rectangle transposed", () => {
const v1t = new Vector2D(v1.x, v2.y); const v1t = new Vector2D(v1.x, v2.y);
const v2t = new Vector2D(v2.x, v1.y); const v2t = new Vector2D(v2.x, v1.y);
expect(r1.equals(new Rectangle(v1t, v2t))).to.be.true; expect(r1.equals(new Rectangle(v1t, v2t))).toBe(true);
}); });
it("should contain itself", () => { it("should contain itself", () => {
expect(r1.contains(v1)).to.be.true; expect(r1.contains(v1)).toBe(true);
expect(r1.contains(v2)).to.be.true; expect(r1.contains(v2)).toBe(true);
expect(r1.contains(r1.origin)).to.be.true; expect(r1.contains(r1.origin)).toBe(true);
expect(r1.contains(r1.corner)).to.be.true; expect(r1.contains(r1.corner)).toBe(true);
expect(r1.contains(r1.middle)).to.be.true; expect(r1.contains(r1.middle)).toBe(true);
}); });
it("should not contain certain points", () => { it("should not contain certain points", () => {
expect(r1.contains(new Vector2D(0, 0))).to.be.false; expect(r1.contains(new Vector2D(0, 0))).toBe(false);
expect(r1.contains(new Vector2D(100, 100))).to.be.false; expect(r1.contains(new Vector2D(100, 100))).toBe(false);
}); });
it("should update a rectangle", () => { it("should update a rectangle", () => {
expect.hasAssertions();
const v = new Vector2D(1, 1); const v = new Vector2D(1, 1);
check_rectangle(r1.update(v1.plus(v), undefined), 2, 3, 2, 3); check_rectangle(r1.update(v1.plus(v), undefined), 2, 3, 2, 3);
@ -83,6 +87,7 @@ describe("Rectangle Tests", () => {
}); });
it("should move a rectangle", () => { it("should move a rectangle", () => {
expect.hasAssertions();
const v = new Vector2D(1, 1); const v = new Vector2D(1, 1);
check_rectangle(r1.move(v), 2, 3, 3, 4); check_rectangle(r1.move(v), 2, 3, 3, 4);

View file

@ -1,4 +1,4 @@
import { expect } from "chai"; import { describe, expect, it } from "vitest";
import { Vector2D } from "@/lib/rects/vector2d"; import { Vector2D } from "@/lib/rects/vector2d";
@ -7,35 +7,35 @@ describe("Vector2D Tests", () => {
it("should create a default vector", () => { it("should create a default vector", () => {
const v0 = new Vector2D(); const v0 = new Vector2D();
expect(v0.x).to.equal(0); expect(v0.x).toEqual(0);
expect(v0.y).to.equal(0); expect(v0.y).toEqual(0);
}); });
it("should create a vector", () => { it("should create a vector", () => {
expect(v.x).to.equal(1); expect(v.x).toEqual(1);
expect(v.y).to.equal(2); expect(v.y).toEqual(2);
}); });
it("should add vectors", () => { it("should add vectors", () => {
const v2 = v.plus(new Vector2D(3, 4)); const v2 = v.plus(new Vector2D(3, 4));
expect(v2.x).to.equal(4); expect(v2.x).toEqual(4);
expect(v2.y).to.equal(6); expect(v2.y).toEqual(6);
}); });
it("should subtract vectors", () => { it("should subtract vectors", () => {
const v2 = v.minus(new Vector2D(3, 4)); const v2 = v.minus(new Vector2D(3, 4));
expect(v2.x).to.equal(-2); expect(v2.x).toEqual(-2);
expect(v2.y).to.equal(-2); expect(v2.y).toEqual(-2);
}); });
it("should scale vectors", () => { it("should scale vectors", () => {
const v2 = v.scale(3); const v2 = v.scale(3);
expect(v2.x).to.equal(3); expect(v2.x).toEqual(3);
expect(v2.y).to.equal(6); expect(v2.y).toEqual(6);
}); });
it("should compare vectors", () => { it("should compare vectors", () => {
expect(v.equals(v.scale(1))).to.be.true; expect(v.equals(v.scale(1))).toBe(true);
expect(v.equals(v.scale(2))).to.be.false; expect(v.equals(v.scale(2))).toBe(false);
}); });
}); });

View file

@ -4,8 +4,8 @@
<BulmaToast @handle="on_toast_handle" class="content"> <BulmaToast @handle="on_toast_handle" class="content">
<p> <p>
Du hast noch keine Türchen geöffnet, vielleicht gibt es ein Anzeigeproblem Du hast noch keine Türchen geöffnet, vielleicht gibt es ein Anzeigeproblem in Deinem
in Deinem Webbrowser? Webbrowser?
</p> </p>
<div class="level"> <div class="level">
<div class="level-item"> <div class="level-item">

View file

@ -12,12 +12,7 @@
<div class="field"> <div class="field">
<label class="label">Username</label> <label class="label">Username</label>
<div class="control"> <div class="control">
<input <input ref="username_input" class="input" type="text" v-model="creds.username" />
ref="username_input"
class="input"
type="text"
v-model="creds.username"
/>
</div> </div>
</div> </div>

View file

@ -18,10 +18,7 @@
</template> </template>
</div> </div>
<button <button v-if="state.show !== 'loading'" class="modal-close is-large has-background-primary" />
v-if="state.show !== 'loading'"
class="modal-close is-large has-background-primary"
/>
</div> </div>
</template> </template>

View file

@ -7,12 +7,9 @@
Alle {{ store.user_doors.length }} Türchen offen! Alle {{ store.user_doors.length }} Türchen offen!
</template> </template>
<template v-else> <template v-else>
<template v-if="store.user_doors.length === 0"> <template v-if="store.user_doors.length === 0"> Zeit bis zum ersten Türchen: </template>
Zeit bis zum ersten Türchen:
</template>
<template v-else> <template v-else>
{{ store.user_doors.length }} Türchen offen. Zeit bis zum nächsten {{ store.user_doors.length }} Türchen offen. Zeit bis zum nächsten Türchen:
Türchen:
</template> </template>
<CountDown :until="store.next_door_target" /> <CountDown :until="store.next_door_target" />
</template> </template>

View file

@ -8,7 +8,7 @@
<h3>Zuordnung Buchstaben</h3> <h3>Zuordnung Buchstaben</h3>
<div class="tags are-medium"> <div class="tags are-medium">
<template v-for="(data, day) in day_data" :key="`part-${day}`"> <template v-for="[day, data] in day_data" :key="`part-${day}`">
<span v-if="data.part === ''" class="tag is-warning"> <span v-if="data.part === ''" class="tag is-warning">
{{ day }} {{ day }}
</span> </span>
@ -21,7 +21,7 @@
<h3>Zuordnung Bilder</h3> <h3>Zuordnung Bilder</h3>
<div class="tags are-medium"> <div class="tags are-medium">
<span <span
v-for="(data, day) in day_data" v-for="[day, data] in day_data"
:key="`image-${day}`" :key="`image-${day}`"
:class="'tag is-' + (data.part === '' ? 'warning' : 'primary')" :class="'tag is-' + (data.part === '' ? 'warning' : 'primary')"
> >
@ -32,7 +32,7 @@
<h3>Alle Türchen</h3> <h3>Alle Türchen</h3>
<div class="tags are-medium"> <div class="tags are-medium">
<BulmaButton <BulmaButton
v-for="(data, day) in day_data" v-for="[day, data] in day_data"
:key="`btn-${day}`" :key="`btn-${day}`"
:class="'tag is-' + (data.part === '' ? 'warning' : 'info')" :class="'tag is-' + (data.part === '' ? 'warning' : 'info')"
:icon="['fas', 'fa-door-open']" :icon="['fas', 'fa-door-open']"
@ -48,14 +48,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { API } from "@/lib/api"; import { API } from "@/lib/api";
import { name_door, objForEach } from "@/lib/helpers"; import { name_door, objForEach } from "@/lib/helpers";
import type { ImageData, NumStrDict } from "@/lib/model"; import type { ImageData } from "@/lib/model";
import { ref } from "vue"; import { ref } from "vue";
import MultiModal, { type HMultiModal } from "../MultiModal.vue"; import MultiModal, { type HMultiModal } from "../MultiModal.vue";
import BulmaButton from "../bulma/Button.vue"; import BulmaButton from "../bulma/Button.vue";
import BulmaDrawer from "../bulma/Drawer.vue"; import BulmaDrawer from "../bulma/Drawer.vue";
const day_data = ref<Record<number, { part: string; image_name: string }>>({}); interface DayData {
part: string;
image_name: string;
}
const day_data = ref<Map<number, DayData>>(new Map());
let modal: HMultiModal | undefined; let modal: HMultiModal | undefined;
@ -65,24 +70,31 @@ function on_modal_handle(handle: HMultiModal): void {
async function on_open(): Promise<void> { async function on_open(): Promise<void> {
const [day_parts, day_image_names] = await Promise.all([ const [day_parts, day_image_names] = await Promise.all([
API.request<NumStrDict>("admin/day_parts"), API.request<Record<number, string>>("admin/day_parts"),
API.request<NumStrDict>("admin/day_image_names"), API.request<Record<number, string>>("admin/day_image_names"),
]); ]);
const _ensure_day_in_data = (day: number) => { const _get_data = (day: number) => {
if (!(day in day_data.value)) { let result = day_data.value.get(day);
day_data.value[day] = { part: "", image_name: "" };
if (result === undefined) {
result = { part: "", image_name: "" };
day_data.value.set(day, result);
} }
return result;
}; };
// for (const [day, part] of day_parts.entries()) {
// _get_data(day).part = part;
// }
objForEach(day_parts, (day, part) => { objForEach(day_parts, (day, part) => {
_ensure_day_in_data(day); _get_data(day).part = part;
day_data.value[day].part = part;
}); });
objForEach(day_image_names, (day, image_name) => { objForEach(day_image_names, (day, image_name) => {
_ensure_day_in_data(day); _get_data(day).image_name = image_name;
day_data.value[day].image_name = image_name;
}); });
} }

View file

@ -9,15 +9,11 @@
<dt>Wert</dt> <dt>Wert</dt>
<dd> <dd>
Eingabe: Eingabe:
<span class="is-family-monospace"> <span class="is-family-monospace"> "{{ admin_config_model.solution.value }}" </span>
"{{ admin_config_model.solution.value }}"
</span>
</dd> </dd>
<dd> <dd>
Ausgabe: Ausgabe:
<span class="is-family-monospace"> <span class="is-family-monospace"> "{{ admin_config_model.solution.clean }}" </span>
"{{ admin_config_model.solution.clean }}"
</span>
</dd> </dd>
<dt>Transformation</dt> <dt>Transformation</dt>
@ -47,9 +43,7 @@
<dd>{{ store.user_doors.length }}</dd> <dd>{{ store.user_doors.length }}</dd>
<dt>Zeit zum nächsten Türchen</dt> <dt>Zeit zum nächsten Türchen</dt>
<dd v-if="store.next_door_target === null"> <dd v-if="store.next_door_target === null">Kein nächstes Türchen</dd>
Kein nächstes Türchen
</dd>
<dd v-else><CountDown :until="store.next_door_target" /></dd> <dd v-else><CountDown :until="store.next_door_target" /></dd>
<dt>Erstes Türchen</dt> <dt>Erstes Türchen</dt>
@ -65,9 +59,7 @@
<dd>{{ fmt_puzzle_date("end") }}</dd> <dd>{{ fmt_puzzle_date("end") }}</dd>
<dt>Zufalls-Seed</dt> <dt>Zufalls-Seed</dt>
<dd class="is-family-monospace"> <dd class="is-family-monospace">"{{ admin_config_model.puzzle.seed }}"</dd>
"{{ admin_config_model.puzzle.seed }}"
</dd>
<dt>Extra-Tage</dt> <dt>Extra-Tage</dt>
<dd> <dd>
@ -121,10 +113,7 @@
<dd>{{ admin_config_model.image.border }} px</dd> <dd>{{ admin_config_model.image.border }} px</dd>
<dt>Schriftarten</dt> <dt>Schriftarten</dt>
<dd <dd v-for="(font, index) in admin_config_model.fonts" :key="`font-${index}`">
v-for="(font, index) in admin_config_model.fonts"
:key="`font-${index}`"
>
{{ font.file }} ({{ font.size }} pt) {{ font.file }} ({{ font.size }} pt)
</dd> </dd>
</dl> </dl>
@ -243,7 +232,7 @@ const admin_config_model = ref<AdminConfigModel>({
}); });
const doors = ref<DoorSaved[]>([]); const doors = ref<DoorSaved[]>([]);
const creds = ref<Record<string, Credentials>>({ const creds = ref({
dav: { dav: {
username: "", username: "",
password: "", password: "",
@ -276,10 +265,7 @@ async function on_open(): Promise<void> {
clear_credentials(creds.value.ui); clear_credentials(creds.value.ui);
} }
async function load_credentials( async function load_credentials(creds: Credentials, endpoint: string): Promise<void> {
creds: Credentials,
endpoint: string,
): Promise<void> {
try { try {
const new_creds = await API.request<Credentials>(endpoint); const new_creds = await API.request<Credentials>(endpoint);

View file

@ -8,11 +8,7 @@
:icon="['fas', 'fa-backward']" :icon="['fas', 'fa-backward']"
/> />
<BulmaBreadcrumbs <BulmaBreadcrumbs :steps="steps" v-model="current_step" class="level-item mb-0" />
:steps="steps"
v-model="current_step"
class="level-item mb-0"
/>
<BulmaButton <BulmaButton
:disabled="current_step === 2" :disabled="current_step === 2"

View file

@ -3,11 +3,7 @@
<button class="button"> <button class="button">
<slot name="default"> <slot name="default">
<span v-if="icon !== undefined" class="icon"> <span v-if="icon !== undefined" class="icon">
<FontAwesomeIcon <FontAwesomeIcon v-if="icon !== undefined" :icon="icon" :beat-fade="busy" />
v-if="icon !== undefined"
:icon="icon"
:beat-fade="busy"
/>
</span> </span>
</slot> </slot>
<span v-if="text !== undefined">{{ text }}</span> <span v-if="text !== undefined">{{ text }}</span>

View file

@ -6,18 +6,13 @@
<p v-if="refreshable && is_open" class="card-header-icon px-0"> <p v-if="refreshable && is_open" class="card-header-icon px-0">
<BulmaButton class="is-small is-primary" @click="load"> <BulmaButton class="is-small is-primary" @click="load">
<FontAwesomeIcon <FontAwesomeIcon :icon="['fas', 'arrows-rotate']" :spin="state === 'loading'" />
:icon="['fas', 'arrows-rotate']"
:spin="state === 'loading'"
/>
</BulmaButton> </BulmaButton>
</p> </p>
<button class="card-header-icon" @click="toggle"> <button class="card-header-icon" @click="toggle">
<span class="icon"> <span class="icon">
<FontAwesomeIcon <FontAwesomeIcon :icon="['fas', is_open ? 'angle-down' : 'angle-right']" />
:icon="['fas', is_open ? 'angle-down' : 'angle-right']"
/>
</span> </span>
</button> </button>
</header> </header>

View file

@ -6,7 +6,11 @@
> >
<div <div
class="has-text-danger" class="has-text-danger"
style="text-shadow: 0 0 10px white, 0 0 20px white" style="
text-shadow:
0 0 10px white,
0 0 20px white;
"
> >
{{ door.day }} {{ door.day }}
</div> </div>

View file

@ -27,13 +27,7 @@ import { computed } from "vue";
const store = advent22Store(); const store = advent22Store();
type BulmaVariant = type BulmaVariant = "primary" | "link" | "info" | "success" | "warning" | "danger";
| "primary"
| "link"
| "info"
| "success"
| "warning"
| "danger";
withDefaults( withDefaults(
defineProps<{ defineProps<{

View file

@ -32,11 +32,7 @@ function get_event_thous(event: MouseEvent): Vector2D {
type TCEventType = "mousedown" | "mousemove" | "mouseup" | "click" | "dblclick"; type TCEventType = "mousedown" | "mousemove" | "mouseup" | "click" | "dblclick";
const is_tceventtype = (t: unknown): t is TCEventType => const is_tceventtype = (t: unknown): t is TCEventType =>
t === "mousedown" || t === "mousedown" || t === "mousemove" || t === "mouseup" || t === "click" || t === "dblclick";
t === "mousemove" ||
t === "mouseup" ||
t === "click" ||
t === "dblclick";
const emit = defineEmits<{ const emit = defineEmits<{
(event: TCEventType, e: MouseEvent, point: Vector2D): void; (event: TCEventType, e: MouseEvent, point: Vector2D): void;

View file

@ -14,12 +14,7 @@
:door="door" :door="door"
force_visible force_visible
/> />
<SVGRect <SVGRect v-if="preview_visible" variant="success" :rectangle="preview" visible />
v-if="preview_visible"
variant="success"
:rectangle="preview"
visible
/>
</ThouCanvas> </ThouCanvas>
</template> </template>

View file

@ -14,9 +14,10 @@
<img :src="unwrap_loading(store.background_image).data_url" /> <img :src="unwrap_loading(store.background_image).data_url" />
<ThouCanvas> <ThouCanvas>
<PreviewDoor <PreviewDoor
v-for="(_, index) in model" v-for="(door, index) in model"
:key="`door-${index}`" :key="`door-${index}`"
v-model="model[index]" :model-value="door"
@update:model-value="updateAt(index, door)"
/> />
</ThouCanvas> </ThouCanvas>
</figure> </figure>
@ -24,7 +25,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { type VueLike, unwrap_loading } from "@/lib/helpers"; import { unwrap_loading, type VueLike } from "@/lib/helpers";
import { Door } from "@/lib/rects/door"; import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store"; import { advent22Store } from "@/lib/store";
@ -32,5 +33,12 @@ import ThouCanvas from "../calendar/ThouCanvas.vue";
import PreviewDoor from "./PreviewDoor.vue"; import PreviewDoor from "./PreviewDoor.vue";
const model = defineModel<VueLike<Door>[]>({ required: true }); const model = defineModel<VueLike<Door>[]>({ required: true });
function updateAt(i: number, val: VueLike<Door>) {
const copy = [...model.value];
copy[i] = val;
model.value = copy;
}
const store = advent22Store(); const store = advent22Store();
</script> </script>

View file

@ -1,6 +0,0 @@
/* eslint-disable */
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View file

@ -16,23 +16,9 @@ interface Params {
} }
export class API { export class API {
private static get api_baseurl(): string {
// in production mode, return "proto://hostname/api"
if (process.env.NODE_ENV === "production") {
return `${window.location.protocol}//${window.location.host}/api`;
} else if (process.env.NODE_ENV !== "development") {
// not in prouction or development mode
// eslint-disable-next-line no-console
console.warn("Unexpected NODE_ENV value: ", process.env.NODE_ENV);
}
// in development mode, return "proto://hostname:8000/api"
return `${window.location.protocol}//${window.location.hostname}:8000/api`;
}
private static readonly axios = axios.create({ private static readonly axios = axios.create({
timeout: 10e3, timeout: 15e3,
baseURL: this.api_baseurl, baseURL: "/api",
}); });
private static readonly creds_key = "advent22/credentials"; private static readonly creds_key = "advent22/credentials";
@ -84,7 +70,6 @@ export class API {
const response = await this.axios.request<T>(this.get_axios_config(p)); const response = await this.axios.request<T>(this.get_axios_config(p));
return response.data; return response.data;
} catch (reason) { } catch (reason) {
// eslint-disable-next-line no-console
console.error(`Failed to query ${p.endpoint}: ${reason}`); console.error(`Failed to query ${p.endpoint}: ${reason}`);
throw new APIError(reason, p.endpoint); throw new APIError(reason, p.endpoint);
} }

View file

@ -1,10 +1,7 @@
import { nextTick, type UnwrapRef } from "vue"; import { nextTick, type UnwrapRef } from "vue";
import { APIError } from "./api_error"; import { APIError } from "./api_error";
export function objForEach<T>( export function objForEach<T>(obj: T, f: (k: keyof T, v: T[keyof T]) => void): void {
obj: T,
f: (k: keyof T, v: T[keyof T]) => void,
): void {
for (const k in obj) { for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) { if (Object.prototype.hasOwnProperty.call(obj, k)) {
f(k, obj[k]); f(k, obj[k]);
@ -42,7 +39,6 @@ export function handle_error(error: unknown): void {
if (error instanceof APIError) { if (error instanceof APIError) {
error.alert(); error.alert();
} else { } else {
// eslint-disable-next-line no-console
console.error(error); console.error(error);
} }
} }

View file

@ -45,10 +45,6 @@ export interface SiteConfigModel {
footer: string; footer: string;
} }
export interface NumStrDict {
[key: number]: string;
}
export interface DoorSaved { export interface DoorSaved {
day: number; day: number;
x1: number; x1: number;

View file

@ -24,10 +24,7 @@ export class Door {
// integer coercion // integer coercion
let day = Number(value); let day = Number(value);
day = day = !Number.isNaN(day) && Number.isFinite(day) ? Math.trunc(day) : Door.MIN_DAY;
!Number.isNaN(day) && Number.isFinite(day)
? Math.trunc(day)
: Door.MIN_DAY;
this._day = Math.max(day, Door.MIN_DAY); this._day = Math.max(day, Door.MIN_DAY);
} }

View file

@ -71,9 +71,6 @@ export class Rectangle {
} }
public move(vector: Vector2D): Rectangle { public move(vector: Vector2D): Rectangle {
return new Rectangle( return new Rectangle(this.corner_1.plus(vector), this.corner_2.plus(vector));
this.corner_1.plus(vector),
this.corner_2.plus(vector),
);
} }
} }

View file

@ -1,4 +1,4 @@
import { acceptHMRUpdate, defineStore } from "pinia"; import { defineStore } from "pinia";
import { API } from "./api"; import { API } from "./api";
import type { Loading } from "./helpers"; import type { Loading } from "./helpers";
import type { Credentials, DoorSaved, ImageData, SiteConfigModel } from "./model"; import type { Credentials, DoorSaved, ImageData, SiteConfigModel } from "./model";
@ -55,8 +55,7 @@ export const advent22Store = defineStore("advent22", {
const favicon = await API.request<ImageData>("user/favicon"); const favicon = await API.request<ImageData>("user/favicon");
const link: HTMLLinkElement = const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") ?? document.querySelector("link[rel*='icon']") ?? document.createElement("link");
document.createElement("link");
link.rel = "shortcut icon"; link.rel = "shortcut icon";
link.type = "image/x-icon"; link.type = "image/x-icon";
link.href = favicon.data_url; link.href = favicon.data_url;
@ -66,8 +65,7 @@ export const advent22Store = defineStore("advent22", {
} catch {} } catch {}
try { try {
const [is_admin, site_config, background_image, user_doors, next_door] = const [is_admin, site_config, background_image, user_doors, next_door] = await Promise.all([
await Promise.all([
this.update_is_admin(), this.update_is_admin(),
API.request<SiteConfigModel>("user/site_config"), API.request<SiteConfigModel>("user/site_config"),
API.request<ImageData>("user/background_image"), API.request<ImageData>("user/background_image"),
@ -78,8 +76,7 @@ export const advent22Store = defineStore("advent22", {
document.title = site_config.title; document.title = site_config.title;
if (site_config.subtitle !== "") if (site_config.subtitle !== "") document.title += " " + site_config.subtitle;
document.title += " " + site_config.subtitle;
this.site_config = site_config; this.site_config = site_config;
this.background_image = background_image; this.background_image = background_image;
@ -123,9 +120,3 @@ export const advent22Store = defineStore("advent22", {
}, },
}, },
}); });
if (import.meta.webpackHot) {
import.meta.webpackHot.accept(
acceptHMRUpdate(advent22Store, import.meta.webpackHot),
);
}

12
ui/src/stores/counter.ts Normal file
View file

@ -0,0 +1,12 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});

14
ui/tsconfig.app.json Normal file
View file

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"lib": ["es2020", "dom", "dom.iterable", "es2022.object", "es2023.array"],
"paths": {
"@/*": ["./src/*"]
}
}
}

View file

@ -1,33 +1,14 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "files": [],
"compilerOptions": { "references": [
"experimentalDecorators": true, {
"lib": [ "path": "./tsconfig.node.json"
"es2020",
"dom",
"dom.iterable",
"es2022.object",
"es2023.array",
],
// "moduleResolution": "node",
// "sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"mocha",
"chai",
],
"paths": {
"@/*": [
"src/*",
]
}, },
{
"path": "./tsconfig.app.json"
}, },
"include": [ {
"src/**/*.vue", "path": "./tsconfig.vitest.json"
"src/**/*.ts", }
// "src/**/*.tsx", ]
"tests/**/*.ts",
// "tests/**/*.tsx",
],
} }

19
ui/tsconfig.node.json Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

10
ui/tsconfig.vitest.json Normal file
View file

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.app.json",
"include": ["src/**/__tests__/*", "env.d.ts"],
"exclude": [],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"types": ["node", "jsdom"]
}
}

45
ui/vite.config.ts Normal file
View file

@ -0,0 +1,45 @@
import { fileURLToPath, URL } from "node:url";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import { analyzer } from "vite-bundle-analyzer";
import { createHtmlPlugin } from "vite-plugin-html";
import vueDevTools from "vite-plugin-vue-devtools";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
analyzer({
analyzerMode: "static",
}),
createHtmlPlugin({
inject: {
data: {
title: "Kalender-Gewinnspiel",
},
},
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
sourcemap: true,
},
server: {
proxy: {
"/api": {
target: "http://api:8000",
changeOrigin: true,
secure: false,
},
},
},
});

14
ui/vitest.config.ts Normal file
View file

@ -0,0 +1,14 @@
import { fileURLToPath } from "node:url";
import { mergeConfig, defineConfig, configDefaults } from "vitest/config";
import viteConfig from "./vite.config";
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: "jsdom",
exclude: [...configDefaults.exclude, "e2e/**"],
root: fileURLToPath(new URL("./", import.meta.url)),
},
}),
);

View file

@ -1,26 +0,0 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const { defineConfig } = require("@vue/cli-service");
const webpack = require("webpack");
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
host: "127.0.0.1",
},
pages: {
index: {
entry: "src/main.ts",
title: "Kalender-Gewinnspiel",
},
},
// https://stackoverflow.com/a/77765007
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
// Vue CLI is in maintenance mode, and probably won't merge my PR to fix this in their tooling
// https://github.com/vuejs/vue-cli/pull/7443
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
}),
],
},
});

File diff suppressed because it is too large Load diff