Compare commits

..

No commits in common. "master" and "0.1.0" have entirely different histories.

75 changed files with 10041 additions and 4357 deletions

View file

@ -1,14 +0,0 @@
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,12 +11,9 @@
**/.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
@ -27,7 +24,4 @@
**/npm-debug.log* **/npm-debug.log*
**/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,6 +1,51 @@
ARG NODE_VERSION=24 ARG NODE_VERSION=24
ARG PYTHON_VERSION=3.14 ARG PYTHON_VERSION=3.14-slim
#############
# 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 #
@ -9,90 +54,76 @@ ARG PYTHON_VERSION=3.14
ARG NODE_VERSION ARG NODE_VERSION
FROM node:${NODE_VERSION} AS build-ui FROM node:${NODE_VERSION} AS build-ui
# install ui dependencies # env setup
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;
# copy and build ui # install advent22_ui dependencies
COPY ui/package*.json ui/yarn*.lock ./
RUN set -ex; \
corepack enable; \
yarn install;
# copy and build advent22_ui
COPY ui ./ COPY ui ./
RUN --mount=type=cache,id=ui,target=/root/.yarn \ RUN set -ex; \
set -ex; \ yarn dlx update-browserslist-db@latest; \
\ yarn build --dest /tmp/advent22_ui/html; \
yarn build --outDir /opt/advent22/ui; \ # exclude webpack-bundle-analyzer output
# exclude vite-bundle-analyzer output rm -f /tmp/advent22_ui/html/report.html;
rm /opt/advent22/ui/stats.html;
######################
############### # python preparation #
# install app # ######################
###############
ARG PYTHON_VERSION ARG PYTHON_VERSION
FROM dhi.io/python:${PYTHON_VERSION}-dev AS install-app FROM python:${PYTHON_VERSION} AS uvicorn-gunicorn
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
WORKDIR /opt/advent22 ENV \
ENV UV_COMPILE_BYTECODE=1 \ PRODUCTION_MODE="true" \
UV_NO_DEV=1 \ PORT="8000" \
UV_LINK_MODE="copy" MODULE_NAME="advent22_api.app"
EXPOSE 8000
RUN --mount=type=bind,source=api/uv.lock,target=api/uv.lock \ WORKDIR /opt/advent22
--mount=type=bind,source=api/pyproject.toml,target=api/pyproject.toml \ VOLUME [ "/opt/advent22" ]
--mount=type=bind,source=api/.python-version,target=api/.python-version \
--mount=type=cache,id=api,target=/root/.cache/uv \ COPY --from=build-api /usr/local/src/advent22_api/dist /usr/local/share/advent22_api.dist
set -ex; \ RUN set -ex; \
# remove example app
rm -rf /app; \
\
# # 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
mkdir data; \ chown nobody:nogroup ./
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 \
;
# install api # add prepared advent22_ui
COPY api api/ COPY --from=build-ui /tmp/advent22_ui /usr/local/share/advent22_ui
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

@ -1,62 +1,56 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python // README at: https://github.com/devcontainers/templates/tree/main/src/python
{ {
"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
"dockerComposeFile": "../../.devcontainer/docker_compose.yml", "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie",
"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/uv:1": {}, "ghcr.io/devcontainers-extra/features/poetry:2": {},
"ghcr.io/devcontainers-extra/features/zsh-plugins:0": { "ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
"plugins": "git-flow uv" "packages": "git-flow"
},
"ghcr.io/itsmechlark/features/redis-server:1": {}
}, },
"ghcr.io/devcontainers-extra/features/apt-get-packages:1": {
"packages": "git-flow" "containerEnv": {
"TZ": "Europe/Berlin"
}, },
"ghcr.io/itsmechlark/features/redis-server:1": {}
},
"containerEnv": { // Configure tool-specific properties.
"TZ": "Europe/Berlin", "customizations": {
"UV_CACHE_DIR": "/workspaces/advent22/.uv_cache" // Configure properties specific to VS Code.
}, "vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"terminal.integrated.defaultProfile.linux": "zsh"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"be5invis.toml",
"mhutchie.git-graph",
"ms-python.python",
"ms-python.black-formatter",
"ms-python.flake8",
"ms-python.isort",
"ms-python.vscode-pylance"
]
}
},
// Configure tool-specific properties. // Use 'postCreateCommand' to run commands after the container is created.
"customizations": { "postCreateCommand": "sudo /usr/local/py-utils/bin/poetry self add poetry-plugin-up",
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"terminal.integrated.defaultProfile.linux": "zsh"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"astral-sh.ty",
"charliermarsh.ruff",
"be5invis.toml",
"mhutchie.git-graph",
"ms-python.python",
"ms-python.vscode-pylance"
]
}
},
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postStartCommand' to run commands after the container is started.
"postCreateCommand": "uv tool install uv-upx", "postStartCommand": "poetry install"
// Use 'postStartCommand' to run commands after the container is started. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"postStartCommand": "uv tool upgrade uv-upx && uv sync" // "forwardPorts": [],
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "forwardPorts": [], // "remoteUser": "root"
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

