diff --git a/api/.flake8 b/api/.flake8 index e4e4892..223d36c 100644 --- a/api/.flake8 +++ b/api/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length = 80 -select = C,E,F,W,B,B950 -extend-ignore = E203, E501 +extend-select = B950 +extend-ignore = E203,E501 diff --git a/api/.isort.cfg b/api/.isort.cfg new file mode 100644 index 0000000..d056ad0 --- /dev/null +++ b/api/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile = black +line_length = 80 diff --git a/api/advent22_api/core/advent_image.py b/api/advent22_api/core/advent_image.py index 8bc5fc4..56c1b49 100644 --- a/api/advent22_api/core/advent_image.py +++ b/api/advent22_api/core/advent_image.py @@ -1,22 +1,29 @@ import colorsys +import logging from dataclasses import dataclass from typing import Self, TypeAlias, cast import numpy as np -from PIL import Image, ImageDraw, ImageFont +from PIL import Image as PILImage +from PIL import ImageDraw +from PIL.Image import Image, Resampling +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] + +_logger = logging.getLogger(__name__) @dataclass(slots=True, frozen=True) class AdventImage: - img: Image.Image + img: Image @classmethod - async def from_img(cls, img: Image.Image, cfg: Config) -> Self: + async def from_img(cls, img: Image, cfg: Config) -> Self: """ Einen quadratischen Ausschnitt aus der Mitte des Bilds nehmen """ @@ -42,7 +49,7 @@ class AdventImage: return cls( img.resize( size=(cfg.image.size, cfg.image.size), - resample=Image.LANCZOS, + resample=Resampling.LANCZOS, ) ) @@ -50,10 +57,10 @@ class AdventImage: self, xy: _XY, text: str | bytes, - font: "ImageFont._Font", + font: FreeTypeFont, anchor: str | None = "mm", **text_kwargs, - ) -> "Image._Box | None": + ) -> _Box | None: """ Koordinaten (links, oben, rechts, unten) des betroffenen Rechtecks bestimmen, wenn das Bild mit einem Text @@ -61,7 +68,7 @@ class AdventImage: """ # Neues 1-Bit Bild, gleiche Größe - mask = Image.new(mode="1", size=self.img.size, color=0) + mask = PILImage.new(mode="1", size=self.img.size) # Text auf Maske auftragen ImageDraw.Draw(mask).text( @@ -78,15 +85,15 @@ class AdventImage: async def get_average_color( self, - box: "Image._Box", - ) -> tuple[int, int, int]: + box: _Box, + ) -> _RGB: """ Durchschnittsfarbe eines rechteckigen Ausschnitts in einem Bild berechnen """ - pixel_data = self.img.crop(box).getdata() - mean_color: np.ndarray = np.mean(pixel_data, axis=0) + pixel_data = np.asarray(self.img.crop(box)) + mean_color: np.ndarray = np.mean(pixel_data, axis=(0, 1)) return cast(_RGB, tuple(mean_color.astype(int))) @@ -94,7 +101,7 @@ class AdventImage: self, xy: _XY, text: str | bytes, - font: "ImageFont._Font", + font: FreeTypeFont, anchor: str | None = "mm", **text_kwargs, ) -> None: @@ -108,31 +115,34 @@ class AdventImage: xy=xy, text=text, font=font, anchor=anchor, **text_kwargs ) - if text_box is not None: - # Durchschnittsfarbe bestimmen - text_color = await self.get_average_color( - box=text_box, - ) + if text_box is None: + _logger.warning("Konnte Bildbereich nicht finden!") + return - # etwas heller/dunkler machen - tc_h, tc_s, tc_v = colorsys.rgb_to_hsv(*text_color) - tc_v = int((tc_v - 127) * 0.97) + 127 + # Durchschnittsfarbe bestimmen + text_color = await self.get_average_color( + box=text_box, + ) - if tc_v < 127: - tc_v += 3 + # etwas heller/dunkler machen + tc_h, tc_s, tc_v = colorsys.rgb_to_hsv(*text_color) + tc_v = int((tc_v - 127) * 0.97) + 127 - else: - tc_v -= 3 + if tc_v < 127: + tc_v += 3 - text_color = colorsys.hsv_to_rgb(tc_h, tc_s, tc_v) - text_color = tuple(int(val) for val in text_color) + else: + tc_v -= 3 - # Buchstaben verstecken - ImageDraw.Draw(self.img).text( - xy=xy, - text=text, - font=font, - fill=cast(_RGB, text_color), - anchor=anchor, - **text_kwargs, - ) + text_color = colorsys.hsv_to_rgb(tc_h, tc_s, tc_v) + text_color = tuple(int(val) for val in text_color) + + # Buchstaben verstecken + ImageDraw.Draw(self.img).text( + xy=xy, + text=text, + font=font, + fill=cast(_RGB, text_color), + anchor=anchor, + **text_kwargs, + ) diff --git a/api/advent22_api/core/config.py b/api/advent22_api/core/config.py index 8811661..03c1730 100644 --- a/api/advent22_api/core/config.py +++ b/api/advent22_api/core/config.py @@ -4,15 +4,10 @@ from markdown import markdown from pydantic import BaseModel, ConfigDict, field_validator from .dav.webdav import WebDAV -from .settings import SETTINGS +from .settings import SETTINGS, Credentials from .transformed_string import TransformedString -class User(BaseModel): - name: str - password: str - - class Site(BaseModel): model_config = ConfigDict(validate_default=True) @@ -60,7 +55,7 @@ class Image(BaseModel): class Config(BaseModel): # Login-Daten für Admin-Modus - admin: User + admin: Credentials # Lösungswort solution: TransformedString diff --git a/api/advent22_api/core/dav/webdav.py b/api/advent22_api/core/dav/webdav.py index 2e60a16..a5d8b67 100644 --- a/api/advent22_api/core/dav/webdav.py +++ b/api/advent22_api/core/dav/webdav.py @@ -16,8 +16,8 @@ class WebDAV: _webdav_client = WebDAVclient( { "webdav_hostname": SETTINGS.webdav.url, - "webdav_login": SETTINGS.webdav.username, - "webdav_password": SETTINGS.webdav.password, + "webdav_login": SETTINGS.webdav.auth.username, + "webdav_password": SETTINGS.webdav.auth.password, } ) diff --git a/api/advent22_api/core/depends.py b/api/advent22_api/core/depends.py index 7566644..15c90ab 100644 --- a/api/advent22_api/core/depends.py +++ b/api/advent22_api/core/depends.py @@ -5,7 +5,9 @@ from io import BytesIO from typing import cast from fastapi import Depends -from PIL import Image, ImageFont +from PIL import ImageFont +from PIL.Image import Image +from PIL.ImageFont import FreeTypeFont from .advent_image import _XY, AdventImage from .calendar_config import CalendarConfig, get_calendar_config @@ -22,6 +24,8 @@ from .helpers import ( set_len, ) +RE_NUM = re.compile(r"/(\d+)\.", flags=re.IGNORECASE) + async def get_all_sorted_days( cal_cfg: CalendarConfig = Depends(get_calendar_config), @@ -107,11 +111,10 @@ async def get_all_manual_image_names( Bilder: "manual" zuordnen """ - num_re = re.compile(r"/(\d+)\.", flags=re.IGNORECASE) return { int(num_match.group(1)): name for name in manual_image_names - if (num_match := num_re.search(name)) is not None + if (num_match := RE_NUM.search(name)) is not None } @@ -138,7 +141,7 @@ class TTFont: size: int = 50 @property - async def font(self) -> "ImageFont._Font": + async def font(self) -> FreeTypeFont: return ImageFont.truetype( font=BytesIO(await WebDAV.read_bytes(self.file_name)), size=100, @@ -169,7 +172,7 @@ async def gen_day_auto_image( auto_image_names: dict[int, str], day_parts: dict[int, str], ttfonts: list[TTFont], -) -> Image.Image: +) -> Image: """ Automatisch generiertes Bild erstellen """ @@ -200,7 +203,7 @@ async def get_day_image( auto_image_names: dict[int, str] = Depends(get_all_auto_image_names), day_parts: dict[int, str] = Depends(get_all_parts), ttfonts: list[TTFont] = Depends(get_all_ttfonts), -) -> Image.Image | None: +) -> Image | None: """ Bild für einen Tag abrufen """ diff --git a/api/advent22_api/core/helpers.py b/api/advent22_api/core/helpers.py index 78675d1..bbf7260 100644 --- a/api/advent22_api/core/helpers.py +++ b/api/advent22_api/core/helpers.py @@ -1,3 +1,4 @@ +import base64 import itertools import random import re @@ -5,8 +6,9 @@ from datetime import date, datetime, timedelta from io import BytesIO from typing import Any, Awaitable, Callable, Iterable, Self, Sequence, TypeVar -from fastapi.responses import StreamingResponse -from PIL import Image +from PIL import Image as PILImage +from PIL.Image import Image, Resampling +from pydantic import BaseModel from .config import get_config from .dav.webdav import WebDAV @@ -103,7 +105,7 @@ list_images_manual = list_helper("/images_manual", RE_IMG) list_fonts = list_helper("/files", RE_TTF) -async def load_image(file_name: str) -> Image.Image: +async def load_image(file_name: str) -> Image: """ Versuche, Bild aus Datei zu laden """ @@ -111,28 +113,54 @@ async def load_image(file_name: str) -> Image.Image: if not await WebDAV.exists(file_name): raise RuntimeError(f"DAV-File {file_name} does not exist!") - return Image.open(BytesIO(await WebDAV.read_bytes(file_name))) + return PILImage.open(BytesIO(await WebDAV.read_bytes(file_name))) -async def api_return_ico(img: Image.Image) -> StreamingResponse: +class ImageData(BaseModel): + width: int + height: int + aspect_ratio: float + data_url: str + + @classmethod + def create( + cls, + *, + media_type: str, + content: BytesIO, + width: int, + height: int, + ) -> Self: + img_data = base64.b64encode(content.getvalue()).decode("utf-8") + + return cls( + width=width, + height=height, + aspect_ratio=width / height, + data_url=f"data:{media_type};base64,{img_data}", + ) + + +async def api_return_ico(img: Image) -> ImageData: """ ICO-Bild mit API zurückgeben """ - # JPEG-Daten in Puffer speichern + # ICO-Daten in Puffer speichern (256px) img_buffer = BytesIO() - img.resize(size=(256, 256), resample=Image.LANCZOS) + img.resize(size=(256, 256), resample=Resampling.LANCZOS) img.save(img_buffer, format="ICO") - img_buffer.seek(0) # zurückgeben - return StreamingResponse( + return ImageData.create( media_type="image/x-icon", content=img_buffer, + width=img.width, + height=img.height, ) -async def api_return_jpeg(img: Image.Image) -> StreamingResponse: +async def api_return_jpeg(img: Image) -> ImageData: """ JPEG-Bild mit API zurückgeben """ @@ -140,12 +168,13 @@ async def api_return_jpeg(img: Image.Image) -> StreamingResponse: # JPEG-Daten in Puffer speichern img_buffer = BytesIO() img.save(img_buffer, format="JPEG", quality=85) - img_buffer.seek(0) # zurückgeben - return StreamingResponse( + return ImageData.create( media_type="image/jpeg", content=img_buffer, + width=img.width, + height=img.height, ) diff --git a/api/advent22_api/core/settings.py b/api/advent22_api/core/settings.py index 2a4ffc9..d0d2408 100644 --- a/api/advent22_api/core/settings.py +++ b/api/advent22_api/core/settings.py @@ -6,6 +6,11 @@ from pydantic_settings import BaseSettings, SettingsConfigDict T = TypeVar("T") +class Credentials(BaseModel): + username: str = "" + password: str = "" + + class DavSettings(BaseModel): """ Connection to a DAV server. @@ -16,8 +21,10 @@ class DavSettings(BaseModel): path: str = "/remote.php/webdav" prefix: str = "/advent22" - username: str = "advent22_user" - password: str = "password" + auth: Credentials = Credentials( + username="advent22_user", + password="password", + ) cache_ttl: int = 60 * 10 config_filename: str = "config.toml" diff --git a/api/advent22_api/routers/_security.py b/api/advent22_api/routers/_security.py index 6a6eb81..47e14c1 100644 --- a/api/advent22_api/routers/_security.py +++ b/api/advent22_api/routers/_security.py @@ -21,7 +21,7 @@ async def user_is_admin( username_correct = secrets.compare_digest( credentials.username.lower(), - cfg.admin.name.lower(), + cfg.admin.username.lower(), ) password_correct = secrets.compare_digest( credentials.password, diff --git a/api/advent22_api/routers/admin.py b/api/advent22_api/routers/admin.py index ee5bb4b..cc7fd8a 100644 --- a/api/advent22_api/routers/admin.py +++ b/api/advent22_api/routers/admin.py @@ -5,7 +5,11 @@ from pydantic import BaseModel from advent22_api.core.helpers import EventDates -from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config +from ..core.calendar_config import ( + CalendarConfig, + DoorsSaved, + get_calendar_config, +) from ..core.config import Config, Image, get_config from ..core.depends import ( TTFont, @@ -14,7 +18,7 @@ from ..core.depends import ( get_all_parts, get_all_ttfonts, ) -from ..core.settings import SETTINGS, RedisSettings +from ..core.settings import SETTINGS, Credentials, RedisSettings from ._security import require_admin, user_is_admin router = APIRouter(prefix="/admin", tags=["admin"]) @@ -170,24 +174,16 @@ async def put_doors( await cal_cfg.change(cfg) -@router.get("/dav_credentials") -async def get_dav_credentials( - _: None = Depends(require_admin), -) -> tuple[str, str]: - """ - Zugangsdaten für WebDAV - """ - - return SETTINGS.webdav.username, SETTINGS.webdav.password - - -@router.get("/ui_credentials") -async def get_ui_credentials( +@router.get("/credentials/{name}") +async def get_credentials( + name: str, _: None = Depends(require_admin), cfg: Config = Depends(get_config), -) -> tuple[str, str]: - """ - Zugangsdaten für Admin-UI - """ +) -> Credentials: - return cfg.admin.name, cfg.admin.password + if name == "dav": + return SETTINGS.webdav.auth + elif name == "ui": + return cfg.admin + else: + return Credentials() diff --git a/api/advent22_api/routers/user.py b/api/advent22_api/routers/user.py index 65ca8d4..20ee228 100644 --- a/api/advent22_api/routers/user.py +++ b/api/advent22_api/routers/user.py @@ -1,25 +1,31 @@ from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.responses import StreamingResponse -from PIL import Image +from PIL.Image import Image -from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config +from ..core.calendar_config import ( + CalendarConfig, + DoorsSaved, + get_calendar_config, +) from ..core.config import Config, Site, get_config from ..core.depends import get_all_event_dates, get_day_image -from ..core.helpers import EventDates, api_return_ico, api_return_jpeg, load_image +from ..core.helpers import ( + EventDates, + ImageData, + api_return_ico, + api_return_jpeg, + load_image, +) from ._security import user_can_view_day, user_is_admin, user_visible_days router = APIRouter(prefix="/user", tags=["user"]) -@router.get( - "/background_image", - response_class=StreamingResponse, -) +@router.get("/background_image") async def get_background_image( cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> StreamingResponse: +) -> ImageData: """ Hintergrundbild laden """ @@ -27,13 +33,10 @@ async def get_background_image( return await api_return_jpeg(await load_image(f"files/{cal_cfg.background}")) -@router.get( - "/favicon", - response_class=StreamingResponse, -) +@router.get("/favicon") async def get_favicon( cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> StreamingResponse: +) -> ImageData: """ Favicon laden """ @@ -68,15 +71,12 @@ async def get_doors( return [door for door in cal_cfg.doors if door.day in visible_days] -@router.get( - "/image_{day}", - response_class=StreamingResponse, -) +@router.get("/image_{day}") async def get_image_for_day( user_can_view: bool = Depends(user_can_view_day), is_admin: bool = Depends(user_is_admin), - image: Image.Image | None = Depends(get_day_image), -) -> StreamingResponse: + image: Image | None = Depends(get_day_image), +) -> ImageData: """ Bild für einen Tag erstellen """ diff --git a/ui/.devcontainer/devcontainer.json b/ui/.devcontainer/devcontainer.json index 1d9f5b2..c4804c1 100644 --- a/ui/.devcontainer/devcontainer.json +++ b/ui/.devcontainer/devcontainer.json @@ -2,15 +2,19 @@ // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node { "name": "Advent22 UI", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:1-18-bookworm", + "image": "mcr.microsoft.com/devcontainers/javascript-node:4-20-trixie", + // 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/apt-get-packages:1": { - "packages": "git-flow, git-lfs" + "packages": "git-flow" }, "ghcr.io/devcontainers-extra/features/vue-cli:2": {} }, + // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. @@ -29,11 +33,16 @@ ] } }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "yarn install", - "postStartCommand": "yarn install --production false", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "node" + + // Use 'postStartCommand' to run commands after the container is started. + "postStartCommand": "npx --yes update-browserslist-db@latest && yarn install --production false" + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index cf326ae..70723bd 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -2,33 +2,37 @@ module.exports = { root: true, env: { - node: true + node: true, }, - 'extends': [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - '@vue/typescript/recommended' + extends: [ + "plugin:vue/vue3-essential", + "eslint:recommended", + "@vue/typescript/recommended", ], parserOptions: { - ecmaVersion: 2020 + ecmaVersion: 2020, }, rules: { - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' + "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)' + "**/__tests__/*.{j,t}s?(x)", + "**/tests/unit/**/*.spec.{j,t}s?(x)", ], env: { - mocha: true + mocha: true, + }, + rules: { + "@typescript-eslint/no-unused-expressions": "off", } - } - ] -} + }, + ], +}; diff --git a/ui/.vscode/extensions.json b/ui/.vscode/extensions.json index e0be954..45d98df 100644 --- a/ui/.vscode/extensions.json +++ b/ui/.vscode/extensions.json @@ -1,5 +1,3 @@ { - "recommendations": [ - "sdras.vue-vscode-snippets" - ] -} \ No newline at end of file + "recommendations": ["sdras.vue-vscode-snippets"] +} diff --git a/ui/.vscode/launch.json b/ui/.vscode/launch.json index 91156ea..5423ca1 100644 --- a/ui/.vscode/launch.json +++ b/ui/.vscode/launch.json @@ -12,4 +12,4 @@ "webRoot": "${workspaceFolder}" } ] -} \ No newline at end of file +} diff --git a/ui/.vscode/settings.json b/ui/.vscode/settings.json index 37a5aab..57ee8c2 100644 --- a/ui/.vscode/settings.json +++ b/ui/.vscode/settings.json @@ -1,22 +1,23 @@ { - "editor.formatOnSave": true, - "[vue]": { + "[scss][vue][typescript][javascript][json][jsonc][jsonl]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + + "[jsonc]": { + "editor.formatOnSave": false, }, + "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, + "git.closeDiffOnOperation": true, + "editor.formatOnSave": true, "editor.tabSize": 2, + "sass.disableAutoIndent": true, "sass.format.convert": false, "sass.format.deleteWhitespace": true, + "prettier.trailingComma": "all", - "volar.inlayHints.eventArgumentInInlineHandlers": false, -} \ No newline at end of file +} diff --git a/ui/.vscode/tasks.json b/ui/.vscode/tasks.json index 2cd5033..e5ffa95 100644 --- a/ui/.vscode/tasks.json +++ b/ui/.vscode/tasks.json @@ -1,12 +1,12 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "serve", - "problemMatcher": [], - "label": "UI starten", - "detail": "vue-cli-service serve" - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "serve", + "problemMatcher": [], + "label": "UI starten", + "detail": "vue-cli-service serve" + } + ] +} diff --git a/ui/README.md b/ui/README.md index 5715cb1..9ed3aab 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,24 +1,29 @@ # advent22_ui ## Project setup + ``` yarn install ``` ### Compiles and hot-reloads for development + ``` yarn serve ``` ### Compiles and minifies for production + ``` yarn build ``` ### Lints and fixes files + ``` yarn lint ``` ### Customize configuration + See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/ui/package.json b/ui/package.json index 0e92cae..fb1a4e6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,45 +3,44 @@ "version": "0.1.0", "private": true, "scripts": { - "serve": "vue-cli-service serve --host 0.0.0.0 --port 8080", + "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" + "lint": "vue-cli-service lint", + "ui": "vue ui --host 0.0.0.0 --headless" }, "devDependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-brands-svg-icons": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", - "@fortawesome/vue-fontawesome": "^3.0.6", - "@types/chai": "^4.3.14", - "@types/luxon": "^3.4.2", - "@types/mocha": "^10.0.6", - "@typescript-eslint/eslint-plugin": "^7.3.1", - "@typescript-eslint/parser": "^7.3.1", - "@vue/cli-plugin-babel": "~5.0.0", - "@vue/cli-plugin-eslint": "~5.0.0", - "@vue/cli-plugin-typescript": "~5.0.0", - "@vue/cli-plugin-unit-mocha": "~5.0.0", - "@vue/cli-service": "~5.0.0", + "@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", + "@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", - "@vue/test-utils": "^2.4.5", - "@vueuse/core": "^10.9.0", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.8.1", "animate.css": "^4.1.1", - "axios": "^1.6.8", - "bulma": "^0.9.4", - "bulma-prefers-dark": "^0.1.0-beta.1", + "axios": "^1.13.5", + "bulma": "^1.0.4", "bulma-toast": "2.4.3", - "chai": "^4.3.10", - "core-js": "^3.36.1", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.23.0", - "luxon": "^3.4.4", - "pinia": "^2.1.7", - "sass": "^1.72.0", - "sass-loader": "^14.1.1", - "typescript": "~5.4.3", - "vue": "^3.4.21", - "vue-class-component": "^8.0.0-0" + "chai": "^6.2.2", + "core-js": "^3.48.0", + "eslint": "^8.57.1", + "eslint-plugin-vue": "^9.33.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" } } diff --git a/ui/public/index.html b/ui/public/index.html index 3c5b076..17c86a1 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -1,30 +1,38 @@
- - - - + + + +