-
- {% if auth.enabled %}
- {% include "build-status/login-modal.jinja2" %}
- {% endif %}
-
- {% include "build-status/alerts.jinja2" %}
-
- {% include "build-status/dashboard.jinja2" %}
- {% include "build-status/package-add-modal.jinja2" %}
- {% include "build-status/package-rebuild-modal.jinja2" %}
- {% include "build-status/key-import-modal.jinja2" %}
-
- {% include "build-status/package-info-modal.jinja2" %}
-
- {% include "build-status/table.jinja2" %}
-
-
+
+
diff --git a/pyproject.toml b/pyproject.toml
index dc82e6ca..aae0ba62 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -118,11 +118,14 @@ include = [
"CONTRIBUTING.md",
"SECURITY.md",
"package",
+ "frontend",
"subpackages.py",
"web.png",
]
exclude = [
"package/archlinux",
+ "frontend/node_modules",
+ "frontend/package-lock.json",
]
[tool.flit.external-data]
diff --git a/src/ahriman/core/configuration/schema.py b/src/ahriman/core/configuration/schema.py
index d1284915..66038637 100644
--- a/src/ahriman/core/configuration/schema.py
+++ b/src/ahriman/core/configuration/schema.py
@@ -398,6 +398,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"path_exists": True,
"path_type": "dir",
},
+ "template": {
+ "type": "string",
+ "empty": False,
+ },
"templates": {
"type": "list",
"coerce": "list",
diff --git a/src/ahriman/core/utils.py b/src/ahriman/core/utils.py
index 6ac48ce8..40e6fe47 100644
--- a/src/ahriman/core/utils.py
+++ b/src/ahriman/core/utils.py
@@ -35,7 +35,7 @@ from enum import Enum
from filelock import FileLock
from pathlib import Path
from pwd import getpwuid
-from typing import Any, IO, TypeVar
+from typing import Any, IO, TypeVar, cast
from ahriman.core.exceptions import CalledProcessError, OptionError, UnsafeRunError
from ahriman.core.types import Comparable
@@ -285,16 +285,17 @@ def filelock(path: Path) -> Iterator[FileLock]:
lock_path.unlink(missing_ok=True)
-def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]:
+def filter_json(source: T, known_fields: Iterable[str] | None = None) -> T:
"""
- filter json object by fields used for json-to-object conversion
+ recursively filter json object removing ``None`` values and optionally filtering by known fields
Args:
- source(dict[str, Any]): raw json object
- known_fields(Iterable[str]): list of fields which have to be known for the target object
+ source(T): raw json object (dict, list, or scalar)
+ known_fields(Iterable[str] | None, optional): list of fields which have to be known for the target object
+ (Default value = None)
Returns:
- dict[str, Any]: json object without unknown and empty fields
+ T: json without ``None`` values
Examples:
This wrapper is mainly used for the dataclasses, thus the flow must be something like this::
@@ -306,7 +307,15 @@ def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str
>>> properties = filter_json(dump, known_fields)
>>> package = Package(**properties)
"""
- return {key: value for key, value in source.items() if key in known_fields and value is not None}
+ if isinstance(source, dict):
+ return cast(T, {
+ key: filter_json(value)
+ for key, value in source.items()
+ if value is not None and (known_fields is None or key in known_fields)
+ })
+ if isinstance(source, list):
+ return cast(T, [filter_json(value) for value in source if value is not None])
+ return source
def full_version(epoch: str | int | None, pkgver: str, pkgrel: str) -> str:
diff --git a/src/ahriman/web/schemas/__init__.py b/src/ahriman/web/schemas/__init__.py
index 2710f174..b04d2621 100644
--- a/src/ahriman/web/schemas/__init__.py
+++ b/src/ahriman/web/schemas/__init__.py
@@ -19,7 +19,9 @@
#
from ahriman.web.schemas.any_schema import AnySchema
from ahriman.web.schemas.aur_package_schema import AURPackageSchema
+from ahriman.web.schemas.auth_info_schema import AuthInfoSchema
from ahriman.web.schemas.auth_schema import AuthSchema
+from ahriman.web.schemas.auto_refresh_interval_schema import AutoRefreshIntervalSchema
from ahriman.web.schemas.build_options_schema import BuildOptionsSchema
from ahriman.web.schemas.changes_schema import ChangesSchema
from ahriman.web.schemas.configuration_schema import ConfigurationSchema
@@ -30,6 +32,7 @@ from ahriman.web.schemas.event_schema import EventSchema
from ahriman.web.schemas.event_search_schema import EventSearchSchema
from ahriman.web.schemas.file_schema import FileSchema
from ahriman.web.schemas.info_schema import InfoSchema
+from ahriman.web.schemas.info_v2_schema import InfoV2Schema
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
from ahriman.web.schemas.log_schema import LogSchema
from ahriman.web.schemas.login_schema import LoginSchema
diff --git a/src/ahriman/web/schemas/auth_info_schema.py b/src/ahriman/web/schemas/auth_info_schema.py
new file mode 100644
index 00000000..5c2904a0
--- /dev/null
+++ b/src/ahriman/web/schemas/auth_info_schema.py
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from ahriman.web.apispec import Schema, fields
+
+
+class AuthInfoSchema(Schema):
+ """
+ authorization information schema
+ """
+
+ control = fields.String(required=True, metadata={
+ "description": "HTML control for login interface",
+ })
+ enabled = fields.Boolean(required=True, metadata={
+ "description": "Whether authentication is enabled or not",
+ })
+ username = fields.String(metadata={
+ "description": "Currently authenticated username if any",
+ })
diff --git a/src/ahriman/web/schemas/auto_refresh_interval_schema.py b/src/ahriman/web/schemas/auto_refresh_interval_schema.py
new file mode 100644
index 00000000..83d01b86
--- /dev/null
+++ b/src/ahriman/web/schemas/auto_refresh_interval_schema.py
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from ahriman.web.apispec import Schema, fields
+
+
+class AutoRefreshIntervalSchema(Schema):
+ """
+ auto refresh interval schema
+ """
+
+ interval = fields.Integer(required=True, metadata={
+ "description": "Auto refresh interval in milliseconds",
+ })
+ is_active = fields.Boolean(required=True, metadata={
+ "description": "Whether this interval is the default active one",
+ })
+ text = fields.String(required=True, metadata={
+ "description": "Human readable interval description",
+ })
diff --git a/src/ahriman/web/schemas/info_schema.py b/src/ahriman/web/schemas/info_schema.py
index 877d9f2c..5e8bca46 100644
--- a/src/ahriman/web/schemas/info_schema.py
+++ b/src/ahriman/web/schemas/info_schema.py
@@ -27,7 +27,7 @@ class InfoSchema(Schema):
response service information schema
"""
- auth = fields.Boolean(dump_default=False, required=True, metadata={
+ auth = fields.Boolean(required=True, metadata={
"description": "Whether authentication is enabled or not",
})
repositories = fields.Nested(RepositoryIdSchema(many=True), required=True, metadata={
diff --git a/src/ahriman/web/schemas/info_v2_schema.py b/src/ahriman/web/schemas/info_v2_schema.py
new file mode 100644
index 00000000..ea8f7007
--- /dev/null
+++ b/src/ahriman/web/schemas/info_v2_schema.py
@@ -0,0 +1,50 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from ahriman import __version__
+from ahriman.web.apispec import Schema, fields
+from ahriman.web.schemas.auth_info_schema import AuthInfoSchema
+from ahriman.web.schemas.auto_refresh_interval_schema import AutoRefreshIntervalSchema
+from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
+
+
+class InfoV2Schema(Schema):
+ """
+ response service information schema
+ """
+
+ auth = fields.Nested(AuthInfoSchema(), required=True, metadata={
+ "description": "Authorization descriptor",
+ })
+ autorefresh_intervals = fields.Nested(AutoRefreshIntervalSchema(many=True), metadata={
+ "description": "Available auto refresh intervals",
+ })
+ docs_enabled = fields.Boolean(metadata={
+ "description": "Whether API documentation is enabled",
+ })
+ index_url = fields.String(metadata={
+ "description": "URL to the repository index page",
+ })
+ repositories = fields.Nested(RepositoryIdSchema(many=True), required=True, metadata={
+ "description": "List of loaded repositories",
+ })
+ version = fields.String(required=True, metadata={
+ "description": "Service version",
+ "example": __version__,
+ })
diff --git a/src/ahriman/web/schemas/repository_id_schema.py b/src/ahriman/web/schemas/repository_id_schema.py
index e3dff167..5a0606cb 100644
--- a/src/ahriman/web/schemas/repository_id_schema.py
+++ b/src/ahriman/web/schemas/repository_id_schema.py
@@ -29,6 +29,10 @@ class RepositoryIdSchema(Schema):
"description": "Repository architecture",
"example": "x86_64",
})
+ id = fields.String(metadata={
+ "description": "Unique repository identifier",
+ "example": "aur-x86_64",
+ })
repository = fields.String(metadata={
"description": "Repository name",
"example": "aur",
diff --git a/src/ahriman/web/server_info.py b/src/ahriman/web/server_info.py
new file mode 100644
index 00000000..faf2733f
--- /dev/null
+++ b/src/ahriman/web/server_info.py
@@ -0,0 +1,69 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from collections.abc import Callable
+from typing import Any
+
+from ahriman import __version__
+from ahriman.core.auth.helpers import authorized_userid
+from ahriman.core.types import Comparable
+from ahriman.core.utils import pretty_interval
+from ahriman.web.apispec import aiohttp_apispec
+from ahriman.web.views.base import BaseView
+
+
+async def server_info(view: BaseView) -> dict[str, Any]:
+ """
+ generate server info which can be used in responses directly
+
+ Args:
+ view(BaseView): view of the request
+
+ Returns:
+ dict[str, Any]: server info as a json response
+ """
+ autorefresh_intervals = [
+ {
+ "interval": interval * 1000, # milliseconds
+ "is_active": index == 0, # first element is always default
+ "text": pretty_interval(interval),
+ }
+ for index, interval in enumerate(view.configuration.getintlist("web", "autorefresh_intervals", fallback=[]))
+ if interval > 0 # special case if 0 exists and first, refresh will not be turned on by default
+ ]
+ comparator: Callable[[dict[str, Any]], Comparable] = lambda interval: interval["interval"]
+
+ return {
+ "auth": {
+ "control": view.validator.auth_control,
+ "enabled": view.validator.enabled,
+ "username": await authorized_userid(view.request),
+ },
+ "autorefresh_intervals": sorted(autorefresh_intervals, key=comparator),
+ "docs_enabled": aiohttp_apispec is not None,
+ "index_url": view.configuration.get("web", "index_url", fallback=None),
+ "repositories": [
+ {
+ "id": repository_id.id,
+ **repository_id.view(),
+ }
+ for repository_id in sorted(view.services)
+ ],
+ "version": __version__,
+ }
diff --git a/src/ahriman/web/views/api/swagger.py b/src/ahriman/web/views/api/swagger.py
index f60566ca..36bbc278 100644
--- a/src/ahriman/web/views/api/swagger.py
+++ b/src/ahriman/web/views/api/swagger.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import Response, json_response
+from aiohttp.web import Response
from collections.abc import Callable
from typing import ClassVar
@@ -96,4 +96,4 @@ class SwaggerView(BaseView):
for key, value in schema.items()
}
- return json_response(spec)
+ return self.json_response(spec)
diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py
index 4a28e44a..ba93b746 100644
--- a/src/ahriman/web/views/base.py
+++ b/src/ahriman/web/views/base.py
@@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
+from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, Response, StreamResponse, View, json_response
from aiohttp_cors import CorsViewMixin
from collections.abc import Awaitable, Callable
-from typing import ClassVar, TypeVar
+from typing import Any, ClassVar, TypeVar
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
@@ -29,6 +29,7 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.sign.gpg import GPG
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
+from ahriman.core.utils import filter_json
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.keys import AuthKey, ConfigurationKey, SpawnKey, WatcherKey, WorkersKey
@@ -162,6 +163,20 @@ class BaseView(View, CorsViewMixin):
raise KeyError(f"Key {key} is missing or empty") from None
return value
+ @staticmethod
+ def json_response(data: dict[str, Any] | list[Any], **kwargs: Any) -> Response:
+ """
+ filter and convert data and return :class:`aiohttp.web.Response` object
+
+ Args:
+ data(dict[str, Any]): response in json format
+ **kwargs(Any): keyword arguments for :func:`aiohttp.web.json_response` function
+
+ Returns:
+ Response: generated response object
+ """
+ return json_response(filter_json(data), **kwargs)
+
# pylint: disable=not-callable,protected-access
async def head(self) -> StreamResponse:
"""
diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py
index ec9dfd07..b2046441 100644
--- a/src/ahriman/web/views/index.py
+++ b/src/ahriman/web/views/index.py
@@ -19,12 +19,11 @@
#
import aiohttp_jinja2
-from typing import Any, ClassVar
+from aiohttp.web import Response
+from typing import ClassVar
-from ahriman.core.auth.helpers import authorized_userid
-from ahriman.core.utils import pretty_interval
from ahriman.models.user_access import UserAccess
-from ahriman.web.apispec import aiohttp_apispec
+from ahriman.web.server_info import server_info
from ahriman.web.views.base import BaseView
@@ -48,6 +47,7 @@ class IndexView(BaseView):
* id - unique repository identifier, string, required
* repository - repository name, string, required
* architecture - repository architecture, string, required
+ * version - service version, string, required
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
@@ -56,41 +56,14 @@ class IndexView(BaseView):
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/", "/index.html"]
- @aiohttp_jinja2.template("build-status.jinja2")
- async def get(self) -> dict[str, Any]:
+ async def get(self) -> Response:
"""
process get request. No parameters supported here
Returns:
- dict[str, Any]: parameters for jinja template
+ Response: 200 with rendered index page
"""
- auth_username = await authorized_userid(self.request)
- auth = {
- "control": self.validator.auth_control,
- "enabled": self.validator.enabled,
- "username": auth_username,
- }
+ context = await server_info(self)
- autorefresh_intervals = [
- {
- "interval": interval * 1000, # milliseconds
- "is_active": index == 0, # first element is always default
- "text": pretty_interval(interval),
- }
- for index, interval in enumerate(self.configuration.getintlist("web", "autorefresh_intervals", fallback=[]))
- if interval > 0 # special case if 0 exists and first, refresh will not be turned on by default
- ]
-
- return {
- "auth": auth,
- "autorefresh_intervals": sorted(autorefresh_intervals, key=lambda interval: interval["interval"]),
- "docs_enabled": aiohttp_apispec is not None,
- "index_url": self.configuration.get("web", "index_url", fallback=None),
- "repositories": [
- {
- "id": repository.id,
- **repository.view(),
- }
- for repository in sorted(self.services)
- ]
- }
+ template = self.configuration.get("web", "template", fallback="build-status.jinja2")
+ return aiohttp_jinja2.render_template(template, self.request, context)
diff --git a/src/ahriman/web/views/v1/auditlog/events.py b/src/ahriman/web/views/v1/auditlog/events.py
index 53a945a0..968e174c 100644
--- a/src/ahriman/web/views/v1/auditlog/events.py
+++ b/src/ahriman/web/views/v1/auditlog/events.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response
from typing import ClassVar
from ahriman.models.event import Event
@@ -70,7 +70,7 @@ class EventsView(BaseView):
events = self.service().event_get(event, object_id, from_date, to_date, limit, offset)
response = [event.view() for event in events]
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Audit log"],
diff --git a/src/ahriman/web/views/v1/distributed/workers.py b/src/ahriman/web/views/v1/distributed/workers.py
index 56bb1cdf..d5de4f27 100644
--- a/src/ahriman/web/views/v1/distributed/workers.py
+++ b/src/ahriman/web/views/v1/distributed/workers.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response
from collections.abc import Callable
from typing import ClassVar
@@ -78,7 +78,7 @@ class WorkersView(BaseView):
comparator: Callable[[Worker], Comparable] = lambda item: item.identifier
response = [worker.view() for worker in sorted(workers, key=comparator)]
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Distributed"],
diff --git a/src/ahriman/web/views/v1/packages/changes.py b/src/ahriman/web/views/v1/packages/changes.py
index 22b504ea..00b3f619 100644
--- a/src/ahriman/web/views/v1/packages/changes.py
+++ b/src/ahriman/web/views/v1/packages/changes.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response
from typing import ClassVar
from ahriman.models.changes import Changes
@@ -65,7 +65,7 @@ class ChangesView(StatusViewGuard, BaseView):
changes = self.service(package_base=package_base).package_changes_get(package_base)
- return json_response(changes.view())
+ return self.json_response(changes.view())
@apidocs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/packages/dependencies.py b/src/ahriman/web/views/v1/packages/dependencies.py
index 611f0167..9d4de53e 100644
--- a/src/ahriman/web/views/v1/packages/dependencies.py
+++ b/src/ahriman/web/views/v1/packages/dependencies.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response
from typing import ClassVar
from ahriman.models.dependencies import Dependencies
@@ -65,7 +65,7 @@ class DependenciesView(StatusViewGuard, BaseView):
dependencies = self.service(package_base=package_base).package_dependencies_get(package_base)
- return json_response(dependencies.view())
+ return self.json_response(dependencies.view())
@apidocs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/packages/logs.py b/src/ahriman/web/views/v1/packages/logs.py
index 5d712d83..26f4bf10 100644
--- a/src/ahriman/web/views/v1/packages/logs.py
+++ b/src/ahriman/web/views/v1/packages/logs.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response
from typing import ClassVar
from ahriman.core.exceptions import UnknownPackageError
@@ -99,7 +99,7 @@ class LogsView(StatusViewGuard, BaseView):
"status": status.view(),
"logs": "\n".join(f"[{pretty_datetime(log_record.created)}] {log_record.message}" for log_record in logs)
}
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/packages/package.py b/src/ahriman/web/views/v1/packages/package.py
index d119c8cd..79e2546b 100644
--- a/src/ahriman/web/views/v1/packages/package.py
+++ b/src/ahriman/web/views/v1/packages/package.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response
from typing import ClassVar
from ahriman.core.exceptions import UnknownPackageError
@@ -105,7 +105,7 @@ class PackageView(StatusViewGuard, BaseView):
"repository": repository_id.view(),
}
]
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/packages/packages.py b/src/ahriman/web/views/v1/packages/packages.py
index e914d54d..a93d82b9 100644
--- a/src/ahriman/web/views/v1/packages/packages.py
+++ b/src/ahriman/web/views/v1/packages/packages.py
@@ -19,7 +19,7 @@
#
import itertools
-from aiohttp.web import HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPNoContent, Response
from collections.abc import Callable
from typing import ClassVar
@@ -78,7 +78,7 @@ class PackagesView(StatusViewGuard, BaseView):
} for package, status in itertools.islice(sorted(packages, key=comparator), offset, stop)
]
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/packages/patch.py b/src/ahriman/web/views/v1/packages/patch.py
index 8850f5e8..9ec13bd4 100644
--- a/src/ahriman/web/views/v1/packages/patch.py
+++ b/src/ahriman/web/views/v1/packages/patch.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPNoContent, HTTPNotFound, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -89,4 +89,4 @@ class PatchView(StatusViewGuard, BaseView):
if selected is None:
raise HTTPNotFound(reason=f"Patch {variable} is unknown")
- return json_response(selected.view())
+ return self.json_response(selected.view())
diff --git a/src/ahriman/web/views/v1/packages/patches.py b/src/ahriman/web/views/v1/packages/patches.py
index 031af8b5..dd8346e0 100644
--- a/src/ahriman/web/views/v1/packages/patches.py
+++ b/src/ahriman/web/views/v1/packages/patches.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response
from typing import ClassVar
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@@ -60,7 +60,7 @@ class PatchesView(StatusViewGuard, BaseView):
patches = self.service().package_patches_get(package_base, None)
response = [patch.view() for patch in patches]
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Packages"],
diff --git a/src/ahriman/web/views/v1/service/add.py b/src/ahriman/web/views/v1/service/add.py
index 9307bb64..a0356f77 100644
--- a/src/ahriman/web/views/v1/service/add.py
+++ b/src/ahriman/web/views/v1/service/add.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, Response, json_response
+from aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@@ -78,4 +78,4 @@ class AddView(BaseView):
refresh=data.get("refresh", False),
)
- return json_response({"process_id": process_id})
+ return self.json_response({"process_id": process_id})
diff --git a/src/ahriman/web/views/v1/service/config.py b/src/ahriman/web/views/v1/service/config.py
index bdb74920..0801bce4 100644
--- a/src/ahriman/web/views/v1/service/config.py
+++ b/src/ahriman/web/views/v1/service/config.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPNoContent, Response
from typing import ClassVar
from ahriman.core.formatters import ConfigurationPrinter
@@ -64,7 +64,7 @@ class ConfigView(BaseView):
for key, value in values.items()
if key not in ConfigurationPrinter.HIDE_KEYS
]
- return json_response(response)
+ return self.json_response(response)
@apidocs(
tags=["Actions"],
diff --git a/src/ahriman/web/views/v1/service/pgp.py b/src/ahriman/web/views/v1/service/pgp.py
index fd7b1b98..7c682d91 100644
--- a/src/ahriman/web/views/v1/service/pgp.py
+++ b/src/ahriman/web/views/v1/service/pgp.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -71,7 +71,7 @@ class PGPView(BaseView):
except Exception:
raise HTTPNotFound(reason=f"Key {key} is unknown")
- return json_response({"key": key})
+ return self.json_response({"key": key})
@apidocs(
tags=["Actions"],
@@ -100,4 +100,4 @@ class PGPView(BaseView):
process_id = self.spawner.key_import(key, data.get("server"))
- return json_response({"process_id": process_id})
+ return self.json_response({"process_id": process_id})
diff --git a/src/ahriman/web/views/v1/service/process.py b/src/ahriman/web/views/v1/service/process.py
index 6e32f685..bd8d42e1 100644
--- a/src/ahriman/web/views/v1/service/process.py
+++ b/src/ahriman/web/views/v1/service/process.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPNotFound, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -66,4 +66,4 @@ class ProcessView(BaseView):
"is_alive": is_alive,
}
- return json_response(response)
+ return self.json_response(response)
diff --git a/src/ahriman/web/views/v1/service/rebuild.py b/src/ahriman/web/views/v1/service/rebuild.py
index a0469b45..0e88749d 100644
--- a/src/ahriman/web/views/v1/service/rebuild.py
+++ b/src/ahriman/web/views/v1/service/rebuild.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, Response, json_response
+from aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -74,4 +74,4 @@ class RebuildView(BaseView):
increment=data.get("increment", True),
)
- return json_response({"process_id": process_id})
+ return self.json_response({"process_id": process_id})
diff --git a/src/ahriman/web/views/v1/service/remove.py b/src/ahriman/web/views/v1/service/remove.py
index b2e1668e..8a21589a 100644
--- a/src/ahriman/web/views/v1/service/remove.py
+++ b/src/ahriman/web/views/v1/service/remove.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, Response, json_response
+from aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -67,4 +67,4 @@ class RemoveView(BaseView):
repository_id = self.repository_id()
process_id = self.spawner.packages_remove(repository_id, packages)
- return json_response({"process_id": process_id})
+ return self.json_response({"process_id": process_id})
diff --git a/src/ahriman/web/views/v1/service/request.py b/src/ahriman/web/views/v1/service/request.py
index 93ae3f38..55eb2e28 100644
--- a/src/ahriman/web/views/v1/service/request.py
+++ b/src/ahriman/web/views/v1/service/request.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, Response, json_response
+from aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@@ -78,4 +78,4 @@ class RequestView(BaseView):
refresh=False, # refresh doesn't work here
)
- return json_response({"process_id": process_id})
+ return self.json_response({"process_id": process_id})
diff --git a/src/ahriman/web/views/v1/service/search.py b/src/ahriman/web/views/v1/service/search.py
index 08b958e8..bd8ffb41 100644
--- a/src/ahriman/web/views/v1/service/search.py
+++ b/src/ahriman/web/views/v1/service/search.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response
from collections.abc import Callable
from typing import ClassVar
@@ -83,4 +83,4 @@ class SearchView(BaseView):
"description": package.description,
} for package in sorted(packages, key=comparator)
]
- return json_response(response)
+ return self.json_response(response)
diff --git a/src/ahriman/web/views/v1/service/update.py b/src/ahriman/web/views/v1/service/update.py
index 5eb14eaa..bc95b457 100644
--- a/src/ahriman/web/views/v1/service/update.py
+++ b/src/ahriman/web/views/v1/service/update.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, Response, json_response
+from aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -75,4 +75,4 @@ class UpdateView(BaseView):
refresh=data.get("refresh", False),
)
- return json_response({"process_id": process_id})
+ return self.json_response({"process_id": process_id})
diff --git a/src/ahriman/web/views/v1/status/info.py b/src/ahriman/web/views/v1/status/info.py
index 7e92cd63..e1635f91 100644
--- a/src/ahriman/web/views/v1/status/info.py
+++ b/src/ahriman/web/views/v1/status/info.py
@@ -17,13 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import Response, json_response
+from aiohttp.web import Response
from typing import ClassVar
-from ahriman import __version__
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import InfoSchema
+from ahriman.web.server_info import server_info
from ahriman.web.views.base import BaseView
@@ -52,13 +52,11 @@ class InfoView(BaseView):
Returns:
Response: 200 with service information object
"""
+ info = await server_info(self)
response = {
- "auth": self.validator.enabled,
- "repositories": [
- repository_id.view()
- for repository_id in sorted(self.services)
- ],
- "version": __version__,
+ "auth": info["auth"]["enabled"],
+ "repositories": info["repositories"],
+ "version": info["version"],
}
- return json_response(response)
+ return self.json_response(response)
diff --git a/src/ahriman/web/views/v1/status/repositories.py b/src/ahriman/web/views/v1/status/repositories.py
index 5e3ecc6a..81e333e3 100644
--- a/src/ahriman/web/views/v1/status/repositories.py
+++ b/src/ahriman/web/views/v1/status/repositories.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import Response, json_response
+from aiohttp.web import Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -56,4 +56,4 @@ class RepositoriesView(BaseView):
for repository_id in sorted(self.services)
]
- return json_response(repositories)
+ return self.json_response(repositories)
diff --git a/src/ahriman/web/views/v1/status/status.py b/src/ahriman/web/views/v1/status/status.py
index bc72f709..55a851a0 100644
--- a/src/ahriman/web/views/v1/status/status.py
+++ b/src/ahriman/web/views/v1/status/status.py
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
+from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response
from typing import ClassVar
from ahriman import __version__
@@ -75,7 +75,7 @@ class StatusView(StatusViewGuard, BaseView):
version=__version__,
)
- return json_response(status.view())
+ return self.json_response(status.view())
@apidocs(
tags=["Status"],
diff --git a/src/ahriman/web/views/v2/packages/logs.py b/src/ahriman/web/views/v2/packages/logs.py
index b2ab43a1..231e7091 100644
--- a/src/ahriman/web/views/v2/packages/logs.py
+++ b/src/ahriman/web/views/v2/packages/logs.py
@@ -19,7 +19,7 @@
#
import itertools
-from aiohttp.web import Response, json_response
+from aiohttp.web import Response
from dataclasses import replace
from typing import ClassVar
@@ -31,8 +31,7 @@ from ahriman.web.views.status_view_guard import StatusViewGuard
class LogsView(StatusViewGuard, BaseView):
- """ else:
-
+ """
package logs web view
Attributes:
@@ -80,4 +79,4 @@ class LogsView(StatusViewGuard, BaseView):
]
response = [log_record.view() for log_record in logs]
- return json_response(response)
+ return self.json_response(response)
diff --git a/src/ahriman/web/views/v2/status/__init__.py b/src/ahriman/web/views/v2/status/__init__.py
new file mode 100644
index 00000000..cddc28d6
--- /dev/null
+++ b/src/ahriman/web/views/v2/status/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
diff --git a/src/ahriman/web/views/v2/status/info.py b/src/ahriman/web/views/v2/status/info.py
new file mode 100644
index 00000000..c486173b
--- /dev/null
+++ b/src/ahriman/web/views/v2/status/info.py
@@ -0,0 +1,56 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from aiohttp.web import Response
+from typing import ClassVar
+
+from ahriman.models.user_access import UserAccess
+from ahriman.web.apispec.decorators import apidocs
+from ahriman.web.schemas import InfoV2Schema
+from ahriman.web.server_info import server_info
+from ahriman.web.views.base import BaseView
+
+
+class InfoView(BaseView):
+ """
+ web service information view
+
+ Attributes:
+ GET_PERMISSION(UserAccess): (class attribute) get permissions of self
+ """
+
+ GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
+ ROUTES = ["/api/v2/info"]
+
+ @apidocs(
+ tags=["Status"],
+ summary="Service information",
+ description="Perform basic service health check and returns its information",
+ permission=GET_PERMISSION,
+ schema=InfoV2Schema,
+ )
+ async def get(self) -> Response:
+ """
+ get service information
+
+ Returns:
+ Response: 200 with service information object
+ """
+ response = await server_info(self)
+ return self.json_response(response)
diff --git a/subpackages.py b/subpackages.py
index ec871c39..62a9f13e 100644
--- a/subpackages.py
+++ b/subpackages.py
@@ -45,10 +45,12 @@ SUBPACKAGES = {
prefix / "lib" / "systemd" / "system" / "ahriman-web.service",
prefix / "lib" / "systemd" / "system" / "ahriman-web@.service",
prefix / "share" / "ahriman" / "settings" / "ahriman.ini.d" / "00-web.ini",
- prefix / "share" / "ahriman" / "templates" / "api.jinja2",
prefix / "share" / "ahriman" / "templates" / "build-status",
- prefix / "share" / "ahriman" / "templates" / "build-status.jinja2",
+ prefix / "share" / "ahriman" / "templates" / "build-status.jinja",
+ prefix / "share" / "ahriman" / "templates" / "build-status-v2.jinja",
+ prefix / "share" / "ahriman" / "templates" / "api.jinja2",
prefix / "share" / "ahriman" / "templates" / "error.jinja2",
+ prefix / "share" / "ahriman" / "templates" / "static",
site_packages / "ahriman" / "application" / "handlers" / "web.py",
site_packages / "ahriman" / "core" / "auth",
site_packages / "ahriman" / "web",
diff --git a/tests/ahriman/core/test_utils.py b/tests/ahriman/core/test_utils.py
index 10a84aea..5d1c87cd 100644
--- a/tests/ahriman/core/test_utils.py
+++ b/tests/ahriman/core/test_utils.py
@@ -549,9 +549,6 @@ def test_walk(resource_path_root: Path) -> None:
must traverse directory recursively
"""
expected = sorted([
- resource_path_root / "core" / "ahriman.ini",
- resource_path_root / "core" / "arcanisrepo.files.tar.gz",
- resource_path_root / "core" / "logging.ini",
resource_path_root / "models" / "aur_error",
resource_path_root / "models" / "big_file_checksum",
resource_path_root / "models" / "empty_file_checksum",
@@ -569,26 +566,6 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_yay_pkgbuild",
resource_path_root / "models" / "pkgbuild",
resource_path_root / "models" / "utf8",
- resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "dashboard.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "package-info-modal.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "package-rebuild-modal.jinja2",
- resource_path_root / "web" / "templates" / "build-status" / "table.jinja2",
- resource_path_root / "web" / "templates" / "static" / "favicon.ico",
- resource_path_root / "web" / "templates" / "static" / "logo.svg",
- resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
- resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
- resource_path_root / "web" / "templates" / "api.jinja2",
- resource_path_root / "web" / "templates" / "build-status.jinja2",
- resource_path_root / "web" / "templates" / "email-index.jinja2",
- resource_path_root / "web" / "templates" / "error.jinja2",
- resource_path_root / "web" / "templates" / "repo-index.jinja2",
- resource_path_root / "web" / "templates" / "rss.jinja2",
- resource_path_root / "web" / "templates" / "shell",
- resource_path_root / "web" / "templates" / "telegram-index.jinja2",
])
- local_files = list(sorted(walk(resource_path_root)))
+ local_files = list(path for path in sorted(walk(resource_path_root)) if path.parent.name == "models")
assert local_files == expected
diff --git a/tests/ahriman/web/schemas/test_auth_info_schema.py b/tests/ahriman/web/schemas/test_auth_info_schema.py
new file mode 100644
index 00000000..1982fb6b
--- /dev/null
+++ b/tests/ahriman/web/schemas/test_auth_info_schema.py
@@ -0,0 +1 @@
+# schema testing goes in view class tests
diff --git a/tests/ahriman/web/schemas/test_auto_refresh_interval_schema.py b/tests/ahriman/web/schemas/test_auto_refresh_interval_schema.py
new file mode 100644
index 00000000..1982fb6b
--- /dev/null
+++ b/tests/ahriman/web/schemas/test_auto_refresh_interval_schema.py
@@ -0,0 +1 @@
+# schema testing goes in view class tests
diff --git a/tests/ahriman/web/schemas/test_info_v2_schema.py b/tests/ahriman/web/schemas/test_info_v2_schema.py
new file mode 100644
index 00000000..1982fb6b
--- /dev/null
+++ b/tests/ahriman/web/schemas/test_info_v2_schema.py
@@ -0,0 +1 @@
+# schema testing goes in view class tests
diff --git a/tests/ahriman/web/test_server_info.py b/tests/ahriman/web/test_server_info.py
new file mode 100644
index 00000000..6cbec8f0
--- /dev/null
+++ b/tests/ahriman/web/test_server_info.py
@@ -0,0 +1,26 @@
+import pytest
+
+from aiohttp.web import Application
+
+from ahriman import __version__
+from ahriman.models.repository_id import RepositoryId
+from ahriman.web.server_info import server_info
+from ahriman.web.views.index import IndexView
+
+
+async def test_server_info(application: Application, repository_id: RepositoryId) -> None:
+ """
+ must generate server info
+ """
+ request = pytest.helpers.request(application, "", "GET")
+ view = IndexView(request)
+ result = await server_info(view)
+
+ assert result["repositories"] == [{"id": repository_id.id, **repository_id.view()}]
+ assert not result["auth"]["enabled"]
+ assert result["auth"]["username"] is None
+ assert result["auth"]["control"]
+ assert result["version"] == __version__
+ assert result["autorefresh_intervals"] == []
+ assert result["docs_enabled"]
+ assert result["index_url"] is None
diff --git a/tests/ahriman/web/views/v1/status/test_view_v1_status_info.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_info.py
index ff741482..14bc82b0 100644
--- a/tests/ahriman/web/views/v1/status/test_view_v1_status_info.py
+++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_info.py
@@ -35,6 +35,6 @@ async def test_get(client: TestClient, repository_id: RepositoryId) -> None:
json = await response.json()
assert not response_schema.validate(json)
- assert json["repositories"] == [repository_id.view()]
+ assert json["repositories"] == [{"id": repository_id.id, **repository_id.view()}]
assert not json["auth"]
assert json["version"] == __version__
diff --git a/tests/ahriman/web/views/v2/status/test_view_v2_status_info.py b/tests/ahriman/web/views/v2/status/test_view_v2_status_info.py
new file mode 100644
index 00000000..b77ed5e5
--- /dev/null
+++ b/tests/ahriman/web/views/v2/status/test_view_v2_status_info.py
@@ -0,0 +1,43 @@
+import pytest
+
+from aiohttp.test_utils import TestClient
+
+from ahriman import __version__
+from ahriman.models.repository_id import RepositoryId
+from ahriman.models.user_access import UserAccess
+from ahriman.web.views.v2.status.info import InfoView
+
+
+async def test_get_permission() -> None:
+ """
+ must return correct permission for the request
+ """
+ for method in ("GET",):
+ request = pytest.helpers.request("", "", method)
+ assert await InfoView.get_permission(request) == UserAccess.Unauthorized
+
+
+def test_routes() -> None:
+ """
+ must return correct routes
+ """
+ assert InfoView.ROUTES == ["/api/v2/info"]
+
+
+async def test_get(client: TestClient, repository_id: RepositoryId) -> None:
+ """
+ must return service information
+ """
+ response_schema = pytest.helpers.schema_response(InfoView.get)
+
+ response = await client.get("/api/v2/info")
+ assert response.ok
+ json = await response.json()
+ assert not response_schema.validate(json)
+
+ assert json["repositories"] == [{"id": repository_id.id, **repository_id.view()}]
+ assert not json["auth"]["enabled"]
+ assert json["auth"]["control"]
+ assert json["version"] == __version__
+ assert json["autorefresh_intervals"] == []
+ assert json["docs_enabled"]
diff --git a/tox.toml b/tox.toml
index 390f5c84..432f03fe 100644
--- a/tox.toml
+++ b/tox.toml
@@ -78,6 +78,9 @@ commands = [
[env.check]
description = "Run common checks like linter, mypy, etc"
+allowlist_externals = [
+ "npx",
+]
dependency_groups = [
"check",
]
@@ -123,6 +126,11 @@ commands = [
"--non-interactive",
"--package", "{[project]name}",
],
+ [
+ "npx",
+ "eslint",
+ "frontend",
+ ],
]
[env.docs]
@@ -193,6 +201,24 @@ commands = [
],
]
+[env.frontend]
+description = "Build frontend HTML and JS"
+allowlist_externals = [
+ "npm",
+]
+change_dir = "frontend"
+commands = [
+ [
+ "npm",
+ "install",
+ ],
+ [
+ "npm",
+ "run",
+ "build",
+ ],
+]
+
[env.html]
description = "Generate html documentation"
dependency_groups = [