4
api/.flake8 Normal file
View file

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

3
api/.isort.cfg Normal file
View file

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

View file

@ -1 +0,0 @@
3.14

View file

@ -1,29 +1,22 @@
{ {
// Verwendet IntelliSense zum Ermitteln möglicher Attribute. // Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "FastAPI CLI (dev)", "name": "Main Module",
"type": "debugpy", "type": "python",
"request": "launch", "request": "launch",
"module": "fastapi", "module": "advent22_api.main",
"args": [ "pythonArgs": [
"dev", "-Xfrozen_modules=off",
"--host", ],
"0.0.0.0", "env": {
"--port", "PYDEVD_DISABLE_FILE_VALIDATION": "1",
"8000", "WEBDAV__CACHE_TTL": "30",
"--entrypoint", },
"advent22_api.app:app", "justMyCode": true,
"--reload-dir", }
"${workspaceFolder}/advent22_api" ]
], }
"env": {
"ADVENT22__WEBDAV__CACHE_TTL": "30"
},
"justMyCode": true
}
]
}

View file

@ -3,16 +3,31 @@
"[python]": { "[python]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff", "editor.defaultFormatter": "ms-python.black-formatter",
"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",
],
"ty.diagnosticMode": "workspace", "black-formatter.importStrategy": "fromEnvironment",
"ruff.nativeServer": "on" "flake8.importStrategy": "fromEnvironment",
"isort.importStrategy": "fromEnvironment",
} }

View file

@ -1,21 +0,0 @@
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.

View file

View file

