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
# found in python and JS dirs
**/__pycache__
**/node_modules
**/.pytest_cache
**/__pycache__/
**/node_modules/
**/.pytest_cache/
**/.ruff_cache/
**/.uv_cache/
**/.venv/
# env files
**/.env
@ -25,3 +28,6 @@
**/yarn-debug.log*
**/yarn-error.log*
**/pnpm-debug.log*
# custom files
api/api.conf

View file

@ -1,51 +1,6 @@
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 #
@ -54,76 +9,90 @@ RUN poetry build --format wheel --output ./dist
ARG NODE_VERSION
FROM node:${NODE_VERSION} AS build-ui
# env setup
# install ui dependencies
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 ui/package*.json ui/yarn*.lock ./
RUN set -ex; \
corepack enable; \
yarn install;
# copy and build advent22_ui
# copy and build ui
COPY ui ./
RUN set -ex; \
yarn dlx update-browserslist-db@latest; \
yarn build --dest /tmp/advent22_ui/html; \
# exclude webpack-bundle-analyzer output
rm -f /tmp/advent22_ui/html/report.html;
RUN --mount=type=cache,id=ui,target=/root/.yarn \
set -ex; \
\
yarn build --outDir /opt/advent22/ui; \
# exclude vite-bundle-analyzer output
rm /opt/advent22/ui/stats.html;
######################
# python preparation #
######################
###############
# install app #
###############
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION} AS uvicorn-gunicorn
# 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
FROM dhi.io/python:${PYTHON_VERSION}-dev AS install-app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# env setup
ENV \
PRODUCTION_MODE="true" \
PORT="8000" \
MODULE_NAME="advent22_api.app"
EXPOSE 8000
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 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; \
RUN --mount=type=bind,source=api/uv.lock,target=api/uv.lock \
--mount=type=bind,source=api/pyproject.toml,target=api/pyproject.toml \
--mount=type=bind,source=api/.python-version,target=api/.python-version \
--mount=type=cache,id=api,target=/root/.cache/uv \
set -ex; \
\
# 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
COPY --from=build-ui /tmp/advent22_ui /usr/local/share/advent22_ui
# install api
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
USER nobody

View file

@ -4,12 +4,18 @@
"name": "Advent22 API",
// 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": {
"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": {
"packages": "git-flow"
},
@ -17,7 +23,8 @@
},
"containerEnv": {
"TZ": "Europe/Berlin"
"TZ": "Europe/Berlin",
"UV_CACHE_DIR": "/workspaces/advent22/.uv_cache"
},
// Configure tool-specific properties.
@ -31,22 +38,21 @@
},
// 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.black-formatter",
"ms-python.flake8",
"ms-python.isort",
"ms-python.vscode-pylance"
]
}
},
// 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.
"postStartCommand": "poetry install"
"postStartCommand": "uv tool upgrade uv-upx && uv sync"
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "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",
"configurations": [
{
"name": "Main Module",
"type": "python",
"name": "FastAPI CLI (dev)",
"type": "debugpy",
"request": "launch",
"module": "advent22_api.main",
"pythonArgs": [
"-Xfrozen_modules=off",
"module": "fastapi",
"args": [
"dev",
"--host",
"0.0.0.0",
"--port",
"8000",
"--entrypoint",
"advent22_api.app:app",
"--reload-dir",
"${workspaceFolder}/advent22_api"
],
"env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"WEBDAV__CACHE_TTL": "30",
"ADVENT22__WEBDAV__CACHE_TTL": "30"
},
"justMyCode": true,
"justMyCode": true
}
]
}