@ -1,4 +1,5 @@
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
@ -32,3 +33,14 @@ 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 Self, cast from typing import AnyStr, Self, TypeAlias
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
type _RGB = tuple[int, int, int] _RGB: TypeAlias = tuple[int, int, int]
type _XY = tuple[float, float] _XY: TypeAlias = tuple[float, float]
type _Box = tuple[int, int, int, int] _Box: TypeAlias = 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: str, text: AnyStr,
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 cast(_RGB, mean_color.astype(int)) return _RGB(mean_color.astype(int))
async def hide_text( async def hide_text(
self, self,
xy: _XY, xy: _XY,
text: str, text: AnyStr,
font: FreeTypeFont, font: FreeTypeFont,
anchor: str | None = "mm", anchor: str | None = "mm",
**text_kwargs, **text_kwargs,
@ -134,10 +134,8 @@ class AdventImage:
else: else:
tc_v -= 3 tc_v -= 3
text_color: tuple[int | float, int | float, int | float] = colorsys.hsv_to_rgb( text_color = colorsys.hsv_to_rgb(tc_h, tc_s, tc_v)
tc_h, tc_s, tc_v text_color = _RGB(int(val) for val in text_color)
)
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,4 +1,5 @@
import tomllib import tomllib
from typing import TypeAlias
import tomli_w import tomli_w
from fastapi import Depends from fastapi import Depends
@ -19,7 +20,7 @@ class DoorSaved(BaseModel):
y2: int y2: int
type DoorsSaved = list[DoorSaved] DoorsSaved: TypeAlias = 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,6 +1,10 @@
from pydantic import BaseModel, Field from typing import TypeVar
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 = ""
@ -51,7 +55,6 @@ 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="__",
@ -62,32 +65,29 @@ class Settings(BaseSettings):
##### #####
production_mode: bool = False production_mode: bool = False
show_api_docs: bool = Field( ui_directory: str = "/usr/local/share/advent22_ui/html"
default_factory=lambda data: not data["production_mode"]
)
ui_directory: str = "/opt/advent22/ui"
##### #####
# openapi settings # openapi settings
##### #####
def __api_docs[T](self, value: T) -> T | None: def __dev_value(self, value: T) -> T | None:
if self.show_api_docs: if self.production_mode:
return value return None
return None return value
@property @property
def openapi_url(self) -> str | None: def openapi_url(self) -> str | None:
return self.__api_docs("/api/openapi.json") return self.__dev_value("/api/openapi.json")
@property @property
def docs_url(self) -> str | None: def docs_url(self) -> str | None:
return self.__api_docs("/api/docs") return self.__dev_value("/api/docs")
@property @property
def redoc_url(self) -> str | None: def redoc_url(self) -> str | None:
return self.__api_docs("/api/redoc") return self.__dev_value("/api/redoc")
##### #####
# webdav settings # webdav settings

22
api/advent22_api/main.py Normal file
View file

@ -0,0 +1,22 @@
#!/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

@ -1,49 +0,0 @@
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 Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,33 @@
[project] [tool.poetry]
name = "advent22-api"
version = "0.2.0"
description = ""
license = {file = "LICENSE"}
readme = "README.md"
requires-python = ">=3.14"
authors = [ authors = [
{name = "Jörn-Michael Miehe", email = "jmm@yavook.de"}, "Jörn-Michael Miehe <jmm@yavook.de>",
{name = "Penner42", email = "unbekannt42@web.de"}, "Penner42 <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",
] ]
description = ""
license = "MIT"
name = "advent22_api"
version = "0.1.0"
[project.scripts] [tool.poetry.dependencies]
advent22 = "advent22_api.production:start" Pillow = "^12.1.1"
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"
[dependency-groups] [tool.poetry.group.dev.dependencies]
dev = [ black = "^26.1.0"
"fastapi[standard]>=0.129.2", flake8 = "^7.3.0"
"pytest>=9.0.2", pytest = "^9.0.2"
"ruff>=0.15.2",
]
[build-system] [build-system]
requires = ["uv_build>=0.10.4,<0.11.0", "packaging"] build-backend = "poetry.core.masonry.api"
build-backend = "uv_build" requires = ["poetry-core>=1.0.0"]
[tool.uv.build-backend]
# module-name = "advent22_api"
module-root = ""

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
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

@ -0,0 +1,20 @@
#!/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}"

4
ui/.browserslistrc Normal file
View file

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

View file

@ -4,17 +4,11 @@
"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
"dockerComposeFile": "../../.devcontainer/docker_compose.yml", "image": "mcr.microsoft.com/devcontainers/javascript-node:4-24-trixie",
"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"
}, },
@ -32,12 +26,9 @@
// 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"
] ]
} }
@ -47,7 +38,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 install" "postStartCommand": "yarn dlx update-browserslist-db@latest && 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": [],

View file

@ -1,8 +0,0 @@
[*.{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

38
ui/.eslintrc.js Normal file
View file

@ -0,0 +1,38 @@
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
View file

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

15
ui/.gitignore vendored
View file

@ -1,19 +1,4 @@
# 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

View file

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

View file

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

View file

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

View file

@ -4,11 +4,10 @@
}, },
"[jsonc]": { "[jsonc]": {
"editor.formatOnSave": false "editor.formatOnSave": false,
}, },
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit" "source.organizeImports": "explicit"
}, },
@ -16,14 +15,9 @@
"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,21 +3,10 @@
"tasks": [ "tasks": [
{ {
"type": "npm", "type": "npm",
"script": "dev", "script": "serve",
"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,54 +1,29 @@
# advent22_ui # advent22_ui
This template should help get you started developing with Vue 3 in Vite. ## Project setup
## 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
``` ```
### Compile and Hot-Reload for Development ### Compiles and hot-reloads for development
```sh ```
yarn dev yarn serve
``` ```
### Type-Check, Compile and Minify for Production ### Compiles and minifies for production
```sh ```
yarn build yarn build
``` ```
### Run Unit Tests with [Vitest](https://vitest.dev/) ### Lints and fixes files
```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/).

5
ui/babel.config.json Normal file
View file

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

1
ui/env.d.ts vendored
View file

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

View file

@ -1,32 +0,0 @@
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,63 +1,47 @@
{ {
"name": "advent22_ui", "name": "advent22_ui",
"version": "0.2.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"packageManager": "yarn@4.12.0", "packageManager": "yarn@4.12.0",
"scripts": { "scripts": {
"dev": "vite --host", "serve": "vue-cli-service serve",
"build": "run-p type-check \"build-only {@}\" --", "build": "vue-cli-service build",
"preview": "vite preview --host", "test:unit": "vue-cli-service test:unit",
"test:unit": "vitest", "test:unit-watch": "vue-cli-service test:unit --watch",
"build-only": "vite build", "lint": "vue-cli-service lint",
"type-check": "vue-tsc --build", "ui": "vue ui --host 0.0.0.0 --headless"
"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",
"@tsconfig/node24": "^24.0.4", "@types/chai": "^5.2.3",
"@types/jsdom": "^27.0.0",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^25.3.0", "@types/mocha": "^10.0.10",
"@vitejs/plugin-vue": "^6.0.4", "@typescript-eslint/eslint-plugin": "^8.55.0",
"@vitest/eslint-plugin": "^1.6.9", "@typescript-eslint/parser": "^8.55.0",
"@vue/eslint-config-typescript": "^14.7.0", "@vue/cli-plugin-babel": "^5.0.9",
"@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",
"eslint": "^10.0.1", "chai": "^6.2.2",
"eslint-config-prettier": "^10.1.8", "core-js": "^3.48.0",
"eslint-plugin-oxlint": "~1.49.0", "eslint": "^8.57.1",
"eslint-plugin-vue": "~10.8.0", "eslint-plugin-vue": "^9.33.0",
"jiti": "^2.6.1",
"jsdom": "^28.1.0",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"npm-run-all2": "^8.0.4", "pinia": "^3.0.4",
"oxlint": "~1.49.0", "sass": "~1.94.3",
"prettier": "3.8.1", "sass-loader": "^16.0.0",
"sass-embedded": "^1.97.3", "typescript": "^5.9.3",
"typescript": "~5.9.3", "vue": "^3.5.25",
"vite": "^7.3.1", "vue-cli-plugin-webpack-bundle-analyzer": "^4.0.0"
"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

@ -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="/favicon.ico" /> <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<!-- Matomo --> <!-- Matomo -->
<script> <script>
let _paq = (window._paq = window._paq || []); let _paq = (window._paq = window._paq || []);
@ -29,11 +29,12 @@
<body> <body>
<noscript> <noscript>
<strong <strong
>Es tut uns leid, aber <%= title %> funktioniert nicht richtig ohne JavaScript. Bitte >Es tut uns leid, aber <%= htmlWebpackPlugin.options.title %>
aktivieren Sie es, um fortzufahren.</strong funktioniert nicht richtig ohne JavaScript. Bitte aktivieren Sie es, um
fortzufahren.</strong
> >
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View file

@ -7,8 +7,15 @@
</section> </section>
<section class="section px-3"> <section class="section px-3">
<progress v-if="store.background_image === 'loading'" class="progress is-primary" max="100" /> <progress
<div v-else-if="store.background_image === 'error'" class="notification is-danger"> v-if="store.background_image === 'loading'"
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

@ -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 in Deinem Du hast noch keine Türchen geöffnet, vielleicht gibt es ein Anzeigeproblem
Webbrowser? in Deinem Webbrowser?
</p> </p>
<div class="level"> <div class="level">
<div class="level-item"> <div class="level-item">

View file

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

View file

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

View file

@ -7,9 +7,12 @@
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"> Zeit bis zum ersten Türchen: </template> <template v-if="store.user_doors.length === 0">
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 Türchen: {{ store.user_doors.length }} Türchen offen. Zeit bis zum nächsten
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="[day, data] in day_data" :key="`part-${day}`"> <template v-for="(data, day) 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="[day, data] in day_data" v-for="(data, day) 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="[day, data] in day_data" v-for="(data, day) 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,19 +48,14 @@
<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 } from "@/lib/model"; import type { ImageData, NumStrDict } 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";
interface DayData { const day_data = ref<Record<number, { part: string; image_name: string }>>({});
part: string;
image_name: string;
}
const day_data = ref<Map<number, DayData>>(new Map());
let modal: HMultiModal | undefined; let modal: HMultiModal | undefined;
@ -70,31 +65,24 @@ 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<Record<number, string>>("admin/day_parts"), API.request<NumStrDict>("admin/day_parts"),
API.request<Record<number, string>>("admin/day_image_names"), API.request<NumStrDict>("admin/day_image_names"),
]); ]);
const _get_data = (day: number) => { const _ensure_day_in_data = (day: number) => {
let result = day_data.value.get(day); if (!(day in day_data.value)) {
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) => {
_get_data(day).part = part; _ensure_day_in_data(day);
day_data.value[day].part = part;
}); });
objForEach(day_image_names, (day, image_name) => { objForEach(day_image_names, (day, image_name) => {
_get_data(day).image_name = image_name; _ensure_day_in_data(day);
day_data.value[day].image_name = image_name;
}); });
} }

View file

@ -9,11 +9,15 @@
<dt>Wert</dt> <dt>Wert</dt>
<dd> <dd>
Eingabe: Eingabe:
<span class="is-family-monospace"> "{{ admin_config_model.solution.value }}" </span> <span class="is-family-monospace">
"{{ admin_config_model.solution.value }}"
</span>
</dd> </dd>
<dd> <dd>
Ausgabe: Ausgabe:
<span class="is-family-monospace"> "{{ admin_config_model.solution.clean }}" </span> <span class="is-family-monospace">
"{{ admin_config_model.solution.clean }}"
</span>
</dd> </dd>
<dt>Transformation</dt> <dt>Transformation</dt>
@ -43,7 +47,9 @@
<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">Kein nächstes Türchen</dd> <dd v-if="store.next_door_target === null">
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>
@ -59,7 +65,9 @@
<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">"{{ admin_config_model.puzzle.seed }}"</dd> <dd class="is-family-monospace">
"{{ admin_config_model.puzzle.seed }}"
</dd>
<dt>Extra-Tage</dt> <dt>Extra-Tage</dt>
<dd> <dd>
@ -113,7 +121,10 @@
<dd>{{ admin_config_model.image.border }} px</dd> <dd>{{ admin_config_model.image.border }} px</dd>
<dt>Schriftarten</dt> <dt>Schriftarten</dt>
<dd v-for="(font, index) in admin_config_model.fonts" :key="`font-${index}`"> <dd
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>
@ -232,7 +243,7 @@ const admin_config_model = ref<AdminConfigModel>({
}); });
const doors = ref<DoorSaved[]>([]); const doors = ref<DoorSaved[]>([]);
const creds = ref({ const creds = ref<Record<string, Credentials>>({
dav: { dav: {
username: "", username: "",
password: "", password: "",
@ -265,7 +276,10 @@ async function on_open(): Promise<void> {
clear_credentials(creds.value.ui); clear_credentials(creds.value.ui);
} }
async function load_credentials(creds: Credentials, endpoint: string): Promise<void> { async function load_credentials(
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,7 +8,11 @@
:icon="['fas', 'fa-backward']" :icon="['fas', 'fa-backward']"
/> />
<BulmaBreadcrumbs :steps="steps" v-model="current_step" class="level-item mb-0" /> <BulmaBreadcrumbs
:steps="steps"
v-model="current_step"
class="level-item mb-0"
/>
<BulmaButton <BulmaButton
:disabled="current_step === 2" :disabled="current_step === 2"

View file

@ -3,7 +3,11 @@
<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 v-if="icon !== undefined" :icon="icon" :beat-fade="busy" /> <FontAwesomeIcon
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,13 +6,18 @@
<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 :icon="['fas', 'arrows-rotate']" :spin="state === 'loading'" /> <FontAwesomeIcon
: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 :icon="['fas', is_open ? 'angle-down' : 'angle-right']" /> <FontAwesomeIcon
:icon="['fas', is_open ? 'angle-down' : 'angle-right']"
/>
</span> </span>
</button> </button>
</header> </header>

View file

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

View file

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

View file

@ -32,7 +32,11 @@ 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 === "mousemove" || t === "mouseup" || t === "click" || t === "dblclick"; t === "mousedown" ||
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,7 +14,12 @@
:door="door" :door="door"
force_visible force_visible
/> />
<SVGRect v-if="preview_visible" variant="success" :rectangle="preview" visible /> <SVGRect
v-if="preview_visible"
variant="success"
:rectangle="preview"
visible
/>
</ThouCanvas> </ThouCanvas>
</template> </template>

View file

@ -14,10 +14,9 @@
<img :src="unwrap_loading(store.background_image).data_url" /> <img :src="unwrap_loading(store.background_image).data_url" />
<ThouCanvas> <ThouCanvas>
<PreviewDoor <PreviewDoor
v-for="(door, index) in model" v-for="(_, index) in model"
:key="`door-${index}`" :key="`door-${index}`"
:model-value="door" v-model="model[index]"
@update:model-value="updateAt(index, door)"
/> />
</ThouCanvas> </ThouCanvas>
</figure> </figure>
@ -25,7 +24,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { unwrap_loading, type VueLike } from "@/lib/helpers"; import { type VueLike, unwrap_loading } 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";
@ -33,12 +32,5 @@ 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>

6
ui/src/d.ts/shims-vue.d.ts vendored Normal file
View file

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

View file

@ -16,9 +16,23 @@ 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: 15e3, timeout: 10e3,
baseURL: "/api", baseURL: this.api_baseurl,
}); });
private static readonly creds_key = "advent22/credentials"; private static readonly creds_key = "advent22/credentials";
@ -70,6 +84,7 @@ 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,7 +1,10 @@
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>(obj: T, f: (k: keyof T, v: T[keyof T]) => void): void { export function objForEach<T>(
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]);
@ -39,6 +42,7 @@ 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,6 +45,10 @@ 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,7 +24,10 @@ export class Door {
// integer coercion // integer coercion
let day = Number(value); let day = Number(value);
day = !Number.isNaN(day) && Number.isFinite(day) ? Math.trunc(day) : Door.MIN_DAY; 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,6 +71,9 @@ export class Rectangle {
} }
public move(vector: Vector2D): Rectangle { public move(vector: Vector2D): Rectangle {
return new Rectangle(this.corner_1.plus(vector), this.corner_2.plus(vector)); return new Rectangle(
this.corner_1.plus(vector),
this.corner_2.plus(vector),
);
} }
} }

View file

@ -1,4 +1,4 @@
import { defineStore } from "pinia"; import { acceptHMRUpdate, 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,28 +55,31 @@ 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.createElement("link"); document.querySelector("link[rel*='icon']") ??
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;
if (link.parentElement === null) if (link.parentElement === null)
document.getElementsByTagName("head")[0]!.appendChild(link); document.getElementsByTagName("head")[0]!.appendChild(link);
} catch {} } catch { }
try { try {
const [is_admin, site_config, background_image, user_doors, next_door] = await Promise.all([ const [is_admin, site_config, background_image, user_doors, next_door] =
this.update_is_admin(), await Promise.all([
API.request<SiteConfigModel>("user/site_config"), this.update_is_admin(),
API.request<ImageData>("user/background_image"), API.request<SiteConfigModel>("user/site_config"),
API.request<DoorSaved[]>("user/doors"), API.request<ImageData>("user/background_image"),
API.request<number | null>("user/next_door"), API.request<DoorSaved[]>("user/doors"),
]); API.request<number | null>("user/next_door"),
]);
void is_admin; // discard value void is_admin; // discard value
document.title = site_config.title; document.title = site_config.title;
if (site_config.subtitle !== "") document.title += " " + site_config.subtitle; if (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;
@ -120,3 +123,9 @@ export const advent22Store = defineStore("advent22", {
}, },
}, },
}); });
if (import.meta.webpackHot) {
import.meta.webpackHot.accept(
acceptHMRUpdate(advent22Store, import.meta.webpackHot),
);
}

View file

@ -1,12 +0,0 @@
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 };
});

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest"; import { expect } from "chai";
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,60 +17,56 @@ describe("Rectangle Tests", () => {
width: number, width: number,
height: number, height: number,
): void { ): void {
expect(r.left).toEqual(left); expect(r.left).to.equal(left);
expect(r.top).toEqual(top); expect(r.top).to.equal(top);
expect(r.width).toEqual(width); expect(r.width).to.equal(width);
expect(r.height).toEqual(height); expect(r.height).to.equal(height);
expect(r.area).toEqual(width * height); expect(r.area).to.equal(width * height);
expect(r.middle.x).toEqual(left + 0.5 * width); expect(r.middle.x).to.equal(left + 0.5 * width);
expect(r.middle.y).toEqual(top + 0.5 * height); expect(r.middle.y).to.equal(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)).toBe(true); expect(r1.equals(r2)).to.be.true;
expect(r1.equals(new Rectangle())).toBe(false); expect(r1.equals(new Rectangle())).to.be.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))).toBe(true); expect(r1.equals(new Rectangle(v1t, v2t))).to.be.true;
}); });
it("should contain itself", () => { it("should contain itself", () => {
expect(r1.contains(v1)).toBe(true); expect(r1.contains(v1)).to.be.true;
expect(r1.contains(v2)).toBe(true); expect(r1.contains(v2)).to.be.true;
expect(r1.contains(r1.origin)).toBe(true); expect(r1.contains(r1.origin)).to.be.true;
expect(r1.contains(r1.corner)).toBe(true); expect(r1.contains(r1.corner)).to.be.true;
expect(r1.contains(r1.middle)).toBe(true); expect(r1.contains(r1.middle)).to.be.true;
}); });
it("should not contain certain points", () => { it("should not contain certain points", () => {
expect(r1.contains(new Vector2D(0, 0))).toBe(false); expect(r1.contains(new Vector2D(0, 0))).to.be.false;
expect(r1.contains(new Vector2D(100, 100))).toBe(false); expect(r1.contains(new Vector2D(100, 100))).to.be.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);
@ -87,7 +83,6 @@ 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 { describe, expect, it } from "vitest"; import { expect } from "chai";
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).toEqual(0); expect(v0.x).to.equal(0);
expect(v0.y).toEqual(0); expect(v0.y).to.equal(0);
}); });
it("should create a vector", () => { it("should create a vector", () => {
expect(v.x).toEqual(1); expect(v.x).to.equal(1);
expect(v.y).toEqual(2); expect(v.y).to.equal(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).toEqual(4); expect(v2.x).to.equal(4);
expect(v2.y).toEqual(6); expect(v2.y).to.equal(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).toEqual(-2); expect(v2.x).to.equal(-2);
expect(v2.y).toEqual(-2); expect(v2.y).to.equal(-2);
}); });
it("should scale vectors", () => { it("should scale vectors", () => {
const v2 = v.scale(3); const v2 = v.scale(3);
expect(v2.x).toEqual(3); expect(v2.x).to.equal(3);
expect(v2.y).toEqual(6); expect(v2.y).to.equal(6);
}); });
it("should compare vectors", () => { it("should compare vectors", () => {
expect(v.equals(v.scale(1))).toBe(true); expect(v.equals(v.scale(1))).to.be.true;
expect(v.equals(v.scale(2))).toBe(false); expect(v.equals(v.scale(2))).to.be.false;
}); });
}); });

View file

@ -1,14 +0,0 @@
{
"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,14 +1,33 @@
{ {
"files": [], "extends": "@vue/tsconfig/tsconfig.dom.json",
"references": [ "compilerOptions": {
{ "experimentalDecorators": true,
"path": "./tsconfig.node.json" "lib": [
"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",
{ "src/**/*.ts",
"path": "./tsconfig.vitest.json" // "src/**/*.tsx",
} "tests/**/*.ts",
] // "tests/**/*.tsx",
} ],
}

View file

@ -1,19 +0,0 @@
{
"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"]
}
}

View file

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

View file

@ -1,45 +0,0 @@
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,
},
},
},
});

View file

@ -1,14 +0,0 @@
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)),
},
}),
);

26
ui/vue.config.js Normal file
View file

@ -0,0 +1,26 @@
/* 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