View file

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

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.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from .core.settings import SETTINGS
@ -33,14 +32,3 @@ if SETTINGS.production_mode:
),
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 logging
from dataclasses import dataclass
from typing import AnyStr, Self, TypeAlias
from typing import Self, cast
import numpy as np
from PIL import Image as PILImage
@ -11,9 +11,9 @@ from PIL.ImageFont import FreeTypeFont
from .config import Config
_RGB: TypeAlias = tuple[int, int, int]
_XY: TypeAlias = tuple[float, float]
_Box: TypeAlias = tuple[int, int, int, int]
type _RGB = tuple[int, int, int]
type _XY = tuple[float, float]
type _Box = tuple[int, int, int, int]
_logger = logging.getLogger(__name__)
@ -56,7 +56,7 @@ class AdventImage:
async def get_text_box(
self,
xy: _XY,
text: AnyStr,
text: str,
font: FreeTypeFont,
anchor: str | None = "mm",
**text_kwargs,
@ -95,12 +95,12 @@ class AdventImage:
pixel_data = np.asarray(self.img.crop(box))
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(
self,
xy: _XY,
text: AnyStr,
text: str,
font: FreeTypeFont,
anchor: str | None = "mm",
**text_kwargs,
@ -134,8 +134,10 @@ class AdventImage:
else:
tc_v -= 3
text_color = colorsys.hsv_to_rgb(tc_h, tc_s, tc_v)
text_color = _RGB(int(val) for val in text_color)
text_color: tuple[int | float, int | float, int | float] = colorsys.hsv_to_rgb(
tc_h, tc_s, tc_v
)
text_color = cast(_RGB, tuple(int(val) for val in text_color))
# Buchstaben verstecken
ImageDraw.Draw(self.img).text(

View file

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

View file

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

View file

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

View file

@ -1,10 +1,6 @@
from typing import TypeVar
from pydantic import BaseModel
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
T = TypeVar("T")
class Credentials(BaseModel):
username: str = ""
@ -55,6 +51,7 @@ class Settings(BaseSettings):
"""
model_config = SettingsConfigDict(
env_prefix="ADVENT22__",
env_file="api.conf",
env_file_encoding="utf-8",
env_nested_delimiter="__",
@ -65,29 +62,32 @@ class Settings(BaseSettings):
#####
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
#####
def __dev_value(self, value: T) -> T | None:
if self.production_mode:
return None
def __api_docs[T](self, value: T) -> T | None:
if self.show_api_docs:
return value
return None
@property
def openapi_url(self) -> str | None:
return self.__dev_value("/api/openapi.json")
return self.__api_docs("/api/openapi.json")
@property
def docs_url(self) -> str | None:
return self.__dev_value("/api/docs")
return self.__api_docs("/api/docs")
@property
def redoc_url(self) -> str | None:
return self.__dev_value("/api/redoc")
return self.__api_docs("/api/redoc")
#####
# 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]
authors = [
"Jörn-Michael Miehe <jmm@yavook.de>",
"Penner42 <unbekannt42@web.de>",
]
[project]
name = "advent22-api"
version = "0.2.0"
description = ""
license = "MIT"
name = "advent22_api"
version = "0.1.0"
license = {file = "LICENSE"}
readme = "README.md"
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]
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"
[project.scripts]
advent22 = "advent22_api.production:start"
[tool.poetry.group.dev.dependencies]
black = "^26.1.0"
flake8 = "^7.3.0"
pytest = "^9.0.2"
[dependency-groups]
dev = [
"fastapi[standard]>=0.129.2",
"pytest>=9.0.2",
"ruff>=0.15.2",
]
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core>=1.0.0"]
requires = ["uv_build>=0.10.4,<0.11.0", "packaging"]
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",
// 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": {
"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": {
"packages": "git-flow"
},
@ -26,9 +32,12 @@
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"mhutchie.git-graph",
"oxc.oxc-vscode",
"Syler.sass-indented",
"vitest.explorer",
"Vue.volar"
]
}
@ -38,7 +47,7 @@
// "postCreateCommand": "yarn install",
// 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.
// "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
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

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]": {
"editor.formatOnSave": false,
"editor.formatOnSave": false
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
@ -15,9 +16,14 @@
"editor.formatOnSave": true,
"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.format.convert": false,
"sass.format.deleteWhitespace": true,
"prettier.trailingComma": "all",
"sass.format.deleteWhitespace": true
}

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

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

View file

@ -1,29 +1,54 @@
# advent22_ui
## Project setup
This template should help get you started developing with Vue 3 in Vite.
```
yarn install
## Recommended IDE Setup
[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
```
yarn serve
```sh
yarn dev
```
### Compiles and minifies for production
### Type-Check, Compile and Minify for Production
```
```sh
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
```
### 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="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="UTF-8" />
<!-- <meta http-equiv="X-UA-Compatible" content="IE=edge" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title><%= title %></title>
<!-- Matomo -->
<script>
let _paq = (window._paq = window._paq || []);
@ -29,12 +29,11 @@
<body>
<noscript>
<strong
>Es tut uns leid, aber <%= htmlWebpackPlugin.options.title %>
funktioniert nicht richtig ohne JavaScript. Bitte aktivieren Sie es, um
fortzufahren.</strong
>Es tut uns leid, aber <%= title %> funktioniert nicht richtig ohne JavaScript. Bitte
aktivieren Sie es, um fortzufahren.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -1,47 +1,63 @@
{
"name": "advent22_ui",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:unit-watch": "vue-cli-service test:unit --watch",
"lint": "vue-cli-service lint",
"ui": "vue ui --host 0.0.0.0 --headless"
"dev": "vite --host",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview --host",
"test:unit": "vitest",
"build-only": "vite build",
"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": {
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@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/mocha": "^10.0.10",
"@typescript-eslint/eslint-plugin": "^8.55.0",
"@typescript-eslint/parser": "^8.55.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",
"@types/node": "^25.3.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vitest/eslint-plugin": "^1.6.9",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
"animate.css": "^4.1.1",
"axios": "^1.13.5",
"bulma": "^1.0.4",
"bulma-toast": "2.4.3",
"chai": "^6.2.2",
"core-js": "^3.48.0",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.33.0",
"eslint": "^10.0.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.49.0",
"eslint-plugin-vue": "~10.8.0",
"jiti": "^2.6.1",
"jsdom": "^28.1.0",
"luxon": "^3.7.2",
"pinia": "^3.0.4",
"sass": "~1.94.3",
"sass-loader": "^16.0.0",
"typescript": "^5.9.3",
"vue": "^3.5.25",
"vue-cli-plugin-webpack-bundle-analyzer": "^4.0.0"
"npm-run-all2": "^8.0.4",
"oxlint": "~1.49.0",
"prettier": "3.8.1",
"sass-embedded": "^1.97.3",
"typescript": "~5.9.3",
"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 class="section px-3">
<progress
v-if="store.background_image === 'loading'"
class="progress is-primary"
max="100"
/>
<div
v-else-if="store.background_image === 'error'"
class="notification is-danger"
>
<progress 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
</div>
<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 { Vector2D } from "@/lib/rects/vector2d";
@ -17,56 +17,60 @@ describe("Rectangle Tests", () => {
width: number,
height: number,
): void {
expect(r.left).to.equal(left);
expect(r.top).to.equal(top);
expect(r.left).toEqual(left);
expect(r.top).toEqual(top);
expect(r.width).to.equal(width);
expect(r.height).to.equal(height);
expect(r.area).to.equal(width * height);
expect(r.width).toEqual(width);
expect(r.height).toEqual(height);
expect(r.area).toEqual(width * height);
expect(r.middle.x).to.equal(left + 0.5 * width);
expect(r.middle.y).to.equal(top + 0.5 * height);
expect(r.middle.x).toEqual(left + 0.5 * width);
expect(r.middle.y).toEqual(top + 0.5 * height);
}
it("should create a default rectangle", () => {
expect.hasAssertions();
check_rectangle(new Rectangle(), 0, 0, 0, 0);
});
it("should create a rectangle", () => {
expect.hasAssertions();
check_rectangle(r1, 1, 2, 3, 4);
});
it("should create the same rectangle backwards", () => {
expect.hasAssertions();
check_rectangle(r2, 1, 2, 3, 4);
});
it("should compare rectangles", () => {
expect(r1.equals(r2)).to.be.true;
expect(r1.equals(new Rectangle())).to.be.false;
expect(r1.equals(r2)).toBe(true);
expect(r1.equals(new Rectangle())).toBe(false);
});
it("should create the same rectangle transposed", () => {
const v1t = new Vector2D(v1.x, v2.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", () => {
expect(r1.contains(v1)).to.be.true;
expect(r1.contains(v2)).to.be.true;
expect(r1.contains(v1)).toBe(true);
expect(r1.contains(v2)).toBe(true);
expect(r1.contains(r1.origin)).to.be.true;
expect(r1.contains(r1.corner)).to.be.true;
expect(r1.contains(r1.middle)).to.be.true;
expect(r1.contains(r1.origin)).toBe(true);
expect(r1.contains(r1.corner)).toBe(true);
expect(r1.contains(r1.middle)).toBe(true);
});
it("should not contain certain points", () => {
expect(r1.contains(new Vector2D(0, 0))).to.be.false;
expect(r1.contains(new Vector2D(100, 100))).to.be.false;
expect(r1.contains(new Vector2D(0, 0))).toBe(false);
expect(r1.contains(new Vector2D(100, 100))).toBe(false);
});
it("should update a rectangle", () => {
expect.hasAssertions();
const v = new Vector2D(1, 1);
check_rectangle(r1.update(v1.plus(v), undefined), 2, 3, 2, 3);
@ -83,6 +87,7 @@ describe("Rectangle Tests", () => {
});
it("should move a rectangle", () => {
expect.hasAssertions();
const v = new Vector2D(1, 1);
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";
@ -7,35 +7,35 @@ describe("Vector2D Tests", () => {
it("should create a default vector", () => {
const v0 = new Vector2D();
expect(v0.x).to.equal(0);
expect(v0.y).to.equal(0);
expect(v0.x).toEqual(0);
expect(v0.y).toEqual(0);
});
it("should create a vector", () => {
expect(v.x).to.equal(1);
expect(v.y).to.equal(2);
expect(v.x).toEqual(1);
expect(v.y).toEqual(2);
});
it("should add vectors", () => {
const v2 = v.plus(new Vector2D(3, 4));
expect(v2.x).to.equal(4);
expect(v2.y).to.equal(6);
expect(v2.x).toEqual(4);
expect(v2.y).toEqual(6);
});
it("should subtract vectors", () => {
const v2 = v.minus(new Vector2D(3, 4));
expect(v2.x).to.equal(-2);
expect(v2.y).to.equal(-2);
expect(v2.x).toEqual(-2);
expect(v2.y).toEqual(-2);
});
it("should scale vectors", () => {
const v2 = v.scale(3);
expect(v2.x).to.equal(3);
expect(v2.y).to.equal(6);
expect(v2.x).toEqual(3);
expect(v2.y).toEqual(6);
});
it("should compare vectors", () => {
expect(v.equals(v.scale(1))).to.be.true;
expect(v.equals(v.scale(2))).to.be.false;
expect(v.equals(v.scale(1))).toBe(true);
expect(v.equals(v.scale(2))).toBe(false);
});
});

View file

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

View file

@ -12,12 +12,7 @@
<div class="field">
<label class="label">Username</label>
<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>

View file

@ -18,10 +18,7 @@
</template>
</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>
</template>

View file

@ -7,12 +7,9 @@
Alle {{ store.user_doors.length }} Türchen offen!
</template>
<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>
{{ 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>
<CountDown :until="store.next_door_target" />
</template>

View file

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

View file

@ -9,15 +9,11 @@
<dt>Wert</dt>
<dd>
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>
Ausgabe:
<span class="is-family-monospace">
"{{ admin_config_model.solution.clean }}"
</span>
<span class="is-family-monospace"> "{{ admin_config_model.solution.clean }}" </span>
</dd>
<dt>Transformation</dt>
@ -47,9 +43,7 @@
<dd>{{ store.user_doors.length }}</dd>
<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>
<dt>Erstes Türchen</dt>
@ -65,9 +59,7 @@
<dd>{{ fmt_puzzle_date("end") }}</dd>
<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>
<dd>
@ -121,10 +113,7 @@
<dd>{{ admin_config_model.image.border }} px</dd>
<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)
</dd>
</dl>
@ -243,7 +232,7 @@ const admin_config_model = ref<AdminConfigModel>({
});
const doors = ref<DoorSaved[]>([]);
const creds = ref<Record<string, Credentials>>({
const creds = ref({
dav: {
username: "",
password: "",
@ -276,10 +265,7 @@ async function on_open(): Promise<void> {
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 {
const new_creds = await API.request<Credentials>(endpoint);

View file

@ -8,11 +8,7 @@
: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
:disabled="current_step === 2"

View file

@ -3,11 +3,7 @@
<button class="button">
<slot name="default">
<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>
</slot>
<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">
<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>
</p>
<button class="card-header-icon" @click="toggle">
<span class="icon">
<FontAwesomeIcon
:icon="['fas', is_open ? 'angle-down' : 'angle-right']"
/>
<FontAwesomeIcon :icon="['fas', is_open ? 'angle-down' : 'angle-right']" />
</span>
</button>
</header>

View file

@ -6,7 +6,11 @@
>
<div
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 }}
</div>

View file

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

View file

@ -32,11 +32,7 @@ function get_event_thous(event: MouseEvent): Vector2D {
type TCEventType = "mousedown" | "mousemove" | "mouseup" | "click" | "dblclick";
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<{
(event: TCEventType, e: MouseEvent, point: Vector2D): void;

View file

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

View file

@ -14,9 +14,10 @@
<img :src="unwrap_loading(store.background_image).data_url" />
<ThouCanvas>
<PreviewDoor
v-for="(_, index) in model"
v-for="(door, index) in model"
:key="`door-${index}`"
v-model="model[index]"
:model-value="door"
@update:model-value="updateAt(index, door)"
/>
</ThouCanvas>
</figure>
@ -24,7 +25,7 @@
</template>
<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 { advent22Store } from "@/lib/store";
@ -32,5 +33,12 @@ import ThouCanvas from "../calendar/ThouCanvas.vue";
import PreviewDoor from "./PreviewDoor.vue";
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();
</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 {
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({
timeout: 10e3,
baseURL: this.api_baseurl,
timeout: 15e3,
baseURL: "/api",
});
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));
return response.data;
} catch (reason) {
// eslint-disable-next-line no-console
console.error(`Failed to query ${p.endpoint}: ${reason}`);
throw new APIError(reason, p.endpoint);
}

View file

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

View file

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

View file

@ -24,10 +24,7 @@ export class Door {
// integer coercion
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);
}

View file

@ -71,9 +71,6 @@ export class 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 { acceptHMRUpdate, defineStore } from "pinia";
import { defineStore } from "pinia";
import { API } from "./api";
import type { Loading } from "./helpers";
import type { Credentials, DoorSaved, ImageData, SiteConfigModel } from "./model";
@ -55,19 +55,17 @@ export const advent22Store = defineStore("advent22", {
const favicon = await API.request<ImageData>("user/favicon");
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") ??
document.createElement("link");
document.querySelector("link[rel*='icon']") ?? document.createElement("link");
link.rel = "shortcut icon";
link.type = "image/x-icon";
link.href = favicon.data_url;
if (link.parentElement === null)
document.getElementsByTagName("head")[0]!.appendChild(link);
} catch { }
} catch {}
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] = await Promise.all([
this.update_is_admin(),
API.request<SiteConfigModel>("user/site_config"),
API.request<ImageData>("user/background_image"),
@ -78,8 +76,7 @@ export const advent22Store = defineStore("advent22", {
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.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",
"compilerOptions": {
"experimentalDecorators": true,
"lib": [
"es2020",
"dom",
"dom.iterable",
"es2022.object",
"es2023.array",
],
// "moduleResolution": "node",
// "sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"mocha",
"chai",
],
"paths": {
"@/*": [
"src/*",
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
},
},
"include": [
"src/**/*.vue",
"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