Compare commits

..

20 Commits

Author SHA1 Message Date
f5c12d03ec Release 2.20.0rc7 2026-03-08 12:18:03 +02:00
606ee2fb0e docs: architecture spec update 2026-03-08 12:16:26 +02:00
875e9cf58a docs: update web preview picture 2026-03-08 03:33:49 +02:00
a809a4b67f feat: support request-id header 2026-03-08 02:51:30 +02:00
437ae2c16c style: some minor style improvements 2026-03-06 21:38:57 +02:00
10661f3127 fix: catch some StopIteration exceptions 2026-03-06 17:04:19 +02:00
88ac3d82a0 feat: explicitly disable archive rotation if option is set to 0 2026-03-06 12:59:48 +02:00
386765ab4e fix: fix dashboard visual 2026-03-06 01:48:11 +02:00
78998182b8 Release 2.20.0rc6 2026-03-06 01:32:13 +02:00
2563391aaf build: correct order of building 2026-03-06 01:31:53 +02:00
90a78e99e7 Release 2.20.0rc5 2026-03-06 01:06:15 +02:00
a05eab9042 feat: brand-new interface (#158)
This was initally generated by ai, but later has been heavily edited.
The reason why it has been implemented is that there are plans to
implement more features to ui, but it becomes hard to add new features
to plain js, so I decided to rewrite it in typescript.

Yet because it is still ai slop, it is still possible to enable old
interface via configuration, even though new interface is turned on by
default to get feedback
2026-03-06 00:59:10 +02:00
db46147f0d fix: mount archive to chroot
while file:// repository is automatically mounted by devtools, it
doesn't mount any directory which contains source of symlinks

This commit adds implicit mount of the archive directory (ro) into
chroot
2026-03-03 15:50:21 +02:00
49ebbc34fa fix: do not update package status if it is unchanged
In order to prevent timestamp bumps, filter by status is added
2026-02-24 15:33:06 +02:00
e376f1307f docs: remove required flag from email.template_full option 2026-02-22 02:57:34 +02:00
415fcf58ce Release 2.20.0rc4 2026-02-21 12:52:51 +02:00
17a2d6362c style: replace """ with " for single line strings 2026-02-21 12:51:58 +02:00
e6275de4ed fix: handle class overrides for retry polices correctly
Previous implementation didn't work as intended because there was still
override in init. Current implementation instead of playing with
guessing separates default and instance setttings. Also update test
cases to handle this scenario correctly
2026-02-21 12:40:11 +02:00
4d009cba6d Release 2.20.0rc3 2026-02-20 20:56:05 +02:00
f6defbf90d fix: rollback samesite option to Lax, because of broken OAuth 2026-02-20 20:54:37 +02:00
199 changed files with 6638 additions and 480 deletions

View File

@@ -12,3 +12,6 @@ __pycache__/
*.pyc *.pyc
*.pyd *.pyd
*.pyo *.pyo
node_modules/
package-lock.json

View File

@@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
# refresh the image # refresh the image
pacman -Syyu --noconfirm pacman -Syyu --noconfirm
# main dependencies # main dependencies
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
# make dependencies # make dependencies
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
# optional dependencies # optional dependencies

View File

@@ -26,7 +26,7 @@ jobs:
- ${{ github.workspace }}:/build - ${{ github.workspace }}:/build
steps: steps:
- run: pacman --noconfirm -Syu base-devel git python-tox - run: pacman --noconfirm -Syu base-devel git npm python-tox
- run: git config --global --add safe.directory * - run: git config --global --add safe.directory *

6
.gitignore vendored
View File

@@ -99,3 +99,9 @@ status_cache.json
*.db *.db
docs/html/ docs/html/
# Frontend
node_modules/
package-lock.json
package/share/ahriman/templates/static/index.js
package/share/ahriman/templates/static/index.css

View File

@@ -125,7 +125,7 @@ Again, the most checks can be performed by `tox` command, though some additional
def __hash__(self) -> int: ... # basically any magic (or look-alike) method def __hash__(self) -> int: ... # basically any magic (or look-alike) method
``` ```
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Same idea applies to frontend classes.
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment. Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
@@ -172,8 +172,9 @@ Again, the most checks can be performed by `tox` command, though some additional
) )
``` ```
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. * Imports goes in alphabetical order, no relative imports allowed. Same rule applies to frontend classes.
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute. * One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. Same rule applies to frontend classes.
* It is possible to create file which contains some functions (e.g. `ahriman.core.utils`), but in this case you would need to define `__all__` attribute.
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools. * The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case. * No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent. * Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
@@ -226,6 +227,8 @@ Again, the most checks can be performed by `tox` command, though some additional
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests. The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
Frontend checks normally are performed by `eslint` (e.g. `npx run eslint`).
## Developers how to ## Developers how to
### Run automated checks ### Run automated checks

View File

@@ -23,6 +23,7 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
RUN pacman -S --noconfirm --asdeps \ RUN pacman -S --noconfirm --asdeps \
devtools \ devtools \
git \ git \
npm \
pyalpm \ pyalpm \
python-bcrypt \ python-bcrypt \
python-filelock \ python-filelock \

View File

@@ -138,11 +138,12 @@ digraph G {
ahriman_core_http [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp",shape="box"]; ahriman_core_http [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp",shape="box"];
ahriman_core_http_sync_ahriman_client [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp\.\nsync_ahriman_client",shape="box"]; ahriman_core_http_sync_ahriman_client [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp\.\nsync_ahriman_client",shape="box"];
ahriman_core_http_sync_http_client [fillcolor="#933f24",fontcolor="#ffffff",label="ahriman\.\ncore\.\nhttp\.\nsync_http_client"]; ahriman_core_http_sync_http_client [fillcolor="#933f24",fontcolor="#ffffff",label="ahriman\.\ncore\.\nhttp\.\nsync_http_client"];
ahriman_core_log [fillcolor="#e53b05",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog"]; ahriman_core_log [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nlog",shape="box"];
ahriman_core_log_http_log_handler [fillcolor="#914730",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nhttp_log_handler"]; ahriman_core_log_http_log_handler [fillcolor="#914730",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nhttp_log_handler"];
ahriman_core_log_journal_handler [fillcolor="#ac6149",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\njournal_handler"]; ahriman_core_log_journal_handler [fillcolor="#ac6149",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\njournal_handler"];
ahriman_core_log_lazy_logging [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlazy_logging"]; ahriman_core_log_lazy_logging [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nlog\.\nlazy_logging",shape="box"];
ahriman_core_log_log_loader [fillcolor="#82402b",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlog_loader"]; ahriman_core_log_log_context [fillcolor="#db582f",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlog_context"];
ahriman_core_log_log_loader [fillcolor="#7a3c28",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlog_loader"];
ahriman_core_module_loader [fillcolor="#ce5e3b",fontcolor="#ffffff",label="ahriman\.\ncore\.\nmodule_loader"]; ahriman_core_module_loader [fillcolor="#ce5e3b",fontcolor="#ffffff",label="ahriman\.\ncore\.\nmodule_loader"];
ahriman_core_report [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport",shape="box"]; ahriman_core_report [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport",shape="box"];
ahriman_core_report_console [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport\.\nconsole",shape="box"]; ahriman_core_report_console [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport\.\nconsole",shape="box"];
@@ -242,15 +243,18 @@ digraph G {
ahriman_web_apispec_info [fillcolor="#a14f35",fontcolor="#ffffff",label="ahriman\.\nweb\.\napispec\.\ninfo"]; ahriman_web_apispec_info [fillcolor="#a14f35",fontcolor="#ffffff",label="ahriman\.\nweb\.\napispec\.\ninfo"];
ahriman_web_cors [fillcolor="#b0573a",fontcolor="#ffffff",label="ahriman\.\nweb\.\ncors"]; ahriman_web_cors [fillcolor="#b0573a",fontcolor="#ffffff",label="ahriman\.\nweb\.\ncors"];
ahriman_web_keys [fillcolor="#823017",fontcolor="#ffffff",label="ahriman\.\nweb\.\nkeys"]; ahriman_web_keys [fillcolor="#823017",fontcolor="#ffffff",label="ahriman\.\nweb\.\nkeys"];
ahriman_web_middlewares [fillcolor="#e9410c",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares"]; ahriman_web_middlewares [fillcolor="#ef3e06",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares"];
ahriman_web_middlewares_auth_handler [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nauth_handler"]; ahriman_web_middlewares_auth_handler [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nauth_handler"];
ahriman_web_middlewares_exception_handler [fillcolor="#994b33",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nexception_handler"]; ahriman_web_middlewares_exception_handler [fillcolor="#994b33",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nexception_handler"];
ahriman_web_middlewares_metrics_handler [fillcolor="#a34628",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nmetrics_handler"]; ahriman_web_middlewares_metrics_handler [fillcolor="#a34628",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nmetrics_handler"];
ahriman_web_middlewares_request_id_handler [fillcolor="#8a442e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nrequest_id_handler"];
ahriman_web_routes [fillcolor="#8a442e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nroutes"]; ahriman_web_routes [fillcolor="#8a442e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nroutes"];
ahriman_web_schemas [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas",shape="box"]; ahriman_web_schemas [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas",shape="box"];
ahriman_web_schemas_any_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nany_schema"]; ahriman_web_schemas_any_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nany_schema"];
ahriman_web_schemas_aur_package_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\naur_package_schema"]; ahriman_web_schemas_aur_package_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\naur_package_schema"];
ahriman_web_schemas_auth_info_schema [fillcolor="#c45431",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauth_info_schema"];
ahriman_web_schemas_auth_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauth_schema"]; ahriman_web_schemas_auth_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauth_schema"];
ahriman_web_schemas_auto_refresh_interval_schema [fillcolor="#c45431",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nauto_refresh_interval_schema"];
ahriman_web_schemas_build_options_schema [fillcolor="#d04e24",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nbuild_options_schema"]; ahriman_web_schemas_build_options_schema [fillcolor="#d04e24",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nbuild_options_schema"];
ahriman_web_schemas_changes_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nchanges_schema"]; ahriman_web_schemas_changes_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nchanges_schema"];
ahriman_web_schemas_configuration_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nconfiguration_schema"]; ahriman_web_schemas_configuration_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nconfiguration_schema"];
@@ -261,6 +265,7 @@ digraph G {
ahriman_web_schemas_event_search_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\nevent_search_schema",shape="box"]; ahriman_web_schemas_event_search_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\nevent_search_schema",shape="box"];
ahriman_web_schemas_file_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nfile_schema"]; ahriman_web_schemas_file_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nfile_schema"];
ahriman_web_schemas_info_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninfo_schema",shape="box"]; ahriman_web_schemas_info_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninfo_schema",shape="box"];
ahriman_web_schemas_info_v2_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninfo_v2_schema",shape="box"];
ahriman_web_schemas_internal_status_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninternal_status_schema",shape="box"]; ahriman_web_schemas_internal_status_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\ninternal_status_schema",shape="box"];
ahriman_web_schemas_log_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nlog_schema"]; ahriman_web_schemas_log_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nlog_schema"];
ahriman_web_schemas_login_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nlogin_schema"]; ahriman_web_schemas_login_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nlogin_schema"];
@@ -289,11 +294,12 @@ digraph G {
ahriman_web_schemas_status_schema [fillcolor="#ca4116",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nstatus_schema"]; ahriman_web_schemas_status_schema [fillcolor="#ca4116",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nstatus_schema"];
ahriman_web_schemas_update_flags_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\nupdate_flags_schema",shape="box"]; ahriman_web_schemas_update_flags_schema [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas\.\nupdate_flags_schema",shape="box"];
ahriman_web_schemas_worker_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nworker_schema"]; ahriman_web_schemas_worker_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nworker_schema"];
ahriman_web_server_info [fillcolor="#93371a",fontcolor="#ffffff",label="ahriman\.\nweb\.\nserver_info"];
ahriman_web_views [fillcolor="#f94810",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews"]; ahriman_web_views [fillcolor="#f94810",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews"];
ahriman_web_views_api_docs [fillcolor="#794434",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\napi\.\ndocs"]; ahriman_web_views_api_docs [fillcolor="#794434",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\napi\.\ndocs"];
ahriman_web_views_api_swagger [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\napi\.\nswagger"]; ahriman_web_views_api_swagger [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\napi\.\nswagger"];
ahriman_web_views_base [fillcolor="#952603",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nbase"]; ahriman_web_views_base [fillcolor="#952603",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nbase"];
ahriman_web_views_index [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nindex"]; ahriman_web_views_index [fillcolor="#794434",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nindex"];
ahriman_web_views_static [fillcolor="#884d3a",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nstatic"]; ahriman_web_views_static [fillcolor="#884d3a",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nstatic"];
ahriman_web_views_status_view_guard [fillcolor="#ef3e06",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nstatus_view_guard"]; ahriman_web_views_status_view_guard [fillcolor="#ef3e06",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nstatus_view_guard"];
ahriman_web_views_v1_auditlog_events [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nauditlog\.\nevents"]; ahriman_web_views_v1_auditlog_events [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nauditlog\.\nevents"];
@@ -316,13 +322,14 @@ digraph G {
ahriman_web_views_v1_service_search [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nsearch"]; ahriman_web_views_v1_service_search [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nsearch"];
ahriman_web_views_v1_service_update [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nupdate"]; ahriman_web_views_v1_service_update [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nupdate"];
ahriman_web_views_v1_service_upload [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nupload"]; ahriman_web_views_v1_service_upload [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nservice\.\nupload"];
ahriman_web_views_v1_status_info [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\ninfo"]; ahriman_web_views_v1_status_info [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\ninfo"];
ahriman_web_views_v1_status_metrics [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nmetrics"]; ahriman_web_views_v1_status_metrics [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nmetrics"];
ahriman_web_views_v1_status_repositories [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nrepositories"]; ahriman_web_views_v1_status_repositories [fillcolor="#724031",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nrepositories"];
ahriman_web_views_v1_status_status [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nstatus"]; ahriman_web_views_v1_status_status [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nstatus\.\nstatus"];
ahriman_web_views_v1_user_login [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nuser\.\nlogin"]; ahriman_web_views_v1_user_login [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nuser\.\nlogin"];
ahriman_web_views_v1_user_logout [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nuser\.\nlogout"]; ahriman_web_views_v1_user_logout [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv1\.\nuser\.\nlogout"];
ahriman_web_views_v2_packages_logs [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv2\.\npackages\.\nlogs"]; ahriman_web_views_v2_packages_logs [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv2\.\npackages\.\nlogs"];
ahriman_web_views_v2_status_info [fillcolor="#6b3c2e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nviews\.\nv2\.\nstatus\.\ninfo"];
ahriman_web_web [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nweb"]; ahriman_web_web [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nweb"];
aioauth_client [fillcolor="#c07d40",shape="folder"]; aioauth_client [fillcolor="#c07d40",shape="folder"];
aiohttp [fillcolor="#f9b506",shape="folder"]; aiohttp [fillcolor="#f9b506",shape="folder"];
@@ -488,11 +495,12 @@ digraph G {
ahriman_core -> ahriman_models_worker [fillcolor="#ef3e06",minlen="2"]; ahriman_core -> ahriman_models_worker [fillcolor="#ef3e06",minlen="2"];
ahriman_core -> ahriman_web_keys [fillcolor="#ef3e06",minlen="2"]; ahriman_core -> ahriman_web_keys [fillcolor="#ef3e06",minlen="2"];
ahriman_core -> ahriman_web_middlewares_auth_handler [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_middlewares_auth_handler [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_middlewares_request_id_handler [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_routes [fillcolor="#ef3e06",minlen="2"]; ahriman_core -> ahriman_web_routes [fillcolor="#ef3e06",minlen="2"];
ahriman_core -> ahriman_web_server_info [fillcolor="#ef3e06",minlen="2"];
ahriman_core -> ahriman_web_views_api_docs [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_views_api_docs [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_views_api_swagger [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_views_api_swagger [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_views_base [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_views_base [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_views_index [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_views_status_view_guard [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_views_status_view_guard [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_views_v1_distributed_workers [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_views_v1_distributed_workers [fillcolor="#ef3e06",minlen="3"];
ahriman_core -> ahriman_web_views_v1_packages_logs [fillcolor="#ef3e06",minlen="3"]; ahriman_core -> ahriman_web_views_v1_packages_logs [fillcolor="#ef3e06",minlen="3"];
@@ -542,13 +550,13 @@ digraph G {
ahriman_core_archive_archive_trigger -> ahriman_core_archive [fillcolor="blue",weight="3"]; ahriman_core_archive_archive_trigger -> ahriman_core_archive [fillcolor="blue",weight="3"];
ahriman_core_auth -> ahriman_web_keys [fillcolor="blue",minlen="2"]; ahriman_core_auth -> ahriman_web_keys [fillcolor="blue",minlen="2"];
ahriman_core_auth -> ahriman_web_middlewares_auth_handler [fillcolor="blue",minlen="3"]; ahriman_core_auth -> ahriman_web_middlewares_auth_handler [fillcolor="blue",minlen="3"];
ahriman_core_auth -> ahriman_web_server_info [fillcolor="blue",minlen="2"];
ahriman_core_auth -> ahriman_web_views_base [fillcolor="blue",minlen="3"]; ahriman_core_auth -> ahriman_web_views_base [fillcolor="blue",minlen="3"];
ahriman_core_auth -> ahriman_web_views_index [fillcolor="blue",minlen="3"];
ahriman_core_auth -> ahriman_web_views_v1_user_login [fillcolor="blue",minlen="3"]; ahriman_core_auth -> ahriman_web_views_v1_user_login [fillcolor="blue",minlen="3"];
ahriman_core_auth -> ahriman_web_views_v1_user_logout [fillcolor="blue",minlen="3"]; ahriman_core_auth -> ahriman_web_views_v1_user_logout [fillcolor="blue",minlen="3"];
ahriman_core_auth -> ahriman_web_web [fillcolor="blue",minlen="2"]; ahriman_core_auth -> ahriman_web_web [fillcolor="blue",minlen="2"];
ahriman_core_auth_auth -> ahriman_core_auth [fillcolor="blue",weight="3"]; ahriman_core_auth_auth -> ahriman_core_auth [fillcolor="blue",weight="3"];
ahriman_core_auth_helpers -> ahriman_web_views_index [fillcolor="#d04e24",minlen="3"]; ahriman_core_auth_helpers -> ahriman_web_server_info [fillcolor="#d04e24",minlen="3"];
ahriman_core_auth_helpers -> ahriman_web_views_v1_user_login [fillcolor="#d04e24",minlen="3"]; ahriman_core_auth_helpers -> ahriman_web_views_v1_user_login [fillcolor="#d04e24",minlen="3"];
ahriman_core_auth_helpers -> ahriman_web_views_v1_user_logout [fillcolor="#d04e24",minlen="3"]; ahriman_core_auth_helpers -> ahriman_web_views_v1_user_logout [fillcolor="#d04e24",minlen="3"];
ahriman_core_auth_mapping -> ahriman_core_auth_auth [fillcolor="blue",weight="3"]; ahriman_core_auth_mapping -> ahriman_core_auth_auth [fillcolor="blue",weight="3"];
@@ -843,35 +851,39 @@ digraph G {
ahriman_core_http_sync_ahriman_client -> ahriman_core_http [fillcolor="blue",weight="3"]; ahriman_core_http_sync_ahriman_client -> ahriman_core_http [fillcolor="blue",weight="3"];
ahriman_core_http_sync_http_client -> ahriman_core_http [fillcolor="#933f24",weight="3"]; ahriman_core_http_sync_http_client -> ahriman_core_http [fillcolor="#933f24",weight="3"];
ahriman_core_http_sync_http_client -> ahriman_core_http_sync_ahriman_client [fillcolor="#933f24",weight="3"]; ahriman_core_http_sync_http_client -> ahriman_core_http_sync_ahriman_client [fillcolor="#933f24",weight="3"];
ahriman_core_log -> ahriman_application_application_application_properties [fillcolor="#e53b05",minlen="3"]; ahriman_core_log -> ahriman_application_application_application_properties [fillcolor="blue",minlen="3"];
ahriman_core_log -> ahriman_application_application_workers_updater [fillcolor="#e53b05",minlen="3"]; ahriman_core_log -> ahriman_application_application_workers_updater [fillcolor="blue",minlen="3"];
ahriman_core_log -> ahriman_application_handlers_handler [fillcolor="#e53b05",minlen="3"]; ahriman_core_log -> ahriman_application_handlers_handler [fillcolor="blue",minlen="3"];
ahriman_core_log -> ahriman_application_lock [fillcolor="#e53b05",minlen="2"]; ahriman_core_log -> ahriman_application_lock [fillcolor="blue",minlen="2"];
ahriman_core_log -> ahriman_core_alpm_pacman [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_alpm_pacman [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_alpm_repo [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_alpm_repo [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_archive_archive_tree [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_archive_archive_tree [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_auth_auth [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_auth_auth [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_build_tools_package_version [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_build_tools_package_version [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_build_tools_sources [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_build_tools_sources [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_build_tools_task [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_build_tools_task [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_database_migrations [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_database_migrations [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_database_operations_operations [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_database_operations_operations [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_distributed_workers_cache [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_distributed_workers_cache [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_gitremote_remote_pull [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_gitremote_remote_pull [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_gitremote_remote_push [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_gitremote_remote_push [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_http_sync_http_client [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_http_sync_http_client [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_report_report [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_report_report [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_repository_repository_properties [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_repository_repository_properties [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_spawn [fillcolor="#e53b05",weight="2"]; ahriman_core_log -> ahriman_core_spawn [fillcolor="blue",weight="2"];
ahriman_core_log -> ahriman_core_status_watcher [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_status_watcher [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_triggers_trigger [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_triggers_trigger [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_triggers_trigger_loader [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_triggers_trigger_loader [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_core_upload_upload [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_core_log -> ahriman_core_upload_upload [fillcolor="blue",minlen="2",weight="2"];
ahriman_core_log -> ahriman_models_package [fillcolor="#e53b05",minlen="2"]; ahriman_core_log -> ahriman_models_package [fillcolor="blue",minlen="2"];
ahriman_core_log -> ahriman_models_repository_paths [fillcolor="#e53b05",minlen="2"]; ahriman_core_log -> ahriman_models_repository_paths [fillcolor="blue",minlen="2"];
ahriman_core_log -> ahriman_web_middlewares_request_id_handler [fillcolor="blue",minlen="3"];
ahriman_core_log_http_log_handler -> ahriman_core_log_log_loader [fillcolor="#914730",weight="3"]; ahriman_core_log_http_log_handler -> ahriman_core_log_log_loader [fillcolor="#914730",weight="3"];
ahriman_core_log_lazy_logging -> ahriman_core_log [fillcolor="#b85a3d",weight="3"]; ahriman_core_log_lazy_logging -> ahriman_core_log [fillcolor="blue",weight="3"];
ahriman_core_log_log_loader -> ahriman_application_handlers_handler [fillcolor="#82402b",minlen="3"]; ahriman_core_log_log_context -> ahriman_core_log_lazy_logging [fillcolor="#db582f",weight="3"];
ahriman_core_log_log_context -> ahriman_core_log_log_loader [fillcolor="#db582f",weight="3"];
ahriman_core_log_log_context -> ahriman_web_middlewares_request_id_handler [fillcolor="#db582f",minlen="3"];
ahriman_core_log_log_loader -> ahriman_application_handlers_handler [fillcolor="#7a3c28",minlen="3"];
ahriman_core_module_loader -> ahriman_application_ahriman [fillcolor="#ce5e3b",minlen="2"]; ahriman_core_module_loader -> ahriman_application_ahriman [fillcolor="#ce5e3b",minlen="2"];
ahriman_core_module_loader -> ahriman_web_routes [fillcolor="#ce5e3b",minlen="2"]; ahriman_core_module_loader -> ahriman_web_routes [fillcolor="#ce5e3b",minlen="2"];
ahriman_core_report_console -> ahriman_core_report_report [fillcolor="blue",weight="3"]; ahriman_core_report_console -> ahriman_core_report_report [fillcolor="blue",weight="3"];
@@ -991,6 +1003,7 @@ digraph G {
ahriman_core_types -> ahriman_core_report_jinja_template [fillcolor="#f94810",minlen="2",weight="2"]; ahriman_core_types -> ahriman_core_report_jinja_template [fillcolor="#f94810",minlen="2",weight="2"];
ahriman_core_types -> ahriman_core_report_rss [fillcolor="#f94810",minlen="2",weight="2"]; ahriman_core_types -> ahriman_core_report_rss [fillcolor="#f94810",minlen="2",weight="2"];
ahriman_core_types -> ahriman_core_utils [fillcolor="#f94810",weight="2"]; ahriman_core_types -> ahriman_core_utils [fillcolor="#f94810",weight="2"];
ahriman_core_types -> ahriman_web_server_info [fillcolor="#f94810",minlen="2"];
ahriman_core_types -> ahriman_web_views_v1_distributed_workers [fillcolor="#f94810",minlen="3"]; ahriman_core_types -> ahriman_web_views_v1_distributed_workers [fillcolor="#f94810",minlen="3"];
ahriman_core_types -> ahriman_web_views_v1_packages_packages [fillcolor="#f94810",minlen="3"]; ahriman_core_types -> ahriman_web_views_v1_packages_packages [fillcolor="#f94810",minlen="3"];
ahriman_core_types -> ahriman_web_views_v1_service_search [fillcolor="#f94810",minlen="3"]; ahriman_core_types -> ahriman_web_views_v1_service_search [fillcolor="#f94810",minlen="3"];
@@ -1060,8 +1073,9 @@ digraph G {
ahriman_core_utils -> ahriman_models_repository_paths [fillcolor="#db3805",minlen="2"]; ahriman_core_utils -> ahriman_models_repository_paths [fillcolor="#db3805",minlen="2"];
ahriman_core_utils -> ahriman_models_repository_stats [fillcolor="#db3805",minlen="2"]; ahriman_core_utils -> ahriman_models_repository_stats [fillcolor="#db3805",minlen="2"];
ahriman_core_utils -> ahriman_models_worker [fillcolor="#db3805",minlen="2"]; ahriman_core_utils -> ahriman_models_worker [fillcolor="#db3805",minlen="2"];
ahriman_core_utils -> ahriman_web_server_info [fillcolor="#db3805",minlen="2"];
ahriman_core_utils -> ahriman_web_views_api_swagger [fillcolor="#db3805",minlen="3"]; ahriman_core_utils -> ahriman_web_views_api_swagger [fillcolor="#db3805",minlen="3"];
ahriman_core_utils -> ahriman_web_views_index [fillcolor="#db3805",minlen="3"]; ahriman_core_utils -> ahriman_web_views_base [fillcolor="#db3805",minlen="3"];
ahriman_core_utils -> ahriman_web_views_v1_packages_logs [fillcolor="#db3805",minlen="3"]; ahriman_core_utils -> ahriman_web_views_v1_packages_logs [fillcolor="#db3805",minlen="3"];
ahriman_core_utils -> ahriman_web_views_v1_service_upload [fillcolor="#db3805",minlen="3"]; ahriman_core_utils -> ahriman_web_views_v1_service_upload [fillcolor="#db3805",minlen="3"];
ahriman_models -> ahriman_application_ahriman [fillcolor="#f94810",minlen="2"]; ahriman_models -> ahriman_application_ahriman [fillcolor="#f94810",minlen="2"];
@@ -1245,6 +1259,7 @@ digraph G {
ahriman_models -> ahriman_web_views_v1_user_login [fillcolor="#f94810",minlen="3"]; ahriman_models -> ahriman_web_views_v1_user_login [fillcolor="#f94810",minlen="3"];
ahriman_models -> ahriman_web_views_v1_user_logout [fillcolor="#f94810",minlen="3"]; ahriman_models -> ahriman_web_views_v1_user_logout [fillcolor="#f94810",minlen="3"];
ahriman_models -> ahriman_web_views_v2_packages_logs [fillcolor="#f94810",minlen="3"]; ahriman_models -> ahriman_web_views_v2_packages_logs [fillcolor="#f94810",minlen="3"];
ahriman_models -> ahriman_web_views_v2_status_info [fillcolor="#f94810",minlen="3"];
ahriman_models -> ahriman_web_web [fillcolor="#f94810",minlen="2"]; ahriman_models -> ahriman_web_web [fillcolor="#f94810",minlen="2"];
ahriman_models_action -> ahriman_application_handlers_change [fillcolor="#e75222",minlen="3"]; ahriman_models_action -> ahriman_application_handlers_change [fillcolor="#e75222",minlen="3"];
ahriman_models_action -> ahriman_application_handlers_patch [fillcolor="#e75222",minlen="3"]; ahriman_models_action -> ahriman_application_handlers_patch [fillcolor="#e75222",minlen="3"];
@@ -1653,6 +1668,7 @@ digraph G {
ahriman_models_user_access -> ahriman_web_views_v1_user_login [fillcolor="#f94810",minlen="3"]; ahriman_models_user_access -> ahriman_web_views_v1_user_login [fillcolor="#f94810",minlen="3"];
ahriman_models_user_access -> ahriman_web_views_v1_user_logout [fillcolor="#f94810",minlen="3"]; ahriman_models_user_access -> ahriman_web_views_v1_user_logout [fillcolor="#f94810",minlen="3"];
ahriman_models_user_access -> ahriman_web_views_v2_packages_logs [fillcolor="#f94810",minlen="3"]; ahriman_models_user_access -> ahriman_web_views_v2_packages_logs [fillcolor="#f94810",minlen="3"];
ahriman_models_user_access -> ahriman_web_views_v2_status_info [fillcolor="#f94810",minlen="3"];
ahriman_models_waiter -> ahriman_application_lock [fillcolor="#c45431",minlen="2"]; ahriman_models_waiter -> ahriman_application_lock [fillcolor="#c45431",minlen="2"];
ahriman_models_waiter -> ahriman_core_report_remote_call [fillcolor="#c45431",minlen="3"]; ahriman_models_waiter -> ahriman_core_report_remote_call [fillcolor="#c45431",minlen="3"];
ahriman_models_worker -> ahriman_application_application_workers_remote_updater [fillcolor="#e9410c",minlen="3"]; ahriman_models_worker -> ahriman_application_application_workers_remote_updater [fillcolor="#e9410c",minlen="3"];
@@ -1663,7 +1679,9 @@ digraph G {
ahriman_web -> ahriman_application_handlers_web [fillcolor="#f94810",minlen="3"]; ahriman_web -> ahriman_application_handlers_web [fillcolor="#f94810",minlen="3"];
ahriman_web_apispec -> ahriman_web_schemas_any_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_any_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_aur_package_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_aur_package_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_auth_info_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_auth_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_auth_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_auto_refresh_interval_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_build_options_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_build_options_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_changes_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_changes_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_configuration_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_configuration_schema [fillcolor="#e53b05",minlen="2",weight="2"];
@@ -1674,6 +1692,7 @@ digraph G {
ahriman_web_apispec -> ahriman_web_schemas_event_search_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_event_search_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_file_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_file_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_info_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_info_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_info_v2_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_internal_status_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_internal_status_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_log_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_log_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_login_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_login_schema [fillcolor="#e53b05",minlen="2",weight="2"];
@@ -1702,9 +1721,9 @@ digraph G {
ahriman_web_apispec -> ahriman_web_schemas_status_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_status_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_update_flags_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_update_flags_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_schemas_worker_schema [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_schemas_worker_schema [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_server_info [fillcolor="#e53b05",weight="2"];
ahriman_web_apispec -> ahriman_web_views_api_docs [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_api_docs [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_api_swagger [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_api_swagger [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_index [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_v1_auditlog_events [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_v1_auditlog_events [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_v1_distributed_workers [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_v1_distributed_workers [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_v1_packages_changes [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_v1_packages_changes [fillcolor="#e53b05",minlen="2",weight="2"];
@@ -1732,6 +1751,7 @@ digraph G {
ahriman_web_apispec -> ahriman_web_views_v1_user_login [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_v1_user_login [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_v1_user_logout [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_v1_user_logout [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_v2_packages_logs [fillcolor="#e53b05",minlen="2",weight="2"]; ahriman_web_apispec -> ahriman_web_views_v2_packages_logs [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_views_v2_status_info [fillcolor="#e53b05",minlen="2",weight="2"];
ahriman_web_apispec -> ahriman_web_web [fillcolor="#e53b05",weight="2"]; ahriman_web_apispec -> ahriman_web_web [fillcolor="#e53b05",weight="2"];
ahriman_web_apispec_decorators -> ahriman_web_views_v1_auditlog_events [fillcolor="#bd3104",minlen="2",weight="2"]; ahriman_web_apispec_decorators -> ahriman_web_views_v1_auditlog_events [fillcolor="#bd3104",minlen="2",weight="2"];
ahriman_web_apispec_decorators -> ahriman_web_views_v1_distributed_workers [fillcolor="#bd3104",minlen="2",weight="2"]; ahriman_web_apispec_decorators -> ahriman_web_views_v1_distributed_workers [fillcolor="#bd3104",minlen="2",weight="2"];
@@ -1760,17 +1780,19 @@ digraph G {
ahriman_web_apispec_decorators -> ahriman_web_views_v1_user_login [fillcolor="#bd3104",minlen="2",weight="2"]; ahriman_web_apispec_decorators -> ahriman_web_views_v1_user_login [fillcolor="#bd3104",minlen="2",weight="2"];
ahriman_web_apispec_decorators -> ahriman_web_views_v1_user_logout [fillcolor="#bd3104",minlen="2",weight="2"]; ahriman_web_apispec_decorators -> ahriman_web_views_v1_user_logout [fillcolor="#bd3104",minlen="2",weight="2"];
ahriman_web_apispec_decorators -> ahriman_web_views_v2_packages_logs [fillcolor="#bd3104",minlen="2",weight="2"]; ahriman_web_apispec_decorators -> ahriman_web_views_v2_packages_logs [fillcolor="#bd3104",minlen="2",weight="2"];
ahriman_web_apispec_decorators -> ahriman_web_views_v2_status_info [fillcolor="#bd3104",minlen="2",weight="2"];
ahriman_web_apispec_info -> ahriman_web_web [fillcolor="#a14f35",minlen="2",weight="2"]; ahriman_web_apispec_info -> ahriman_web_web [fillcolor="#a14f35",minlen="2",weight="2"];
ahriman_web_cors -> ahriman_web_web [fillcolor="#b0573a",weight="2"]; ahriman_web_cors -> ahriman_web_web [fillcolor="#b0573a",weight="2"];
ahriman_web_keys -> ahriman_web_apispec_info [fillcolor="#823017",minlen="2",weight="2"]; ahriman_web_keys -> ahriman_web_apispec_info [fillcolor="#823017",minlen="2",weight="2"];
ahriman_web_keys -> ahriman_web_views_base [fillcolor="#823017",minlen="2",weight="2"]; ahriman_web_keys -> ahriman_web_views_base [fillcolor="#823017",minlen="2",weight="2"];
ahriman_web_keys -> ahriman_web_web [fillcolor="#823017",weight="2"]; ahriman_web_keys -> ahriman_web_web [fillcolor="#823017",weight="2"];
ahriman_web_middlewares -> ahriman_web_views_v1_status_metrics [fillcolor="#e9410c",minlen="2",weight="2"]; ahriman_web_middlewares -> ahriman_web_views_v1_status_metrics [fillcolor="#ef3e06",minlen="2",weight="2"];
ahriman_web_middlewares -> ahriman_web_web [fillcolor="#e9410c",weight="2"]; ahriman_web_middlewares -> ahriman_web_web [fillcolor="#ef3e06",weight="2"];
ahriman_web_middlewares_auth_handler -> ahriman_web_web [fillcolor="#733826",minlen="2",weight="2"]; ahriman_web_middlewares_auth_handler -> ahriman_web_web [fillcolor="#733826",minlen="2",weight="2"];
ahriman_web_middlewares_exception_handler -> ahriman_web_web [fillcolor="#994b33",minlen="2",weight="2"]; ahriman_web_middlewares_exception_handler -> ahriman_web_web [fillcolor="#994b33",minlen="2",weight="2"];
ahriman_web_middlewares_metrics_handler -> ahriman_web_views_v1_status_metrics [fillcolor="#a34628",minlen="2",weight="2"]; ahriman_web_middlewares_metrics_handler -> ahriman_web_views_v1_status_metrics [fillcolor="#a34628",minlen="2",weight="2"];
ahriman_web_middlewares_metrics_handler -> ahriman_web_web [fillcolor="#a34628",minlen="2",weight="2"]; ahriman_web_middlewares_metrics_handler -> ahriman_web_web [fillcolor="#a34628",minlen="2",weight="2"];
ahriman_web_middlewares_request_id_handler -> ahriman_web_web [fillcolor="#8a442e",minlen="2",weight="2"];
ahriman_web_routes -> ahriman_web_web [fillcolor="#8a442e",weight="2"]; ahriman_web_routes -> ahriman_web_web [fillcolor="#8a442e",weight="2"];
ahriman_web_schemas -> ahriman_web_apispec_decorators [fillcolor="blue",minlen="2",weight="2"]; ahriman_web_schemas -> ahriman_web_apispec_decorators [fillcolor="blue",minlen="2",weight="2"];
ahriman_web_schemas -> ahriman_web_views_v1_auditlog_events [fillcolor="blue",minlen="2",weight="2"]; ahriman_web_schemas -> ahriman_web_views_v1_auditlog_events [fillcolor="blue",minlen="2",weight="2"];
@@ -1799,9 +1821,14 @@ digraph G {
ahriman_web_schemas -> ahriman_web_views_v1_status_status [fillcolor="blue",minlen="2",weight="2"]; ahriman_web_schemas -> ahriman_web_views_v1_status_status [fillcolor="blue",minlen="2",weight="2"];
ahriman_web_schemas -> ahriman_web_views_v1_user_login [fillcolor="blue",minlen="2",weight="2"]; ahriman_web_schemas -> ahriman_web_views_v1_user_login [fillcolor="blue",minlen="2",weight="2"];
ahriman_web_schemas -> ahriman_web_views_v2_packages_logs [fillcolor="blue",minlen="2",weight="2"]; ahriman_web_schemas -> ahriman_web_views_v2_packages_logs [fillcolor="blue",minlen="2",weight="2"];
ahriman_web_schemas -> ahriman_web_views_v2_status_info [fillcolor="blue",minlen="2",weight="2"];
ahriman_web_schemas_any_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_any_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
ahriman_web_schemas_aur_package_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_aur_package_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
ahriman_web_schemas_auth_info_schema -> ahriman_web_schemas [fillcolor="#c45431",weight="3"];
ahriman_web_schemas_auth_info_schema -> ahriman_web_schemas_info_v2_schema [fillcolor="#c45431",weight="3"];
ahriman_web_schemas_auth_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_auth_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
ahriman_web_schemas_auto_refresh_interval_schema -> ahriman_web_schemas [fillcolor="#c45431",weight="3"];
ahriman_web_schemas_auto_refresh_interval_schema -> ahriman_web_schemas_info_v2_schema [fillcolor="#c45431",weight="3"];
ahriman_web_schemas_build_options_schema -> ahriman_web_schemas [fillcolor="#d04e24",weight="3"]; ahriman_web_schemas_build_options_schema -> ahriman_web_schemas [fillcolor="#d04e24",weight="3"];
ahriman_web_schemas_build_options_schema -> ahriman_web_schemas_package_names_schema [fillcolor="#d04e24",weight="3"]; ahriman_web_schemas_build_options_schema -> ahriman_web_schemas_package_names_schema [fillcolor="#d04e24",weight="3"];
ahriman_web_schemas_build_options_schema -> ahriman_web_schemas_update_flags_schema [fillcolor="#d04e24",weight="3"]; ahriman_web_schemas_build_options_schema -> ahriman_web_schemas_update_flags_schema [fillcolor="#d04e24",weight="3"];
@@ -1815,6 +1842,7 @@ digraph G {
ahriman_web_schemas_event_search_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"]; ahriman_web_schemas_event_search_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
ahriman_web_schemas_file_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_file_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
ahriman_web_schemas_info_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"]; ahriman_web_schemas_info_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
ahriman_web_schemas_info_v2_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
ahriman_web_schemas_internal_status_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"]; ahriman_web_schemas_internal_status_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
ahriman_web_schemas_log_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_log_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
ahriman_web_schemas_login_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_login_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
@@ -1847,6 +1875,7 @@ digraph G {
ahriman_web_schemas_remote_schema -> ahriman_web_schemas_package_schema [fillcolor="#b44d2d",weight="3"]; ahriman_web_schemas_remote_schema -> ahriman_web_schemas_package_schema [fillcolor="#b44d2d",weight="3"];
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas [fillcolor="#ef3e06",weight="3"]; ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas [fillcolor="#ef3e06",weight="3"];
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_info_schema [fillcolor="#ef3e06",weight="3"]; ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_info_schema [fillcolor="#ef3e06",weight="3"];
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_info_v2_schema [fillcolor="#ef3e06",weight="3"];
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_internal_status_schema [fillcolor="#ef3e06",weight="3"]; ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_internal_status_schema [fillcolor="#ef3e06",weight="3"];
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_package_status_schema [fillcolor="#ef3e06",weight="3"]; ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_package_status_schema [fillcolor="#ef3e06",weight="3"];
ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_package_version_schema [fillcolor="#ef3e06",weight="3"]; ahriman_web_schemas_repository_id_schema -> ahriman_web_schemas_package_version_schema [fillcolor="#ef3e06",weight="3"];
@@ -1860,8 +1889,13 @@ digraph G {
ahriman_web_schemas_status_schema -> ahriman_web_schemas_package_status_schema [fillcolor="#ca4116",weight="3"]; ahriman_web_schemas_status_schema -> ahriman_web_schemas_package_status_schema [fillcolor="#ca4116",weight="3"];
ahriman_web_schemas_update_flags_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"]; ahriman_web_schemas_update_flags_schema -> ahriman_web_schemas [fillcolor="blue",weight="3"];
ahriman_web_schemas_worker_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"]; ahriman_web_schemas_worker_schema -> ahriman_web_schemas [fillcolor="#b85a3d",weight="3"];
ahriman_web_server_info -> ahriman_web_views_index [fillcolor="#93371a",minlen="2",weight="2"];
ahriman_web_server_info -> ahriman_web_views_v1_status_info [fillcolor="#93371a",minlen="2",weight="2"];
ahriman_web_server_info -> ahriman_web_views_v2_status_info [fillcolor="#93371a",minlen="2",weight="2"];
ahriman_web_views -> ahriman_web_routes [fillcolor="#f94810",weight="2"]; ahriman_web_views -> ahriman_web_routes [fillcolor="#f94810",weight="2"];
ahriman_web_views -> ahriman_web_server_info [fillcolor="#f94810",weight="2"];
ahriman_web_views_base -> ahriman_web_routes [fillcolor="#952603",minlen="2",weight="2"]; ahriman_web_views_base -> ahriman_web_routes [fillcolor="#952603",minlen="2",weight="2"];
ahriman_web_views_base -> ahriman_web_server_info [fillcolor="#952603",minlen="2",weight="2"];
ahriman_web_views_base -> ahriman_web_views_api_docs [fillcolor="#952603",weight="3"]; ahriman_web_views_base -> ahriman_web_views_api_docs [fillcolor="#952603",weight="3"];
ahriman_web_views_base -> ahriman_web_views_api_swagger [fillcolor="#952603",weight="3"]; ahriman_web_views_base -> ahriman_web_views_api_swagger [fillcolor="#952603",weight="3"];
ahriman_web_views_base -> ahriman_web_views_index [fillcolor="#952603",weight="3"]; ahriman_web_views_base -> ahriman_web_views_index [fillcolor="#952603",weight="3"];
@@ -1893,6 +1927,7 @@ digraph G {
ahriman_web_views_base -> ahriman_web_views_v1_user_login [fillcolor="#952603",weight="3"]; ahriman_web_views_base -> ahriman_web_views_v1_user_login [fillcolor="#952603",weight="3"];
ahriman_web_views_base -> ahriman_web_views_v1_user_logout [fillcolor="#952603",weight="3"]; ahriman_web_views_base -> ahriman_web_views_v1_user_logout [fillcolor="#952603",weight="3"];
ahriman_web_views_base -> ahriman_web_views_v2_packages_logs [fillcolor="#952603",weight="3"]; ahriman_web_views_base -> ahriman_web_views_v2_packages_logs [fillcolor="#952603",weight="3"];
ahriman_web_views_base -> ahriman_web_views_v2_status_info [fillcolor="#952603",weight="3"];
ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_changes [fillcolor="#ef3e06",weight="3"]; ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_changes [fillcolor="#ef3e06",weight="3"];
ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_dependencies [fillcolor="#ef3e06",weight="3"]; ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_dependencies [fillcolor="#ef3e06",weight="3"];
ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_logs [fillcolor="#ef3e06",weight="3"]; ahriman_web_views_status_view_guard -> ahriman_web_views_v1_packages_logs [fillcolor="#ef3e06",weight="3"];
@@ -1912,9 +1947,11 @@ digraph G {
aiohttp -> ahriman_web_middlewares_auth_handler [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_middlewares_auth_handler [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_middlewares_exception_handler [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_middlewares_exception_handler [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_middlewares_metrics_handler [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_middlewares_metrics_handler [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_middlewares_request_id_handler [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_routes [fillcolor="#f9b506",minlen="2"]; aiohttp -> ahriman_web_routes [fillcolor="#f9b506",minlen="2"];
aiohttp -> ahriman_web_views_api_swagger [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_api_swagger [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_base [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_base [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_index [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_static [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_static [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_v1_auditlog_events [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_v1_auditlog_events [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_v1_distributed_workers [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_v1_distributed_workers [fillcolor="#f9b506",minlen="3"];
@@ -1943,6 +1980,7 @@ digraph G {
aiohttp -> ahriman_web_views_v1_user_login [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_v1_user_login [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_v1_user_logout [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_v1_user_logout [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_v2_packages_logs [fillcolor="#f9b506",minlen="3"]; aiohttp -> ahriman_web_views_v2_packages_logs [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_views_v2_status_info [fillcolor="#f9b506",minlen="3"];
aiohttp -> ahriman_web_web [fillcolor="#f9b506",minlen="2"]; aiohttp -> ahriman_web_web [fillcolor="#f9b506",minlen="2"];
aiohttp -> aiohttp_cors [fillcolor="#f9b506",minlen="2"]; aiohttp -> aiohttp_cors [fillcolor="#f9b506",minlen="2"];
aiohttp -> aiohttp_jinja2 [fillcolor="#f9b506",minlen="2"]; aiohttp -> aiohttp_jinja2 [fillcolor="#f9b506",minlen="2"];

View File

@@ -28,6 +28,14 @@ ahriman.core.log.lazy\_logging module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.log.log\_context module
------------------------------------
.. automodule:: ahriman.core.log.log_context
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.log.log\_loader module ahriman.core.log.log\_loader module
----------------------------------- -----------------------------------

View File

@@ -28,6 +28,14 @@ ahriman.web.middlewares.metrics\_handler module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.middlewares.request\_id\_handler module
---------------------------------------------------
.. automodule:: ahriman.web.middlewares.request_id_handler
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@@ -39,6 +39,14 @@ ahriman.web.routes module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.server\_info module
-------------------------------
.. automodule:: ahriman.web.server_info
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.web module ahriman.web.web module
---------------------- ----------------------

View File

@@ -20,6 +20,14 @@ ahriman.web.schemas.aur\_package\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.auth\_info\_schema module
---------------------------------------------
.. automodule:: ahriman.web.schemas.auth_info_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.auth\_schema module ahriman.web.schemas.auth\_schema module
--------------------------------------- ---------------------------------------
@@ -28,6 +36,14 @@ ahriman.web.schemas.auth\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.auto\_refresh\_interval\_schema module
----------------------------------------------------------
.. automodule:: ahriman.web.schemas.auto_refresh_interval_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.build\_options\_schema module ahriman.web.schemas.build\_options\_schema module
------------------------------------------------- -------------------------------------------------
@@ -108,6 +124,14 @@ ahriman.web.schemas.info\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.info\_v2\_schema module
-------------------------------------------
.. automodule:: ahriman.web.schemas.info_v2_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.internal\_status\_schema module ahriman.web.schemas.internal\_status\_schema module
--------------------------------------------------- ---------------------------------------------------

View File

@@ -8,6 +8,7 @@ Subpackages
:maxdepth: 4 :maxdepth: 4
ahriman.web.views.v2.packages ahriman.web.views.v2.packages
ahriman.web.views.v2.status
Module contents Module contents
--------------- ---------------

View File

@@ -0,0 +1,21 @@
ahriman.web.views.v2.status package
===================================
Submodules
----------
ahriman.web.views.v2.status.info module
---------------------------------------
.. automodule:: ahriman.web.views.v2.status.info
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.web.views.v2.status
:members:
:no-undoc-members:
:show-inheritance:

View File

@@ -9,7 +9,8 @@ Packages have strict rules of importing:
* ``ahriman.application`` package must not be used outside of this package. * ``ahriman.application`` package must not be used outside of this package.
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``, but it is impossible at the moment. * ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``, but it is impossible at the moment.
* ``ahriman.web`` package is allowed to be imported from ``ahriman.application`` (web handler only, only ``ahriman.web.web`` methods). * ``ahriman.web`` package is allowed to be imported from ``ahriman.application`` (web handler only, only ``ahriman.web.web`` methods).
* The idea remains the same for all imports, if an package requires some specific dependencies, it must be imported locally to keep dependencies optional.
The idea remains the same for all imports, if a package requires some specific dependencies, it must be imported locally to keep dependencies optional.
Full dependency diagram: Full dependency diagram:
@@ -42,7 +43,7 @@ This package contains everything required for the most of application actions an
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly. * ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.housekeeping`` package provides few triggers for removing old data. * ``ahriman.core.housekeeping`` package provides few triggers for removing old data.
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes. * ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and some wrappers. * ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger, log context for injecting context variables into log records and some wrappers.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly. * ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation. * ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available). * ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
@@ -85,6 +86,7 @@ Application run
#. Call ``Handler.execute`` method. #. Call ``Handler.execute`` method.
#. Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or continue in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether the application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Config``, which utilizes stdout to print messages). #. Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or continue in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether the application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Config``, which utilizes stdout to print messages).
#. In each child process call lock functions. #. In each child process call lock functions.
#. Load configuration and install logging.
#. After success checks pass control to ``Handler.run`` method defined by specific handler class. #. After success checks pass control to ``Handler.run`` method defined by specific handler class.
#. Return result (success or failure) of each subprocess and exit from application. #. Return result (success or failure) of each subprocess and exit from application.
#. Some handlers may override their status and throw ``ExitCode`` exception. This exception is just silently suppressed and changes application exit code to ``1``. #. Some handlers may override their status and throw ``ExitCode`` exception. This exception is just silently suppressed and changes application exit code to ``1``.
@@ -159,12 +161,12 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
├── aur.files -> aur.files.tar.gz ├── aur.files -> aur.files.tar.gz
└── aur.files.tar.gz └── aur.files.tar.gz
There are multiple subdirectories, some of them are commons for any repository, but some of them are not. There are multiple subdirectories, some of them are common for any repository, but some of them are not.
* ``archive`` is the package archive directory. It is common for all repositories and architectures and contains two subdirectories: * ``archive`` is the package archive directory. It is common for all repositories and architectures and contains two subdirectories:
* ``archive/packages/{first_letter}/{package_base}`` stores the actual built package files and their signatures. * ``archive/packages/{first_letter}/{package_base}`` stores the actual built package files and their signatures.
* ``archive/repos/{YYYY}/{MM}/{DD}/{repository}/{architecture}`` contains daily repository snapshots. Each snapshot is a repository database with symlinks pointing to the corresponding packages in the ``archive/packages`` tree. * ``archive/repos/{YYYY}/{MM}/{DD}/{repository}/{architecture}`` contains daily repository snapshots. Each snapshot is a repository database with symlinks pointing to the corresponding packages in the ``archive/packages`` tree. These directories only appear if ``ahriman.core.archive.ArchiveTrigger`` is enabled.
The archive also allows the build process to skip rebuilding a package if a matching version already exists. The archive also allows the build process to skip rebuilding a package if a matching version already exists.
@@ -239,9 +241,9 @@ Check outdated packages
There are few ways for packages to be marked as out-of-date and hence requiring rebuild. Those are following: There are few ways for packages to be marked as out-of-date and hence requiring rebuild. Those are following:
#. User requested update of the package. It can be caused by calling ``package-add`` subcommand (or ``package-update`` with arguments). #. User requested update of the package. It can be caused by calling ``package-add`` subcommand (or ``package-update`` with arguments).
#. The most common way for packages to be marked as out-of-dated is that the version in AUR (or the official repositories) is newer than in the repository. #. The most common way for packages to be marked as out-of-date is that the version in AUR (or the official repositories) is newer than in the repository.
#. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one. #. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one.
#. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-dated as well. #. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-date as well.
Update packages Update packages
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
@@ -255,6 +257,7 @@ This feature is divided into the following stages: check AUR for updates and run
#. Download package data from AUR. #. Download package data from AUR.
#. Bump ``pkgrel`` if there is duplicate version in the local repository (see explanation below). #. Bump ``pkgrel`` if there is duplicate version in the local repository (see explanation below).
#. Check if there is already built package of the same version in archive (cross-repository support). If so, then just copy built archives and skip steps below.
#. Build every package in clean chroot. #. Build every package in clean chroot.
#. Sign packages if required. #. Sign packages if required.
#. Add packages to database and sign database if required. #. Add packages to database and sign database if required.
@@ -313,7 +316,7 @@ Having the initial dependencies tree, the application is looking for packages wh
Those paths are also filtered by regular expressions set in the configuration. Those paths are also filtered by regular expressions set in the configuration.
All those implicit dependencies are stored in the database and extracted on each check. In case if any of the repository packages doesn't contain any entry anymore (e.g. so version has been changed or modules directory has been changed), the dependent package will be marked as out-of-dated. All those implicit dependencies are stored in the database and extracted on each check. In case if any of the repository packages doesn't contain any entry anymore (e.g. so version has been changed or modules directory has been changed), the dependent package will be marked as out-of-date.
Core functions reference Core functions reference
------------------------ ------------------------
@@ -344,6 +347,8 @@ The ``_Context`` class itself mimics default collection interface (as is ``Mappi
In order to provide statically typed interface, the ``ahriman.models.context_key.ContextKey`` class is used for both ``_Content.get`` and ``_Content.set`` methods; the context instance itself, however, does not store information about types. In order to provide statically typed interface, the ``ahriman.models.context_key.ContextKey`` class is used for both ``_Content.get`` and ``_Content.set`` methods; the context instance itself, however, does not store information about types.
Logging module has its own context variables, which are required to be registered in advance to avoid possible race conditions.
Submodules Submodules
^^^^^^^^^^ ^^^^^^^^^^
@@ -370,7 +375,7 @@ Passwords must be stored in database as ``hash(password + salt)``, where ``passw
means that there is user ``username`` with ``read`` access and password ``password`` hashed by ``sha512`` with salt ``salt``. means that there is user ``username`` with ``read`` access and password ``password`` hashed by ``sha512`` with salt ``salt``.
OAuth provider uses library definitions (``aioauth-client``) in order *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form. OAuth provider uses library definitions (``aioauth-client``) in order to *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature can be used by service reporting (aka robots). OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature can be used by service reporting (aka robots).
@@ -383,7 +388,7 @@ Triggers
Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit. Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit.
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it. The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join the remaining part by ``.`` and interpret as module path, import module and extract attribute from it.
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``list[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs. The loaded triggers will be called with ``ahriman.models.result.Result`` and ``list[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
@@ -407,7 +412,7 @@ PKGBUILD parsing
The application provides a house-made shell parser ``ahriman.core.alpm.pkgbuild_parser.PkgbuildParser`` to process PKGBUILDs and extract package data from them. It relies on the ``shlex.shlex`` parser with some configuration tweaks and adds some token post-processing. The application provides a house-made shell parser ``ahriman.core.alpm.pkgbuild_parser.PkgbuildParser`` to process PKGBUILDs and extract package data from them. It relies on the ``shlex.shlex`` parser with some configuration tweaks and adds some token post-processing.
#. During the parser process, firstly, it extract next token from the source file (basically, the word) and tries to match it to the variable assignment. If so, then just processes accordingly. #. During the parser process, firstly, it extracts the next token from the source file (basically, the word) and tries to match it to the variable assignment. If so, then just processes accordingly.
#. If it is not an assignment, the parser checks if the token was quoted. #. If it is not an assignment, the parser checks if the token was quoted.
#. If it wasn't quoted then the parser tries to match the array starts (two consecutive tokens like ``array=`` and ``(``) or it is function (``function``, ``()`` and ``{``). #. If it wasn't quoted then the parser tries to match the array starts (two consecutive tokens like ``array=`` and ``(``) or it is function (``function``, ``()`` and ``{``).
#. The arrays are processed until the next closing bracket ``)``. After extraction, the parser tries to expand an array according to bash rules (``prefix{first,second}suffix`` constructions). #. The arrays are processed until the next closing bracket ``)``. After extraction, the parser tries to expand an array according to bash rules (``prefix{first,second}suffix`` constructions).
@@ -445,7 +450,7 @@ Web application requires the following python packages to be installed:
Middlewares Middlewares
^^^^^^^^^^^ ^^^^^^^^^^^
Service provides some custom middlewares, e.g. logging every exception (except for user ones) and user authorization. Service provides some custom middlewares, e.g. logging every exception (except for user ones), user authorization and request tracing via ``X-Request-ID`` header.
HEAD and OPTIONS requests HEAD and OPTIONS requests
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -457,7 +462,7 @@ On the other side, ``OPTIONS`` method is implemented in the ``ahriman.web.middle
Web views Web views
^^^^^^^^^ ^^^^^^^^^
All web views are defined in separated package and derived from ``ahriman.web.views.base.Base`` class which provides typed interfaces for web application. All web views are defined in a separate package and derived from ``ahriman.web.views.base.Base`` class which provides typed interfaces for web application.
REST API supports only JSON data. REST API supports only JSON data.
@@ -476,7 +481,7 @@ The views are also divided by supporting API versions (e.g. ``v1``, ``v2``).
Templating Templating
^^^^^^^^^^ ^^^^^^^^^^
Package provides base jinja templates which can be overridden by settings. Vanilla templates actively use bootstrap library. Package provides base jinja templates which can be overridden by settings. The default web interface is a React application. The classic bootstrap-based template is still available as ``build-status-classic.jinja2`` and can be enabled via the ``web.template`` configuration option.
Requests and scopes Requests and scopes
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^

View File

@@ -118,7 +118,6 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
* ``cookie_secret_key`` - secret key which will be used for cookies encryption, string, optional. It must be 32 bytes URL-safe base64-encoded and can be generated as following ``base64.urlsafe_b64encode(os.urandom(32)).decode("utf8")``. If not set, it will be generated automatically; note, however, that in this case, all sessions will be automatically invalidated during the service restart. * ``cookie_secret_key`` - secret key which will be used for cookies encryption, string, optional. It must be 32 bytes URL-safe base64-encoded and can be generated as following ``base64.urlsafe_b64encode(os.urandom(32)).decode("utf8")``. If not set, it will be generated automatically; note, however, that in this case, all sessions will be automatically invalidated during the service restart.
* ``full_access_group`` - name of the secondary group (e.g. ``wheel``) to be used as admin group in the service, string, required in case if ``pam`` is used. * ``full_access_group`` - name of the secondary group (e.g. ``wheel``) to be used as admin group in the service, string, required in case if ``pam`` is used.
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service in seconds, integer, optional, default is 7 days. * ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service in seconds, integer, optional, default is 7 days.
* ``oauth_icon`` - OAuth2 login button icon, string, optional, default is ``google``. Must be valid `Bootstrap icon <https://icons.getbootstrap.com/>`__ name.
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used. * ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used. * ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
* ``permit_root_login`` - allow login as root user, boolean, optional, default ``no``. * ``permit_root_login`` - allow login as root user, boolean, optional, default ``no``.
@@ -188,6 +187,7 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
* ``port`` - port to bind, integer, optional. * ``port`` - port to bind, integer, optional.
* ``service_only`` - disable status routes (including logs), boolean, optional, default ``no``. * ``service_only`` - disable status routes (including logs), boolean, optional, default ``no``.
* ``static_path`` - path to directory with static files, string, required. * ``static_path`` - path to directory with static files, string, required.
* ``template`` - Jinja2 template name for the index page, string, optional, default ``build-status.jinja2``.
* ``templates`` - path to templates directories, space separated list of paths, required. * ``templates`` - path to templates directories, space separated list of paths, required.
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization. * ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration. * ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.

77
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,77 @@
import js from "@eslint/js";
import stylistic from "@stylistic/eslint-plugin";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
files: ["src/**/*.{ts,tsx}"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
"simple-import-sort": simpleImportSort,
"@stylistic": stylistic,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
// imports
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error",
// core
"curly": "error",
"eqeqeq": "error",
"no-console": "error",
"no-eval": "error",
// stylistic
"@stylistic/array-bracket-spacing": ["error", "never"],
"@stylistic/arrow-parens": ["error", "as-needed"],
"@stylistic/brace-style": ["error", "1tbs"],
"@stylistic/comma-dangle": ["error", "always-multiline"],
"@stylistic/comma-spacing": ["error", { before: false, after: true }],
"@stylistic/eol-last": ["error", "always"],
"@stylistic/indent": ["error", 4],
"@stylistic/jsx-curly-brace-presence": ["error", { props: "never", children: "never" }],
"@stylistic/jsx-quotes": ["error", "prefer-double"],
"@stylistic/jsx-self-closing-comp": ["error", { component: true, html: true }],
"@stylistic/max-len": ["error", {
code: 120,
ignoreComments: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
}],
"@stylistic/member-delimiter-style": ["error", { multiline: { delimiter: "semi" }, singleline: { delimiter: "semi" } }],
"@stylistic/no-extra-parens": ["error", "all"],
"@stylistic/no-multi-spaces": "error",
"@stylistic/no-multiple-empty-lines": ["error", { max: 1 }],
"@stylistic/no-trailing-spaces": "error",
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/quotes": ["error", "double"],
"@stylistic/semi": ["error", "always"],
// typescript
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
"@typescript-eslint/no-deprecated": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
},
},
);

16
frontend/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ahriman</title>
<link rel="icon" href="/static/favicon.ico" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

42
frontend/package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "ahriman-frontend",
"private": true,
"type": "module",
"version": "2.20.0-rc7",
"scripts": {
"build": "tsc && vite build",
"dev": "vite",
"lint": "eslint src/",
"lint:fix": "eslint --fix src/",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.9",
"@mui/material": "^7.3.9",
"@mui/x-data-grid": "^8.27.4",
"@tanstack/react-query": "^5.90.21",
"chart.js": "^4.5.1",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
"react-syntax-highlighter": "^16.1.1"
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.39.3",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^6.1.1"
}
}

55
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,55 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material/styles";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import AppLayout from "components/layout/AppLayout";
import { AuthProvider } from "contexts/AuthProvider";
import { ClientProvider } from "contexts/ClientProvider";
import { NotificationProvider } from "contexts/NotificationProvider";
import { RepositoryProvider } from "contexts/RepositoryProvider";
import type React from "react";
import Theme from "theme/Theme";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
},
},
});
export default function App(): React.JSX.Element {
return <QueryClientProvider client={queryClient}>
<ThemeProvider theme={Theme}>
<CssBaseline />
<NotificationProvider>
<ClientProvider>
<AuthProvider>
<RepositoryProvider>
<AppLayout />
</RepositoryProvider>
</AuthProvider>
</ClientProvider>
</NotificationProvider>
</ThemeProvider>
</QueryClientProvider>;
}

View File

@@ -0,0 +1,37 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Client } from "api/client/Client";
import { FetchClient } from "api/client/FetchClient";
import { ServiceClient } from "api/client/ServiceClient";
import type { LoginRequest } from "models/LoginRequest";
export class AhrimanClient extends Client {
readonly fetch = new FetchClient(this);
readonly service = new ServiceClient(this);
async login(data: LoginRequest): Promise<void> {
return this.request("/api/v1/login", { method: "POST", json: data });
}
async logout(): Promise<void> {
return this.request("/api/v1/logout", { method: "POST" });
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export class ApiError extends Error {
status: number;
statusText: string;
body: string;
constructor(status: number, statusText: string, body: string) {
super(`${status} ${statusText}`);
this.status = status;
this.statusText = statusText;
this.body = body;
}
get detail(): string {
try {
const parsed = JSON.parse(this.body) as Record<string, string>;
return parsed.error ?? (this.body || this.message);
} catch {
return this.body || this.message;
}
}
static errorDetail(exception: unknown): string {
return exception instanceof ApiError ? exception.detail : String(exception);
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { ApiError } from "api/client/ApiError";
import type { RequestOptions } from "api/client/RequestOptions";
export class Client {
private static readonly DEFAULT_TIMEOUT = 30_000;
async request<T>(url: string, options: RequestOptions = {}): Promise<T> {
const { method, query, json, timeout = Client.DEFAULT_TIMEOUT } = options;
let fullUrl = url;
if (query) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && value !== null) {
params.set(key, String(value));
}
}
fullUrl += `?${params.toString()}`;
}
const headers: Record<string, string> = {
Accept: "application/json",
"X-Request-ID": crypto.randomUUID(),
};
if (json !== undefined) {
headers["Content-Type"] = "application/json";
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const requestInit: RequestInit = {
method: method ?? (json ? "POST" : "GET"),
headers,
signal: controller.signal,
};
if (json !== undefined) {
requestInit.body = JSON.stringify(json);
}
let response: Response;
try {
response = await fetch(fullUrl, requestInit);
} finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
const body = await response.text();
throw new ApiError(response.status, response.statusText, body);
}
const contentType = response.headers.get("Content-Type") ?? "";
if (!contentType.includes("application/json")) {
return undefined as T;
}
return await response.json() as T;
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { Client } from "api/client/Client";
import type { Changes } from "models/Changes";
import type { Dependencies } from "models/Dependencies";
import type { Event } from "models/Event";
import type { InfoResponse } from "models/InfoResponse";
import type { InternalStatus } from "models/InternalStatus";
import type { LogRecord } from "models/LogRecord";
import type { PackageStatus } from "models/PackageStatus";
import type { Patch } from "models/Patch";
import { RepositoryId } from "models/RepositoryId";
export class FetchClient {
protected client: Client;
constructor(client: Client) {
this.client = client;
}
async fetchPackage(packageBase: string, repository: RepositoryId): Promise<PackageStatus[]> {
return this.client.request<PackageStatus[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}`, {
query: repository.toQuery(),
});
}
async fetchPackageChanges(packageBase: string, repository: RepositoryId): Promise<Changes> {
return this.client.request<Changes>(`/api/v1/packages/${encodeURIComponent(packageBase)}/changes`, {
query: repository.toQuery(),
});
}
async fetchPackageDependencies(packageBase: string, repository: RepositoryId): Promise<Dependencies> {
return this.client.request<Dependencies>(`/api/v1/packages/${encodeURIComponent(packageBase)}/dependencies`, {
query: repository.toQuery(),
});
}
async fetchPackageEvents(repository: RepositoryId, objectId?: string, limit?: number): Promise<Event[]> {
const query: Record<string, string | number> = repository.toQuery();
if (objectId) {
query.object_id = objectId;
}
if (limit) {
query.limit = limit;
}
return this.client.request<Event[]>("/api/v1/events", { query });
}
async fetchPackageLogs(
packageBase: string,
repository: RepositoryId,
version?: string,
processId?: string,
head?: boolean,
): Promise<LogRecord[]> {
const query: Record<string, string | boolean> = { ...repository.toQuery() };
if (version) {
query.version = version;
}
if (processId) {
query.process_id = processId;
}
if (head) {
query.head = true;
}
return this.client.request<LogRecord[]>(`/api/v2/packages/${encodeURIComponent(packageBase)}/logs`, { query });
}
async fetchPackagePatches(packageBase: string): Promise<Patch[]> {
return this.client.request<Patch[]>(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches`);
}
async fetchPackages(repository: RepositoryId): Promise<PackageStatus[]> {
return this.client.request<PackageStatus[]>("/api/v1/packages", { query: repository.toQuery() });
}
async fetchServerInfo(): Promise<InfoResponse> {
const info = await this.client.request<InfoResponse>("/api/v2/info");
return {
...info,
repositories: info.repositories.map(repo =>
new RepositoryId(repo.architecture, repo.repository),
),
};
}
async fetchServerStatus(repository: RepositoryId): Promise<InternalStatus> {
return this.client.request<InternalStatus>("/api/v1/status", { query: repository.toQuery() });
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface RequestOptions {
method?: string;
query?: Record<string, string | number | boolean>;
json?: unknown;
timeout?: number;
}

View File

@@ -0,0 +1,88 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { Client } from "api/client/Client";
import type { AURPackage } from "models/AURPackage";
import type { PackageActionRequest } from "models/PackageActionRequest";
import type { PGPKey } from "models/PGPKey";
import type { PGPKeyRequest } from "models/PGPKeyRequest";
import type { RepositoryId } from "models/RepositoryId";
export class ServiceClient {
protected client: Client;
constructor(client: Client) {
this.client = client;
}
async servicePackageAdd(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
return this.client.request("/api/v1/service/add", { method: "POST", query: repository.toQuery(), json: data });
}
async servicePackagePatchRemove(packageBase: string, key: string): Promise<void> {
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/patches/${encodeURIComponent(key)}`, {
method: "DELETE",
});
}
async servicePackageRemove(repository: RepositoryId, packages: string[]): Promise<void> {
return this.client.request("/api/v1/service/remove", {
method: "POST",
query: repository.toQuery(),
json: { packages },
});
}
async servicePackageRequest(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
return this.client.request("/api/v1/service/request", {
method: "POST",
query: repository.toQuery(),
json: data,
});
}
async servicePackageSearch(query: string): Promise<AURPackage[]> {
return this.client.request<AURPackage[]>("/api/v1/service/search", { query: { for: query } });
}
async servicePackageUpdate(repository: RepositoryId, data: PackageActionRequest): Promise<void> {
return this.client.request("/api/v1/service/update", {
method: "POST",
query: repository.toQuery(),
json: data,
});
}
async servicePGPFetch(key: string, server: string): Promise<PGPKey> {
return this.client.request<PGPKey>("/api/v1/service/pgp", { query: { key, server } });
}
async servicePGPImport(data: PGPKeyRequest): Promise<void> {
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
}
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
return this.client.request("/api/v1/service/rebuild", {
method: "POST",
query: repository.toQuery(),
json: { packages },
});
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import {
ArcElement,
BarElement,
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
LineElement,
PointElement,
Tooltip,
} from "chart.js";
ChartJS.register(
ArcElement,
BarElement,
CategoryScale,
Legend,
LinearScale,
LineElement,
PointElement,
Tooltip,
);

View File

@@ -0,0 +1,43 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { Event } from "models/Event";
import type React from "react";
import { Line } from "react-chartjs-2";
interface EventDurationLineChartProps {
events: Event[];
}
export default function EventDurationLineChart({ events }: EventDurationLineChartProps): React.JSX.Element {
const updateEvents = events.filter(event => event.event === "package-updated");
const data = {
labels: updateEvents.map(event => new Date(event.created * 1000).toISOStringShort()),
datasets: [
{
label: "update duration, s",
data: updateEvents.map(event => event.data?.took ?? 0),
cubicInterpolationMode: "monotone" as const,
tension: 0.4,
},
],
};
return <Line data={data} options={{ responsive: true }} />;
}

View File

@@ -0,0 +1,55 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { blue, indigo } from "@mui/material/colors";
import type { RepositoryStats } from "models/RepositoryStats";
import type React from "react";
import { Bar } from "react-chartjs-2";
interface PackageCountBarChartProps {
stats: RepositoryStats;
}
export default function PackageCountBarChart({ stats }: PackageCountBarChartProps): React.JSX.Element {
return <Bar
data={{
labels: ["packages"],
datasets: [
{
label: "bases",
data: [stats.bases ?? 0],
backgroundColor: indigo[300],
},
{
label: "archives",
data: [stats.packages ?? 0],
backgroundColor: blue[500],
},
],
}}
options={{
maintainAspectRatio: false,
responsive: true,
scales: {
x: { stacked: true },
y: { stacked: false },
},
}}
/>;
}

View File

@@ -0,0 +1,44 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { BuildStatus } from "models/BuildStatus";
import type { Counters } from "models/Counters";
import type React from "react";
import { Pie } from "react-chartjs-2";
import { StatusColors } from "theme/StatusColors";
interface StatusPieChartProps {
counters: Counters;
}
export default function StatusPieChart({ counters }: StatusPieChartProps): React.JSX.Element {
const labels = ["unknown", "pending", "building", "failed", "success"] as BuildStatus[];
const data = {
labels: labels,
datasets: [
{
label: "packages in status",
data: labels.map(label => counters[label]),
backgroundColor: labels.map(label => StatusColors[label]),
},
],
};
return <Pie data={data} options={{ responsive: true }} />;
}

View File

@@ -0,0 +1,90 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import CheckIcon from "@mui/icons-material/Check";
import TimerIcon from "@mui/icons-material/Timer";
import TimerOffIcon from "@mui/icons-material/TimerOff";
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from "@mui/material";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import React, { useState } from "react";
interface AutoRefreshControlProps {
intervals: AutoRefreshInterval[];
currentInterval: number;
onIntervalChange: (interval: number) => void;
}
export default function AutoRefreshControl({
intervals,
currentInterval,
onIntervalChange,
}: AutoRefreshControlProps): React.JSX.Element | null {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
if (intervals.length === 0) {
return null;
}
const enabled = currentInterval > 0;
return <>
<Tooltip title="Auto-refresh">
<IconButton
size="small"
onClick={event => setAnchorEl(event.currentTarget)}
color={enabled ? "primary" : "default"}
>
{enabled ? <TimerIcon fontSize="small" /> : <TimerOffIcon fontSize="small" />}
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem
selected={!enabled}
onClick={() => {
onIntervalChange(0);
setAnchorEl(null);
}}
>
<ListItemIcon>
{!enabled && <CheckIcon fontSize="small" />}
</ListItemIcon>
<ListItemText>Off</ListItemText>
</MenuItem>
{intervals.map(interval =>
<MenuItem
key={interval.interval}
selected={enabled && interval.interval === currentInterval}
onClick={() => {
onIntervalChange(interval.interval);
setAnchorEl(null);
}}
>
<ListItemIcon>
{enabled && interval.interval === currentInterval && <CheckIcon fontSize="small" />}
</ListItemIcon>
<ListItemText>{interval.text}</ListItemText>
</MenuItem>,
)}
</Menu>
</>;
}

View File

@@ -0,0 +1,63 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box } from "@mui/material";
import CopyButton from "components/common/CopyButton";
import React, { type RefObject } from "react";
interface CodeBlockProps {
preRef?: RefObject<HTMLElement | null>;
getText: () => string;
height?: number | string;
onScroll?: () => void;
wordBreak?: boolean;
}
export default function CodeBlock({
preRef,
getText,
height,
onScroll,
wordBreak,
}: CodeBlockProps): React.JSX.Element {
return <Box sx={{ position: "relative" }}>
<Box
ref={preRef}
component="pre"
onScroll={onScroll}
sx={{
backgroundColor: "grey.100",
p: 2,
borderRadius: 1,
overflow: "auto",
height,
fontSize: "0.8rem",
fontFamily: "monospace",
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
}}
>
<code>
{getText()}
</code>
</Box>
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
<CopyButton getText={getText} />
</Box>
</Box>;
}

View File

@@ -0,0 +1,47 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import CheckIcon from "@mui/icons-material/Check";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { IconButton, Tooltip } from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
interface CopyButtonProps {
getText: () => string;
}
export default function CopyButton({ getText }: CopyButtonProps): React.JSX.Element {
const [copied, setCopied] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => () => clearTimeout(timer.current), []);
const handleCopy: () => Promise<void> = async () => {
await navigator.clipboard.writeText(getText());
setCopied(true);
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 2000);
};
return <Tooltip title={copied ? "Copied!" : "Copy"}>
<IconButton size="small" aria-label={copied ? "Copied" : "Copy"} onClick={() => void handleCopy()}>
{copied ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
</IconButton>
</Tooltip>;
}

View File

@@ -0,0 +1,37 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import CloseIcon from "@mui/icons-material/Close";
import { DialogTitle, IconButton, type SxProps, type Theme } from "@mui/material";
import type React from "react";
interface DialogHeaderProps {
children: React.ReactNode;
onClose: () => void;
sx?: SxProps<Theme>;
}
export default function DialogHeader({ children, onClose, sx }: DialogHeaderProps): React.JSX.Element {
return <DialogTitle sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", ...sx }}>
{children}
<IconButton aria-label="Close" onClick={onClose} size="small" sx={{ color: "inherit" }}>
<CloseIcon />
</IconButton>
</DialogTitle>;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
import { Alert, Slide } from "@mui/material";
import type { Notification } from "models/Notification";
import React, { useEffect, useState } from "react";
interface NotificationItemProps {
notification: Notification;
onClose: (id: string) => void;
}
export default function NotificationItem({ notification, onClose }: NotificationItemProps): React.JSX.Element {
const [show, setShow] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setShow(false), 5000);
return () => clearTimeout(timer);
}, []);
return (
<Slide direction="down" in={show} mountOnEnter unmountOnExit onExited={() => onClose(notification.id)}>
<Alert
onClose={() => setShow(false)}
severity={notification.severity}
variant="filled"
sx={{ width: "100%", pointerEvents: "auto" }}
>
<strong>{notification.title}</strong>
{notification.message && ` - ${notification.message}`}
</Alert>
</Slide>
);
}

View File

@@ -0,0 +1,44 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { useRepository } from "hooks/useRepository";
import type { SelectedRepositoryResult } from "hooks/useSelectedRepository";
import type React from "react";
export default function RepositorySelect({
repositorySelect,
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
const { repositories, current } = useRepository();
return <FormControl fullWidth margin="normal">
<InputLabel>repository</InputLabel>
<Select
value={repositorySelect.selectedKey || (current?.key ?? "")}
label="repository"
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
>
{repositories.map(repository =>
<MenuItem key={repository.key} value={repository.key}>
{repository.label}
</MenuItem>,
)}
</Select>
</FormControl>;
}

View File

@@ -0,0 +1,103 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box, Dialog, DialogContent, Grid, Typography } from "@mui/material";
import { skipToken, useQuery } from "@tanstack/react-query";
import PackageCountBarChart from "components/charts/PackageCountBarChart";
import StatusPieChart from "components/charts/StatusPieChart";
import DialogHeader from "components/common/DialogHeader";
import { QueryKeys } from "hooks/QueryKeys";
import { useClient } from "hooks/useClient";
import { useRepository } from "hooks/useRepository";
import type { InternalStatus } from "models/InternalStatus";
import type React from "react";
import { StatusHeaderStyles } from "theme/StatusColors";
interface DashboardDialogProps {
open: boolean;
onClose: () => void;
}
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
const client = useClient();
const { current } = useRepository();
const { data: status } = useQuery<InternalStatus>({
queryKey: current ? QueryKeys.status(current) : ["status"],
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
enabled: open,
});
const headerStyle = status ? StatusHeaderStyles[status.status.status] : {};
return <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<DialogHeader onClose={onClose} sx={headerStyle}>
System health
</DialogHeader>
<DialogContent>
{status &&
<>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2" color="text.secondary" align="right">Repository name</Typography>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2">{status.repository}</Typography>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2" color="text.secondary" align="right">Repository architecture</Typography>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2">{status.architecture}</Typography>
</Grid>
</Grid>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2" color="text.secondary" align="right">Current status</Typography>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2">{status.status.status}</Typography>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2" color="text.secondary" align="right">Updated at</Typography>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<Typography variant="body2">{new Date(status.status.timestamp * 1000).toISOStringShort()}</Typography>
</Grid>
</Grid>
<Grid container spacing={2} sx={{ mt: 2 }}>
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ height: 300 }}>
<PackageCountBarChart stats={status.stats} />
</Box>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ height: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
<StatusPieChart counters={status.packages} />
</Box>
</Grid>
</Grid>
</>
}
</DialogContent>
</Dialog>;
}

View File

@@ -0,0 +1,118 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
TextField,
} from "@mui/material";
import { ApiError } from "api/client/ApiError";
import CodeBlock from "components/common/CodeBlock";
import DialogHeader from "components/common/DialogHeader";
import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification";
import React, { useState } from "react";
interface KeyImportDialogProps {
open: boolean;
onClose: () => void;
}
export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps): React.JSX.Element {
const client = useClient();
const { showSuccess, showError } = useNotification();
const [fingerprint, setFingerprint] = useState("");
const [server, setServer] = useState("keyserver.ubuntu.com");
const [keyBody, setKeyBody] = useState("");
const handleClose = (): void => {
setFingerprint("");
setServer("keyserver.ubuntu.com");
setKeyBody("");
onClose();
};
const handleFetch: () => Promise<void> = async () => {
if (!fingerprint || !server) {
return;
}
try {
const result = await client.service.servicePGPFetch(fingerprint, server);
setKeyBody(result.key);
} catch (exception) {
const detail = ApiError.errorDetail(exception);
showError("Action failed", `Could not fetch key: ${detail}`);
}
};
const handleImport: () => Promise<void> = async () => {
if (!fingerprint || !server) {
return;
}
try {
await client.service.servicePGPImport({ key: fingerprint, server });
handleClose();
showSuccess("Success", `Key ${fingerprint} has been imported`);
} catch (exception) {
const detail = ApiError.errorDetail(exception);
showError("Action failed", `Could not import key ${fingerprint} from ${server}: ${detail}`);
}
};
return <Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
<DialogHeader onClose={handleClose}>
Import key from PGP server
</DialogHeader>
<DialogContent>
<TextField
label="fingerprint"
placeholder="PGP key fingerprint"
fullWidth
margin="normal"
value={fingerprint}
onChange={event => setFingerprint(event.target.value)}
/>
<TextField
label="key server"
placeholder="PGP key server"
fullWidth
margin="normal"
value={server}
onChange={event => setServer(event.target.value)}
/>
{keyBody &&
<Box sx={{ mt: 2 }}>
<CodeBlock getText={() => keyBody} height={300} />
</Box>
}
</DialogContent>
<DialogActions>
<Button onClick={() => void handleImport()} variant="contained" startIcon={<PlayArrowIcon />}>import</Button>
<Button onClick={() => void handleFetch()} variant="contained" color="success" startIcon={<RefreshIcon />}>fetch</Button>
</DialogActions>
</Dialog>;
}

View File

@@ -0,0 +1,118 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import PersonIcon from "@mui/icons-material/Person";
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import {
Button,
Dialog,
DialogActions,
DialogContent,
IconButton,
InputAdornment,
TextField,
} from "@mui/material";
import { ApiError } from "api/client/ApiError";
import DialogHeader from "components/common/DialogHeader";
import { useAuth } from "hooks/useAuth";
import { useNotification } from "hooks/useNotification";
import React, { useState } from "react";
interface LoginDialogProps {
open: boolean;
onClose: () => void;
}
export default function LoginDialog({ open, onClose }: LoginDialogProps): React.JSX.Element {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const { login } = useAuth();
const { showSuccess, showError } = useNotification();
const handleClose = (): void => {
setUsername("");
setPassword("");
setShowPassword(false);
onClose();
};
const handleSubmit: () => Promise<void> = async () => {
if (!username || !password) {
return;
}
try {
await login(username, password);
handleClose();
showSuccess("Logged in", `Successfully logged in as ${username}`);
} catch (exception) {
const detail = ApiError.errorDetail(exception);
if (username === "admin" && password === "admin") {
showError("Login error", "You've entered a password for user \"root\", did you make a typo in username?");
} else {
showError("Login error", `Could not login as ${username}: ${detail}`);
}
}
};
return <Dialog open={open} onClose={handleClose} maxWidth="xs" fullWidth>
<DialogHeader onClose={handleClose}>
Login
</DialogHeader>
<DialogContent>
<TextField
label="username"
fullWidth
margin="normal"
value={username}
onChange={event => setUsername(event.target.value)}
autoFocus
/>
<TextField
label="password"
fullWidth
margin="normal"
type={showPassword ? "text" : "password"}
value={password}
onChange={event => setPassword(event.target.value)}
onKeyDown={event => {
if (event.key === "Enter") {
void handleSubmit();
}
}}
slotProps={{
input: {
endAdornment:
<InputAdornment position="end">
<IconButton aria-label={showPassword ? "Hide password" : "Show password"} onClick={() => setShowPassword(!showPassword)} edge="end" size="small">
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
</InputAdornment>,
},
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => void handleSubmit()} variant="contained" startIcon={<PersonIcon />}>login</Button>
</DialogActions>
</Dialog>;
}

View File

@@ -0,0 +1,193 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/Delete";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import {
Autocomplete,
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
FormControlLabel,
IconButton,
TextField,
} from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { ApiError } from "api/client/ApiError";
import DialogHeader from "components/common/DialogHeader";
import RepositorySelect from "components/common/RepositorySelect";
import { QueryKeys } from "hooks/QueryKeys";
import { useClient } from "hooks/useClient";
import { useDebounce } from "hooks/useDebounce";
import { useNotification } from "hooks/useNotification";
import { useSelectedRepository } from "hooks/useSelectedRepository";
import type { AURPackage } from "models/AURPackage";
import type { PackageActionRequest } from "models/PackageActionRequest";
import React, { useRef, useState } from "react";
interface EnvironmentVariable {
id: number;
key: string;
value: string;
}
interface PackageAddDialogProps {
open: boolean;
onClose: () => void;
}
export default function PackageAddDialog({ open, onClose }: PackageAddDialogProps): React.JSX.Element {
const client = useClient();
const { showSuccess, showError } = useNotification();
const repositorySelect = useSelectedRepository();
const [packageName, setPackageName] = useState("");
const [refreshDatabase, setRefreshDatabase] = useState(true);
const [environmentVariables, setEnvironmentVariables] = useState<EnvironmentVariable[]>([]);
const variableIdCounter = useRef(0);
const handleClose = (): void => {
setPackageName("");
repositorySelect.reset();
setRefreshDatabase(true);
setEnvironmentVariables([]);
onClose();
};
const debouncedSearch = useDebounce(packageName, 500);
const { data: searchResults = [] } = useQuery<AURPackage[]>({
queryKey: QueryKeys.search(debouncedSearch),
queryFn: () => client.service.servicePackageSearch(debouncedSearch),
enabled: debouncedSearch.length >= 3,
});
const handleSubmit = async (action: "add" | "request"): Promise<void> => {
if (!packageName) {
return;
}
const repository = repositorySelect.selectedRepository;
if (!repository) {
return;
}
try {
const patches = environmentVariables.filter(variable => variable.key);
const request: PackageActionRequest = { packages: [packageName], patches };
if (action === "add") {
request.refresh = refreshDatabase;
await client.service.servicePackageAdd(repository, request);
} else {
await client.service.servicePackageRequest(repository, request);
}
handleClose();
showSuccess("Success", `Packages ${packageName} have been ${action === "add" ? "added" : "requested"}`);
} catch (exception) {
const detail = ApiError.errorDetail(exception);
showError("Action failed", `Package ${action} failed: ${detail}`);
}
};
return <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogHeader onClose={handleClose}>
Add new packages
</DialogHeader>
<DialogContent>
<RepositorySelect repositorySelect={repositorySelect} />
<Autocomplete
freeSolo
options={searchResults.map(pkg => pkg.package)}
inputValue={packageName}
onInputChange={(_, value) => setPackageName(value)}
renderOption={(props, option) => {
const pkg = searchResults.find(pkg => pkg.package === option);
return (
<li {...props} key={option}>
{option}{pkg ? ` (${pkg.description})` : ""}
</li>
);
}}
renderInput={params =>
<TextField {...params} label="package" placeholder="AUR package" margin="normal" />
}
/>
<FormControlLabel
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => setRefreshDatabase(checked)} />}
label="update pacman databases"
/>
<Button
fullWidth
variant="outlined"
startIcon={<AddIcon />}
onClick={() => {
const id = variableIdCounter.current++;
setEnvironmentVariables(prev => [...prev, { id, key: "", value: "" }]);
}}
sx={{ mt: 1 }}
>
add environment variable
</Button>
{environmentVariables.map(variable =>
<Box key={variable.id} sx={{ display: "flex", gap: 1, mt: 1, alignItems: "center" }}>
<TextField
size="small"
placeholder="name"
value={variable.key}
onChange={event => {
const newKey = event.target.value;
setEnvironmentVariables(prev =>
prev.map(entry => entry.id === variable.id ? { ...entry, key: newKey } : entry),
);
}}
sx={{ flex: 1 }}
/>
<Box>=</Box>
<TextField
size="small"
placeholder="value"
value={variable.value}
onChange={event => {
const newValue = event.target.value;
setEnvironmentVariables(prev =>
prev.map(entry => entry.id === variable.id ? { ...entry, value: newValue } : entry),
);
}}
sx={{ flex: 1 }}
/>
<IconButton size="small" color="error" aria-label="Remove variable" onClick={() => setEnvironmentVariables(prev => prev.filter(entry => entry.id !== variable.id))}>
<DeleteIcon />
</IconButton>
</Box>,
)}
</DialogContent>
<DialogActions>
<Button onClick={() => void handleSubmit("add")} variant="contained" startIcon={<PlayArrowIcon />}>add</Button>
<Button onClick={() => void handleSubmit("request")} variant="contained" color="success" startIcon={<AddIcon />}>request</Button>
</DialogActions>
</Dialog>;
}

View File

@@ -0,0 +1,194 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box, Dialog, DialogContent, Tab, Tabs } from "@mui/material";
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError } from "api/client/ApiError";
import DialogHeader from "components/common/DialogHeader";
import BuildLogsTab from "components/package/BuildLogsTab";
import ChangesTab from "components/package/ChangesTab";
import EventsTab from "components/package/EventsTab";
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
import PackageInfoActions from "components/package/PackageInfoActions";
import PackagePatchesList from "components/package/PackagePatchesList";
import { QueryKeys } from "hooks/QueryKeys";
import { useAuth } from "hooks/useAuth";
import { useAutoRefresh } from "hooks/useAutoRefresh";
import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification";
import { useRepository } from "hooks/useRepository";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type { Dependencies } from "models/Dependencies";
import type { PackageStatus } from "models/PackageStatus";
import type { Patch } from "models/Patch";
import React, { useState } from "react";
import { StatusHeaderStyles } from "theme/StatusColors";
import { defaultInterval } from "utils";
interface PackageInfoDialogProps {
packageBase: string | null;
open: boolean;
onClose: () => void;
autoRefreshIntervals: AutoRefreshInterval[];
}
export default function PackageInfoDialog({
packageBase,
open,
onClose,
autoRefreshIntervals,
}: PackageInfoDialogProps): React.JSX.Element {
const client = useClient();
const { current } = useRepository();
const { isAuthorized } = useAuth();
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const [localPackageBase, setLocalPackageBase] = useState(packageBase);
if (packageBase !== null && packageBase !== localPackageBase) {
setLocalPackageBase(packageBase);
}
const [tabIndex, setTabIndex] = useState(0);
const [refreshDatabase, setRefreshDatabase] = useState(true);
const handleClose = (): void => {
setTabIndex(0);
setRefreshDatabase(true);
onClose();
};
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
const { data: packageData } = useQuery<PackageStatus[]>({
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
enabled: open,
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
});
const { data: dependencies } = useQuery<Dependencies>({
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
queryFn: localPackageBase && current
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
enabled: open,
});
const { data: patches = [] } = useQuery<Patch[]>({
queryKey: localPackageBase ? QueryKeys.patches(localPackageBase) : ["patches"],
queryFn: localPackageBase ? () => client.fetch.fetchPackagePatches(localPackageBase) : skipToken,
enabled: open,
});
const description: PackageStatus | undefined = packageData?.[0];
const pkg = description?.package;
const status = description?.status;
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
const handleUpdate: () => Promise<void> = async () => {
if (!localPackageBase || !current) {
return;
}
try {
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
showSuccess("Success", `Run update for packages ${localPackageBase}`);
} catch (exception) {
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
}
};
const handleRemove: () => Promise<void> = async () => {
if (!localPackageBase || !current) {
return;
}
try {
await client.service.servicePackageRemove(current, [localPackageBase]);
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
onClose();
} catch (exception) {
showError("Action failed", `Could not remove package: ${ApiError.errorDetail(exception)}`);
}
};
const handleDeletePatch: (key: string) => Promise<void> = async key => {
if (!localPackageBase) {
return;
}
try {
await client.service.servicePackagePatchRemove(localPackageBase, key);
void queryClient.invalidateQueries({ queryKey: QueryKeys.patches(localPackageBase) });
} catch (exception) {
showError("Action failed", `Could not delete variable: ${ApiError.errorDetail(exception)}`);
}
};
return <Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
<DialogHeader onClose={handleClose} sx={headerStyle}>
{pkg && status
? `${pkg.base} ${status.status} at ${new Date(status.timestamp * 1000).toISOStringShort()}`
: localPackageBase ?? ""}
</DialogHeader>
<DialogContent>
{pkg &&
<>
<PackageDetailsGrid pkg={pkg} dependencies={dependencies} />
<PackagePatchesList
patches={patches}
editable={isAuthorized}
onDelete={key => void handleDeletePatch(key)}
/>
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
<Tab label="Build logs" />
<Tab label="Changes" />
<Tab label="Events" />
</Tabs>
</Box>
{tabIndex === 0 && localPackageBase && current &&
<BuildLogsTab
packageBase={localPackageBase}
repository={current}
refreshInterval={autoRefresh.interval}
/>
}
{tabIndex === 1 && localPackageBase && current &&
<ChangesTab packageBase={localPackageBase} repository={current} />
}
{tabIndex === 2 && localPackageBase && current &&
<EventsTab packageBase={localPackageBase} repository={current} />
}
</>
}
</DialogContent>
<PackageInfoActions
isAuthorized={isAuthorized}
refreshDatabase={refreshDatabase}
onRefreshDatabaseChange={setRefreshDatabase}
onUpdate={() => void handleUpdate()}
onRemove={() => void handleRemove()}
autoRefreshIntervals={autoRefreshIntervals}
autoRefreshInterval={autoRefresh.interval}
onAutoRefreshIntervalChange={autoRefresh.setInterval}
/>
</Dialog>;
}

View File

@@ -0,0 +1,88 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Button, Dialog, DialogActions, DialogContent, TextField } from "@mui/material";
import { ApiError } from "api/client/ApiError";
import DialogHeader from "components/common/DialogHeader";
import RepositorySelect from "components/common/RepositorySelect";
import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification";
import { useSelectedRepository } from "hooks/useSelectedRepository";
import React, { useState } from "react";
interface PackageRebuildDialogProps {
open: boolean;
onClose: () => void;
}
export default function PackageRebuildDialog({ open, onClose }: PackageRebuildDialogProps): React.JSX.Element {
const client = useClient();
const { showSuccess, showError } = useNotification();
const repositorySelect = useSelectedRepository();
const [dependency, setDependency] = useState("");
const handleClose = (): void => {
setDependency("");
repositorySelect.reset();
onClose();
};
const handleRebuild: () => Promise<void> = async () => {
if (!dependency) {
return;
}
const repository = repositorySelect.selectedRepository;
if (!repository) {
return;
}
try {
await client.service.serviceRebuild(repository, [dependency]);
handleClose();
showSuccess("Success", `Repository rebuild has been run for packages which depend on ${dependency}`);
} catch (exception) {
const detail = ApiError.errorDetail(exception);
showError("Action failed", `Repository rebuild failed: ${detail}`);
}
};
return <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogHeader onClose={handleClose}>
Rebuild depending packages
</DialogHeader>
<DialogContent>
<RepositorySelect repositorySelect={repositorySelect} />
<TextField
label="dependency"
placeholder="packages dependency"
fullWidth
margin="normal"
value={dependency}
onChange={event => setDependency(event.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => void handleRebuild()} variant="contained" startIcon={<PlayArrowIcon />}>rebuild</Button>
</DialogActions>
</Dialog>;
}

View File

@@ -0,0 +1,76 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box, Container } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import LoginDialog from "components/dialogs/LoginDialog";
import Footer from "components/layout/Footer";
import Navbar from "components/layout/Navbar";
import PackageTable from "components/table/PackageTable";
import { QueryKeys } from "hooks/QueryKeys";
import { useAuth } from "hooks/useAuth";
import { useClient } from "hooks/useClient";
import { useRepository } from "hooks/useRepository";
import type { InfoResponse } from "models/InfoResponse";
import React, { useEffect, useState } from "react";
export default function AppLayout(): React.JSX.Element {
const client = useClient();
const { setAuthState } = useAuth();
const { setRepositories } = useRepository();
const [loginOpen, setLoginOpen] = useState(false);
const { data: info } = useQuery<InfoResponse>({
queryKey: QueryKeys.info,
queryFn: () => client.fetch.fetchServerInfo(),
staleTime: Infinity,
});
// Sync info to contexts when loaded
useEffect(() => {
if (info) {
setAuthState({ enabled: info.auth.enabled, username: info.auth.username ?? null });
setRepositories(info.repositories);
}
}, [info, setAuthState, setRepositories]);
return <Container maxWidth="xl">
<Box sx={{ display: "flex", alignItems: "center", py: 1, gap: 1 }}>
<a href="https://ahriman.readthedocs.io/" title="logo">
<img src="/static/logo.svg" width={30} height={30} alt="" />
</a>
<Box sx={{ flex: 1 }}>
<Navbar />
</Box>
</Box>
<PackageTable
autoRefreshIntervals={info?.autorefresh_intervals ?? []}
/>
<Footer
version={info?.version ?? ""}
docsEnabled={info?.docs_enabled ?? false}
indexUrl={info?.index_url}
onLoginClick={() => info?.auth.external ? window.location.assign("/api/v1/login") : setLoginOpen(true)}
/>
<LoginDialog open={loginOpen} onClose={() => setLoginOpen(false)} />
</Container>;
}

View File

@@ -0,0 +1,92 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import GitHubIcon from "@mui/icons-material/GitHub";
import HomeIcon from "@mui/icons-material/Home";
import LoginIcon from "@mui/icons-material/Login";
import LogoutIcon from "@mui/icons-material/Logout";
import { Box, Button, Link, Typography } from "@mui/material";
import { useAuth } from "hooks/useAuth";
import type React from "react";
interface FooterProps {
version: string;
docsEnabled: boolean;
indexUrl?: string;
onLoginClick: () => void;
}
export default function Footer({ version, docsEnabled, indexUrl, onLoginClick }: FooterProps): React.JSX.Element {
const { enabled: authEnabled, username, logout } = useAuth();
return <Box
component="footer"
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
alignItems: "center",
borderTop: 1,
borderColor: "divider",
mt: 2,
py: 1,
}}
>
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
<Link href="https://github.com/arcan1s/ahriman" underline="hover" color="inherit" sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<GitHubIcon fontSize="small" />
<Typography variant="body2">ahriman {version}</Typography>
</Link>
<Link href="https://github.com/arcan1s/ahriman/releases" underline="hover" color="text.secondary" variant="body2">
releases
</Link>
<Link href="https://github.com/arcan1s/ahriman/issues" underline="hover" color="text.secondary" variant="body2">
report a bug
</Link>
{docsEnabled &&
<Link href="/api-docs" underline="hover" color="text.secondary" variant="body2">
api
</Link>
}
</Box>
{indexUrl &&
<Box>
<Link href={indexUrl} underline="hover" color="inherit" sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<HomeIcon fontSize="small" />
<Typography variant="body2">repo index</Typography>
</Link>
</Box>
}
{authEnabled &&
<Box>
{username ?
<Button size="small" startIcon={<LogoutIcon />} onClick={() => void logout()} sx={{ textTransform: "none" }}>
logout ({username})
</Button>
:
<Button size="small" startIcon={<LoginIcon />} onClick={onLoginClick} sx={{ textTransform: "none" }}>
login
</Button>
}
</Box>
}
</Box>;
}

View File

@@ -0,0 +1,55 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box, Tab, Tabs } from "@mui/material";
import { useRepository } from "hooks/useRepository";
import type React from "react";
export default function Navbar(): React.JSX.Element | null {
const { repositories, current, setCurrent } = useRepository();
if (repositories.length === 0 || !current) {
return null;
}
const currentIndex = repositories.findIndex(repository =>
repository.architecture === current.architecture && repository.repository === current.repository,
);
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={currentIndex >= 0 ? currentIndex : 0}
onChange={(_, newValue: number) => {
const repository = repositories[newValue];
if (repository) {
setCurrent(repository);
}
}}
variant="scrollable"
scrollButtons="auto"
>
{repositories.map(repository =>
<Tab
key={repository.key}
label={repository.label}
/>,
)}
</Tabs>
</Box>;
}

View File

@@ -0,0 +1,185 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import ListIcon from "@mui/icons-material/List";
import { Box, Button, Menu, MenuItem, Typography } from "@mui/material";
import { keepPreviousData, skipToken, useQuery } from "@tanstack/react-query";
import CodeBlock from "components/common/CodeBlock";
import { QueryKeys } from "hooks/QueryKeys";
import { useAutoScroll } from "hooks/useAutoScroll";
import { useClient } from "hooks/useClient";
import type { LogRecord } from "models/LogRecord";
import type { RepositoryId } from "models/RepositoryId";
import React, { useEffect, useMemo, useState } from "react";
interface Logs {
version: string;
processId: string;
created: number;
logs: string;
}
interface BuildLogsTabProps {
packageBase: string;
repository: RepositoryId;
refreshInterval: number;
}
function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boolean): string {
const filtered = filter ? records.filter(filter) : records;
return filtered
.map(record => `[${new Date(record.created * 1000).toISOString()}] ${record.message}`)
.join("\n");
}
export default function BuildLogsTab({
packageBase,
repository,
refreshInterval,
}: BuildLogsTabProps): React.JSX.Element {
const client = useClient();
const [selectedVersionKey, setSelectedVersionKey] = useState<string | null>(null);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const { data: allLogs } = useQuery<LogRecord[]>({
queryKey: QueryKeys.logs(packageBase, repository),
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
enabled: !!packageBase,
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
});
// Build version selectors from all logs
const versions = useMemo<Logs[]>(() => {
if (!allLogs || allLogs.length === 0) {
return [];
}
const grouped: Record<string, LogRecord & { minCreated: number }> = {};
for (const record of allLogs) {
const key = `${record.version}-${record.process_id}`;
const existing = grouped[key];
if (!existing) {
grouped[key] = { ...record, minCreated: record.created };
} else {
existing.minCreated = Math.min(existing.minCreated, record.created);
}
}
return Object.values(grouped)
.sort((left, right) => right.minCreated - left.minCreated)
.map(record => ({
version: record.version,
processId: record.process_id,
created: record.minCreated,
logs: convertLogs(
allLogs,
right => record.version === right.version && record.process_id === right.process_id,
),
}));
}, [allLogs]);
// Compute active index from selected version key, defaulting to newest (index 0)
const activeIndex = useMemo(() => {
if (selectedVersionKey) {
const index = versions.findIndex(record => `${record.version}-${record.processId}` === selectedVersionKey);
if (index >= 0) {
return index;
}
}
return 0;
}, [versions, selectedVersionKey]);
const activeVersion = versions[activeIndex];
const activeVersionKey = activeVersion ? `${activeVersion.version}-${activeVersion.processId}` : null;
// Refresh active version logs
const { data: versionLogs } = useQuery<LogRecord[]>({
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
queryFn: activeVersion
? () => client.fetch.fetchPackageLogs(
packageBase, repository, activeVersion.version, activeVersion.processId,
)
: skipToken,
placeholderData: keepPreviousData,
refetchInterval: refreshInterval > 0 ? refreshInterval : false,
});
// Derive displayed logs: prefer fresh polled data when available
const displayedLogs = useMemo(() => {
if (versionLogs && versionLogs.length > 0) {
return convertLogs(versionLogs);
}
return activeVersion?.logs ?? "";
}, [versionLogs, activeVersion]);
const { preRef, handleScroll, scrollToBottom, resetScroll } = useAutoScroll();
// Reset scroll tracking when active version changes
useEffect(() => {
resetScroll();
}, [activeVersionKey, resetScroll]);
// Scroll to bottom on new logs
useEffect(() => {
scrollToBottom();
}, [displayedLogs, scrollToBottom]);
return <Box sx={{ display: "flex", gap: 1, mt: 1 }}>
<Box>
<Button
size="small"
aria-label="Select version"
startIcon={<ListIcon />}
onClick={event => setAnchorEl(event.currentTarget)}
/>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{versions.map((logs, index) =>
<MenuItem
key={`${logs.version}-${logs.processId}`}
selected={index === activeIndex}
onClick={() => {
setSelectedVersionKey(`${logs.version}-${logs.processId}`);
setAnchorEl(null);
resetScroll();
}}
>
<Typography variant="body2">{new Date(logs.created * 1000).toISOStringShort()}</Typography>
</MenuItem>,
)}
{versions.length === 0 &&
<MenuItem disabled>No logs available</MenuItem>
}
</Menu>
</Box>
<Box sx={{ flex: 1 }}>
<CodeBlock
preRef={preRef}
getText={() => displayedLogs}
height={400}
onScroll={handleScroll}
wordBreak
/>
</Box>
</Box>;
}

View File

@@ -0,0 +1,70 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import CopyButton from "components/common/CopyButton";
import { QueryKeys } from "hooks/QueryKeys";
import { useClient } from "hooks/useClient";
import type { Changes } from "models/Changes";
import type { RepositoryId } from "models/RepositoryId";
import React from "react";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
import { githubGist } from "react-syntax-highlighter/dist/esm/styles/hljs";
SyntaxHighlighter.registerLanguage("diff", diff);
interface ChangesTabProps {
packageBase: string;
repository: RepositoryId;
}
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
const client = useClient();
const { data } = useQuery<Changes>({
queryKey: QueryKeys.changes(packageBase, repository),
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
enabled: !!packageBase,
});
const changesText = data?.changes ?? "";
return <Box sx={{ position: "relative", mt: 1 }}>
<SyntaxHighlighter
language="diff"
style={githubGist}
customStyle={{
padding: "16px",
borderRadius: "4px",
overflow: "auto",
height: 400,
fontSize: "0.8rem",
fontFamily: "monospace",
margin: 0,
}}
>
{changesText}
</SyntaxHighlighter>
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
<CopyButton getText={() => changesText} />
</Box>
</Box>;
}

View File

@@ -0,0 +1,79 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box } from "@mui/material";
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import { useQuery } from "@tanstack/react-query";
import EventDurationLineChart from "components/charts/EventDurationLineChart";
import { QueryKeys } from "hooks/QueryKeys";
import { useClient } from "hooks/useClient";
import type { Event } from "models/Event";
import type { RepositoryId } from "models/RepositoryId";
import type React from "react";
import { useMemo } from "react";
interface EventsTabProps {
packageBase: string;
repository: RepositoryId;
}
interface EventRow {
id: number;
timestamp: string;
event: string;
message: string;
}
const columns: GridColDef<EventRow>[] = [
{ field: "timestamp", headerName: "date", width: 180, align: "right", headerAlign: "right" },
{ field: "event", headerName: "event", flex: 1 },
{ field: "message", headerName: "description", flex: 2 },
];
export default function EventsTab({ packageBase, repository }: EventsTabProps): React.JSX.Element {
const client = useClient();
const { data: events = [] } = useQuery<Event[]>({
queryKey: QueryKeys.events(repository, packageBase),
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
enabled: !!packageBase,
});
const rows = useMemo<EventRow[]>(() => events.map((event, index) => ({
id: index,
timestamp: new Date(event.created * 1000).toISOStringShort(),
event: event.event,
message: event.message ?? "",
})), [events]);
return <Box sx={{ mt: 1 }}>
<EventDurationLineChart events={events} />
<DataGrid
rows={rows}
columns={columns}
density="compact"
initialState={{
sorting: { sortModel: [{ field: "timestamp", sort: "desc" }] },
}}
pageSizeOptions={[10, 25]}
sx={{ height: 400, mt: 1 }}
disableRowSelectionOnClick
/>
</Box>;
}

View File

@@ -0,0 +1,114 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Grid, Link, Typography } from "@mui/material";
import type { Dependencies } from "models/Dependencies";
import type { Package } from "models/Package";
import React from "react";
interface PackageDetailsGridProps {
pkg: Package;
dependencies?: Dependencies;
}
export default function PackageDetailsGrid({ pkg, dependencies }: PackageDetailsGridProps): React.JSX.Element {
const packagesList = Object.entries(pkg.packages)
.map(([name, properties]) => `${name}${properties.description ? ` (${properties.description})` : ""}`);
const groups = Object.values(pkg.packages)
.flatMap(properties => properties.groups ?? []);
const licenses = Object.values(pkg.packages)
.flatMap(properties => properties.licenses ?? []);
const upstreamUrls = Object.values(pkg.packages)
.map(properties => properties.url)
.filter((url): url is string => !!url)
.unique();
const aurUrl = pkg.remote.web_url;
const pkgNames = Object.keys(pkg.packages);
const pkgValues = Object.values(pkg.packages);
const deps = pkgValues
.flatMap(properties => (properties.depends ?? []).filter(dep => !pkgNames.includes(dep)))
.unique();
const makeDeps = pkgValues
.flatMap(properties => (properties.make_depends ?? []).filter(dep => !pkgNames.includes(dep)))
.map(dep => `${dep} (make)`)
.unique();
const optDeps = pkgValues
.flatMap(properties => (properties.opt_depends ?? []).filter(dep => !pkgNames.includes(dep)))
.map(dep => `${dep} (optional)`)
.unique();
const allDepends = [...deps, ...makeDeps, ...optDeps];
const implicitDepends = dependencies
? Object.values(dependencies.paths).flat()
: [];
return <>
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packages</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{packagesList.unique().join("\n")}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">version</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.version}</Typography></Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">packager</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2">{pkg.packager ?? ""}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }} />
<Grid size={{ xs: 8, md: 5 }} />
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">groups</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{groups.unique().join("\n")}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">licenses</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{licenses.unique().join("\n")}</Typography></Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">upstream</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}>
{upstreamUrls.map(url =>
<Link key={url} href={url} target="_blank" rel="noopener noreferrer" underline="hover" display="block" variant="body2">
{url}
</Link>,
)}
</Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">AUR</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}>
<Typography variant="body2">
{aurUrl &&
<Link href={aurUrl} target="_blank" rel="noopener noreferrer" underline="hover">AUR link</Link>
}
</Typography>
</Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 0.5 }}>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">depends</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{allDepends.join("\n")}</Typography></Grid>
<Grid size={{ xs: 4, md: 1 }}><Typography variant="body2" color="text.secondary" align="right">implicitly depends</Typography></Grid>
<Grid size={{ xs: 8, md: 5 }}><Typography variant="body2" sx={{ whiteSpace: "pre-line" }}>{implicitDepends.unique().join("\n")}</Typography></Grid>
</Grid>
</>;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
import DeleteIcon from "@mui/icons-material/Delete";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
import AutoRefreshControl from "components/common/AutoRefreshControl";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type React from "react";
interface PackageInfoActionsProps {
isAuthorized: boolean;
refreshDatabase: boolean;
onRefreshDatabaseChange: (checked: boolean) => void;
onUpdate: () => void;
onRemove: () => void;
autoRefreshIntervals: AutoRefreshInterval[];
autoRefreshInterval: number;
onAutoRefreshIntervalChange: (interval: number) => void;
}
export default function PackageInfoActions({
isAuthorized,
refreshDatabase,
onRefreshDatabaseChange,
onUpdate,
onRemove,
autoRefreshIntervals,
autoRefreshInterval,
onAutoRefreshIntervalChange,
}: PackageInfoActionsProps): React.JSX.Element {
return <DialogActions sx={{ flexWrap: "wrap", gap: 1 }}>
{isAuthorized &&
<>
<FormControlLabel
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
label="update pacman databases"
/>
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
update
</Button>
<Button onClick={onRemove} variant="contained" color="error" startIcon={<DeleteIcon />} size="small">
remove
</Button>
</>
}
<AutoRefreshControl
intervals={autoRefreshIntervals}
currentInterval={autoRefreshInterval}
onIntervalChange={onAutoRefreshIntervalChange}
/>
</DialogActions>;
}

View File

@@ -0,0 +1,65 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import DeleteIcon from "@mui/icons-material/Delete";
import { Box, IconButton, TextField, Typography } from "@mui/material";
import type { Patch } from "models/Patch";
import type React from "react";
interface PackagePatchesListProps {
patches: Patch[];
editable: boolean;
onDelete: (key: string) => void;
}
export default function PackagePatchesList({
patches,
editable,
onDelete,
}: PackagePatchesListProps): React.JSX.Element | null {
if (patches.length === 0) {
return null;
}
return <Box sx={{ mt: 2 }}>
<Typography variant="h6" gutterBottom>Environment variables</Typography>
{patches.map(patch =>
<Box key={patch.key} sx={{ display: "flex", alignItems: "center", gap: 1, mb: 0.5 }}>
<TextField
size="small"
value={patch.key}
disabled
sx={{ flex: 1 }}
/>
<Box>=</Box>
<TextField
size="small"
value={JSON.stringify(patch.value)}
disabled
sx={{ flex: 1 }}
/>
{editable &&
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
<DeleteIcon fontSize="small" />
</IconButton>
}
</Box>,
)}
</Box>;
}

View File

@@ -0,0 +1,196 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Box, Link } from "@mui/material";
import {
DataGrid,
GRID_CHECKBOX_SELECTION_COL_DEF,
type GridColDef,
type GridFilterModel,
type GridRenderCellParams,
type GridRowId,
useGridApiRef,
} from "@mui/x-data-grid";
import DashboardDialog from "components/dialogs/DashboardDialog";
import KeyImportDialog from "components/dialogs/KeyImportDialog";
import PackageAddDialog from "components/dialogs/PackageAddDialog";
import PackageInfoDialog from "components/dialogs/PackageInfoDialog";
import PackageRebuildDialog from "components/dialogs/PackageRebuildDialog";
import PackageTableToolbar from "components/table/PackageTableToolbar";
import StatusCell from "components/table/StatusCell";
import { useDebounce } from "hooks/useDebounce";
import { usePackageTable } from "hooks/usePackageTable";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type { PackageRow } from "models/PackageRow";
import React, { useMemo } from "react";
interface PackageTableProps {
autoRefreshIntervals: AutoRefreshInterval[];
}
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
function createListColumn(
field: keyof PackageRow,
headerName: string,
options: { flex?: number; minWidth?: number; width?: number },
): GridColDef<PackageRow> {
return {
field,
headerName,
...options,
valueGetter: (value: string[]) => (value ?? []).join(" "),
renderCell: (params: GridRenderCellParams<PackageRow>) =>
<Box sx={{ whiteSpace: "pre-line" }}>{((params.row[field] as string[]) ?? []).join("\n")}</Box>,
sortComparator: (left: string, right: string) => left.localeCompare(right),
};
}
export default function PackageTable({ autoRefreshIntervals }: PackageTableProps): React.JSX.Element {
const table = usePackageTable(autoRefreshIntervals);
const apiRef = useGridApiRef();
const debouncedSearch = useDebounce(table.searchText, 300);
const effectiveFilterModel: GridFilterModel = useMemo(
() => ({
...table.filterModel,
quickFilterValues: debouncedSearch ? debouncedSearch.split(/\s+/) : undefined,
}),
[table.filterModel, debouncedSearch],
);
const columns: GridColDef<PackageRow>[] = useMemo(
() => [
{
field: "base",
headerName: "package base",
flex: 1,
minWidth: 150,
renderCell: (params: GridRenderCellParams<PackageRow>) =>
params.row.webUrl ?
<Link href={params.row.webUrl} target="_blank" rel="noopener noreferrer" underline="hover">
{params.value as string}
</Link>
: params.value as string,
},
{ field: "version", headerName: "version", width: 180, align: "right", headerAlign: "right" },
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
createListColumn("groups", "groups", { width: 150 }),
createListColumn("licenses", "licenses", { width: 150 }),
{ field: "packager", headerName: "packager", width: 150 },
{
field: "timestamp",
headerName: "last update",
width: 180,
align: "right",
headerAlign: "right",
},
{
field: "status",
headerName: "status",
width: 120,
align: "center",
headerAlign: "center",
renderCell: (params: GridRenderCellParams<PackageRow>) => <StatusCell status={params.row.status} />,
},
],
[],
);
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
<PackageTableToolbar
hasSelection={table.selectionModel.length > 0}
isAuthorized={table.isAuthorized}
status={table.status}
searchText={table.searchText}
onSearchChange={table.setSearchText}
autoRefresh={{
autoRefreshIntervals,
currentInterval: table.autoRefreshInterval,
onIntervalChange: table.onAutoRefreshIntervalChange,
}}
actions={{
onDashboardClick: () => table.setDialogOpen("dashboard"),
onAddClick: () => table.setDialogOpen("add"),
onUpdateClick: () => void table.handleUpdate(),
onRefreshDatabaseClick: () => void table.handleRefreshDatabase(),
onRebuildClick: () => table.setDialogOpen("rebuild"),
onRemoveClick: () => void table.handleRemove(),
onKeyImportClick: () => table.setDialogOpen("keyImport"),
onReloadClick: table.handleReload,
onExportClick: () => apiRef.current?.exportDataAsCsv(),
}}
/>
<DataGrid
apiRef={apiRef}
rows={table.rows}
columns={columns}
loading={table.isLoading}
getRowHeight={() => "auto"}
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={{ type: "include", ids: new Set<GridRowId>(table.selectionModel) }}
onRowSelectionModelChange={model => {
if (model.type === "exclude") {
const excludeIds = new Set([...model.ids].map(String));
table.setSelectionModel(table.rows.map(row => row.id).filter(id => !excludeIds.has(id)));
} else {
table.setSelectionModel([...model.ids].map(String));
}
}}
paginationModel={table.paginationModel}
onPaginationModelChange={table.setPaginationModel}
pageSizeOptions={PAGE_SIZE_OPTIONS}
columnVisibilityModel={table.columnVisibility}
onColumnVisibilityModelChange={table.setColumnVisibility}
filterModel={effectiveFilterModel}
onFilterModelChange={table.setFilterModel}
initialState={{
sorting: { sortModel: [{ field: "base", sort: "asc" }] },
}}
onCellClick={(params, event) => {
// Don't open info dialog when clicking checkbox or link
if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) {
return;
}
if ((event.target as HTMLElement).closest("a")) {
return;
}
table.setSelectedPackage(String(params.id));
}}
sx={{
flex: 1,
"& .MuiDataGrid-row": { cursor: "pointer" },
}}
density="compact"
/>
<DashboardDialog open={table.dialogOpen === "dashboard"} onClose={() => table.setDialogOpen(null)} />
<PackageAddDialog open={table.dialogOpen === "add"} onClose={() => table.setDialogOpen(null)} />
<PackageRebuildDialog open={table.dialogOpen === "rebuild"} onClose={() => table.setDialogOpen(null)} />
<KeyImportDialog open={table.dialogOpen === "keyImport"} onClose={() => table.setDialogOpen(null)} />
<PackageInfoDialog
packageBase={table.selectedPackage}
open={table.selectedPackage !== null}
onClose={() => table.setSelectedPackage(null)}
autoRefreshIntervals={autoRefreshIntervals}
/>
</Box>;
}

View File

@@ -0,0 +1,185 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import AddIcon from "@mui/icons-material/Add";
import ClearIcon from "@mui/icons-material/Clear";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import InventoryIcon from "@mui/icons-material/Inventory";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import RefreshIcon from "@mui/icons-material/Refresh";
import ReplayIcon from "@mui/icons-material/Replay";
import SearchIcon from "@mui/icons-material/Search";
import VpnKeyIcon from "@mui/icons-material/VpnKey";
import { Box, Button, Divider, IconButton, InputAdornment, Menu, MenuItem, TextField, Tooltip } from "@mui/material";
import AutoRefreshControl from "components/common/AutoRefreshControl";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type { BuildStatus } from "models/BuildStatus";
import React, { useState } from "react";
import { StatusColors } from "theme/StatusColors";
export interface AutoRefreshProps {
autoRefreshIntervals: AutoRefreshInterval[];
currentInterval: number;
onIntervalChange: (interval: number) => void;
}
export interface ToolbarActions {
onDashboardClick: () => void;
onAddClick: () => void;
onUpdateClick: () => void;
onRefreshDatabaseClick: () => void;
onRebuildClick: () => void;
onRemoveClick: () => void;
onKeyImportClick: () => void;
onReloadClick: () => void;
onExportClick: () => void;
}
interface PackageTableToolbarProps {
hasSelection: boolean;
isAuthorized: boolean;
status?: BuildStatus;
searchText: string;
onSearchChange: (text: string) => void;
autoRefresh: AutoRefreshProps;
actions: ToolbarActions;
}
export default function PackageTableToolbar({
hasSelection,
isAuthorized,
status,
searchText,
onSearchChange,
autoRefresh,
actions,
}: PackageTableToolbarProps): React.JSX.Element {
const [packagesAnchorEl, setPackagesAnchorEl] = useState<HTMLElement | null>(null);
return <Box sx={{ display: "flex", gap: 1, mb: 1, flexWrap: "wrap", alignItems: "center" }}>
<Tooltip title="System health">
<IconButton
aria-label="System health"
onClick={actions.onDashboardClick}
sx={{
borderColor: status ? StatusColors[status] : undefined,
borderWidth: 1,
borderStyle: "solid",
color: status ? StatusColors[status] : undefined,
}}
>
<InfoOutlinedIcon />
</IconButton>
</Tooltip>
{isAuthorized &&
<>
<Button
variant="contained"
startIcon={<InventoryIcon />}
onClick={event => setPackagesAnchorEl(event.currentTarget)}
>
packages
</Button>
<Menu
anchorEl={packagesAnchorEl}
open={Boolean(packagesAnchorEl)}
onClose={() => setPackagesAnchorEl(null)}
>
<MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onAddClick();
}}>
<AddIcon fontSize="small" sx={{ mr: 1 }} /> add
</MenuItem>
<MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onUpdateClick();
}}>
<PlayArrowIcon fontSize="small" sx={{ mr: 1 }} /> update
</MenuItem>
<MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onRefreshDatabaseClick();
}}>
<DownloadIcon fontSize="small" sx={{ mr: 1 }} /> update pacman databases
</MenuItem>
<MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onRebuildClick();
}}>
<ReplayIcon fontSize="small" sx={{ mr: 1 }} /> rebuild
</MenuItem>
<Divider />
<MenuItem onClick={() => {
setPackagesAnchorEl(null); actions.onRemoveClick();
}} disabled={!hasSelection}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> remove
</MenuItem>
</Menu>
<Button variant="contained" color="info" startIcon={<VpnKeyIcon />} onClick={actions.onKeyImportClick}>
import key
</Button>
</>
}
<Button variant="outlined" color="secondary" startIcon={<RefreshIcon />} onClick={actions.onReloadClick}>
reload
</Button>
<AutoRefreshControl
intervals={autoRefresh.autoRefreshIntervals}
currentInterval={autoRefresh.currentInterval}
onIntervalChange={autoRefresh.onIntervalChange}
/>
<Box sx={{ flexGrow: 1 }} />
<TextField
size="small"
aria-label="Search packages"
placeholder="search packages..."
value={searchText}
onChange={event => onSearchChange(event.target.value)}
slotProps={{
input: {
startAdornment:
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
,
endAdornment: searchText ?
<InputAdornment position="end">
<IconButton size="small" aria-label="Clear search" onClick={() => onSearchChange("")}>
<ClearIcon fontSize="small" />
</IconButton>
</InputAdornment>
: undefined,
},
}}
sx={{ minWidth: 200 }}
/>
<Tooltip title="Export CSV">
<IconButton size="small" aria-label="Export CSV" onClick={actions.onExportClick}>
<FileDownloadIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>;
}

View File

@@ -0,0 +1,39 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { Chip } from "@mui/material";
import type { BuildStatus } from "models/BuildStatus";
import type React from "react";
import { StatusColors } from "theme/StatusColors";
interface StatusCellProps {
status: BuildStatus;
}
export default function StatusCell({ status }: StatusCellProps): React.JSX.Element {
return <Chip
label={status}
size="small"
sx={{
backgroundColor: StatusColors[status],
color: "common.white",
fontWeight: 500,
}}
/>;
}

View File

@@ -0,0 +1,34 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { createContext } from "react";
interface AuthState {
enabled: boolean;
username: string | null;
}
export interface AuthContextValue extends AuthState {
isAuthorized: boolean;
setAuthState: (state: AuthState) => void;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
export const AuthContext = createContext<AuthContextValue | null>(null);

View File

@@ -0,0 +1,57 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { ApiError } from "api/client/ApiError";
import { AuthContext } from "contexts/AuthContext";
import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification";
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
export function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
const client = useClient();
const [authState, setAuthState] = useState({ enabled: true, username: null as string | null });
const { showError } = useNotification();
const login = useCallback(async (username: string, password: string) => {
await client.login({ username, password });
setAuthState(prev => ({ ...prev, username }));
}, [client]);
const logout = useCallback(async () => {
try {
await client.logout();
setAuthState(prev => ({ ...prev, username: null }));
} catch (exception) {
const detail = ApiError.errorDetail(exception);
showError("Login error", `Could not log out: ${detail}`);
}
}, [client, showError]);
const isAuthorized = useMemo(() =>
!authState.enabled || authState.username !== null, [authState.enabled, authState.username],
);
const value = useMemo(() => ({
...authState, isAuthorized, setAuthState, login, logout,
}), [authState, isAuthorized, login, logout]);
return <AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>;
}

View File

@@ -0,0 +1,23 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { AhrimanClient } from "api/client/AhrimanClient";
import { createContext } from "react";
export const ClientContext = createContext<AhrimanClient | null>(null);

View File

@@ -0,0 +1,32 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { AhrimanClient } from "api/client/AhrimanClient";
import { ClientContext } from "contexts/ClientContext";
import React, { type ReactNode, useMemo } from "react";
export function ClientProvider({ children }: { children: ReactNode }): React.JSX.Element {
const client = useMemo(() => new AhrimanClient(), []);
return (
<ClientContext.Provider value={client}>
{children}
</ClientContext.Provider>
);
}

View File

@@ -0,0 +1,27 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { createContext } from "react";
export interface NotificationContextValue {
showSuccess: (title: string, message: string) => void;
showError: (title: string, message: string) => void;
}
export const NotificationContext = createContext<NotificationContextValue | null>(null);

View File

@@ -0,0 +1,76 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { type AlertColor, Box } from "@mui/material";
import NotificationItem from "components/common/NotificationItem";
import { NotificationContext } from "contexts/NotificationContext";
import type { Notification } from "models/Notification";
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
export function NotificationProvider({ children }: { children: ReactNode }): React.JSX.Element {
const [notifications, setNotifications] = useState<Notification[]>([]);
const addNotification = useCallback((title: string, message: string, severity: AlertColor) => {
const id = `${severity}:${title}:${message}`;
setNotifications(prev => {
if (prev.some(notification => notification.id === id)) {
return prev;
}
return [...prev, { id, title, message, severity }];
});
}, []);
const removeNotification = useCallback((key: string) => {
setNotifications(prev => prev.filter(notification => notification.id !== key));
}, []);
const showSuccess = useCallback(
(title: string, message: string) => addNotification(title, message, "success"),
[addNotification],
);
const showError = useCallback(
(title: string, message: string) => addNotification(title, message, "error"),
[addNotification],
);
const value = useMemo(() => ({ showSuccess, showError }), [showSuccess, showError]);
return <NotificationContext.Provider value={value}>
{children}
<Box
sx={{
position: "fixed",
top: 16,
left: "50%",
transform: "translateX(-50%)",
zIndex: theme => theme.zIndex.snackbar,
display: "flex",
flexDirection: "column",
gap: 1,
maxWidth: 500,
width: "100%",
pointerEvents: "none",
}}
>
{notifications.map(notification =>
<NotificationItem key={notification.id} notification={notification} onClose={removeNotification} />,
)}
</Box>
</NotificationContext.Provider>;
}

View File

@@ -0,0 +1,30 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { RepositoryId } from "models/RepositoryId";
import { createContext } from "react";
export interface RepositoryContextValue {
repositories: RepositoryId[];
current: RepositoryId | null;
setRepositories: (repositories: RepositoryId[]) => void;
setCurrent: (repository: RepositoryId) => void;
}
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);

View File

@@ -0,0 +1,53 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { RepositoryContext } from "contexts/RepositoryContext";
import type { RepositoryId } from "models/RepositoryId";
import React, { type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from "react";
function subscribeToHash(callback: () => void): () => void {
window.addEventListener("hashchange", callback);
return () => window.removeEventListener("hashchange", callback);
}
function getHashSnapshot(): string {
return window.location.hash.replace("#", "");
}
export function RepositoryProvider({ children }: { children: ReactNode }): React.JSX.Element {
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
const current = useMemo(() => {
if (repositories.length === 0) {
return null;
}
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
}, [repositories, hash]);
const setCurrent = useCallback((repository: RepositoryId) => {
window.location.hash = repository.key;
}, []);
const value = useMemo(() => ({
repositories, current, setRepositories, setCurrent,
}), [repositories, current, setCurrent]);
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
}

View File

@@ -0,0 +1,46 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { RepositoryId } from "models/RepositoryId";
export const QueryKeys = {
changes: (packageBase: string, repository: RepositoryId) => ["changes", repository.key, packageBase] as const,
dependencies: (packageBase: string, repository: RepositoryId) => ["dependencies", repository.key, packageBase] as const,
events: (repository: RepositoryId, objectId?: string) => ["events", repository.key, objectId] as const,
info: ["info"] as const,
logs: (packageBase: string, repository: RepositoryId) => ["logs", repository.key, packageBase] as const,
logsVersion: (packageBase: string, repository: RepositoryId, version: string, processId: string) =>
["logs", repository.key, packageBase, version, processId] as const,
package: (packageBase: string, repository: RepositoryId) => ["packages", repository.key, packageBase] as const,
packages: (repository: RepositoryId) => ["packages", repository.key] as const,
patches: (packageBase: string) => ["patches", packageBase] as const,
search: (query: string) => ["search", query] as const,
status: (repository: RepositoryId) => ["status", repository.key] as const,
};

View File

@@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { AuthContext, type AuthContextValue } from "contexts/AuthContext";
import { useContextNotNull } from "hooks/useContextNotNull";
export function useAuth(): AuthContextValue {
return useContextNotNull(AuthContext);
}

View File

@@ -0,0 +1,49 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { useLocalStorage } from "hooks/useLocalStorage";
import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
interface AutoRefreshResult {
interval: number;
setInterval: Dispatch<SetStateAction<number>>;
setPaused: Dispatch<SetStateAction<boolean>>;
}
export function useAutoRefresh(key: string, defaultInterval: number): AutoRefreshResult {
const storageKey = `ahriman-${key}`;
const [interval, setInterval] = useLocalStorage<number>(storageKey, defaultInterval);
const [paused, setPaused] = useState(false);
// Apply defaultInterval when it becomes available (e.g. after info endpoint loads)
// but only if the user hasn't explicitly set a preference
useEffect(() => {
if (defaultInterval > 0 && window.localStorage.getItem(storageKey) === null) {
setInterval(defaultInterval);
}
}, [storageKey, defaultInterval, setInterval]);
const effectiveInterval = paused ? 0 : interval;
return {
interval: effectiveInterval,
setInterval,
setPaused,
};
}

View File

@@ -0,0 +1,63 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { type RefObject, useCallback, useRef } from "react";
interface UseAutoScrollResult {
preRef: RefObject<HTMLElement | null>;
handleScroll: () => void;
scrollToBottom: () => void;
resetScroll: () => void;
}
export function useAutoScroll(): UseAutoScrollResult {
const preRef = useRef<HTMLElement>(null);
const initialScrollDone = useRef(false);
const wasAtBottom = useRef(true);
const handleScroll = useCallback((): void => {
if (preRef.current) {
const element = preRef.current;
wasAtBottom.current = element.scrollTop + element.clientHeight >= element.scrollHeight - 50;
}
}, []);
const resetScroll = useCallback((): void => {
initialScrollDone.current = false;
}, []);
// scroll to bottom on initial load, then only if already near bottom and no active text selection
const scrollToBottom = useCallback((): void => {
if (!preRef.current) {
return;
}
const element = preRef.current;
if (!initialScrollDone.current) {
element.scrollTop = element.scrollHeight;
initialScrollDone.current = true;
} else {
const hasSelection = !document.getSelection()?.isCollapsed;
if (wasAtBottom.current && !hasSelection) {
element.scrollTop = element.scrollHeight;
}
}
}, []);
return { preRef, handleScroll, scrollToBottom, resetScroll };
}

View File

@@ -0,0 +1,26 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { AhrimanClient } from "api/client/AhrimanClient";
import { ClientContext } from "contexts/ClientContext";
import { useContextNotNull } from "hooks/useContextNotNull";
export function useClient(): AhrimanClient {
return useContextNotNull(ClientContext);
}

View File

@@ -0,0 +1,28 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { type Context, useContext } from "react";
export function useContextNotNull<T>(context: Context<T | null>): T {
const ctx = useContext(context);
if (ctx === null) {
throw new Error("must be used within a Provider");
}
return ctx;
}

View File

@@ -0,0 +1,31 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}

View File

@@ -0,0 +1,44 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { type Dispatch, type SetStateAction, useCallback, useState } from "react";
export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item !== null ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue: Dispatch<SetStateAction<T>> = useCallback(
value => {
setStoredValue(prev => {
const nextValue = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(nextValue));
return nextValue;
});
},
[key],
);
return [storedValue, setValue];
}

View File

@@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { NotificationContext, type NotificationContextValue } from "contexts/NotificationContext";
import { useContextNotNull } from "hooks/useContextNotNull";
export function useNotification(): NotificationContextValue {
return useContextNotNull(NotificationContext);
}

View File

@@ -0,0 +1,108 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { useQueryClient } from "@tanstack/react-query";
import { ApiError } from "api/client/ApiError";
import { QueryKeys } from "hooks/QueryKeys";
import { useClient } from "hooks/useClient";
import { useNotification } from "hooks/useNotification";
import { useRepository } from "hooks/useRepository";
import type { RepositoryId } from "models/RepositoryId";
export interface UsePackageActionsResult {
handleReload: () => void;
handleUpdate: () => Promise<void>;
handleRefreshDatabase: () => Promise<void>;
handleRemove: () => Promise<void>;
}
export function usePackageActions(
selectionModel: string[],
setSelectionModel: (model: string[]) => void,
): UsePackageActionsResult {
const client = useClient();
const { current } = useRepository();
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const invalidate = (repository: RepositoryId): void => {
void queryClient.invalidateQueries({ queryKey: QueryKeys.packages(repository) });
void queryClient.invalidateQueries({ queryKey: QueryKeys.status(repository) });
};
const performAction = async (
action: (repository: RepositoryId) => Promise<string>,
errorMessage: string,
): Promise<void> => {
if (!current) {
return;
}
try {
const successMessage = await action(current);
showSuccess("Success", successMessage);
invalidate(current);
setSelectionModel([]);
} catch (exception) {
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
}
};
const handleReload: () => void = () => {
if (current !== null) {
invalidate(current);
}
};
const handleUpdate = (): Promise<void> => performAction(async (repository): Promise<string> => {
if (selectionModel.length === 0) {
await client.service.servicePackageUpdate(repository, { packages: [] });
return "Repository update has been run";
}
await client.service.servicePackageAdd(repository, { packages: selectionModel });
return `Run update for packages ${selectionModel.join(", ")}`;
}, "Packages update failed");
const handleRefreshDatabase = (): Promise<void> => performAction(async (repository): Promise<string> => {
await client.service.servicePackageUpdate(repository, {
packages: [],
refresh: true,
aur: false,
local: false,
manual: false,
});
return "Pacman database update has been requested";
}, "Could not update pacman databases");
const handleRemove = (): Promise<void> => {
if (selectionModel.length === 0) {
return Promise.resolve();
}
return performAction(async (repository): Promise<string> => {
await client.service.servicePackageRemove(repository, selectionModel);
return `Packages ${selectionModel.join(", ")} have been removed`;
}, "Could not remove packages");
};
return {
handleReload,
handleUpdate,
handleRefreshDatabase,
handleRemove,
};
}

View File

@@ -0,0 +1,68 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { skipToken, useQuery } from "@tanstack/react-query";
import { QueryKeys } from "hooks/QueryKeys";
import { useAuth } from "hooks/useAuth";
import { useAutoRefresh } from "hooks/useAutoRefresh";
import { useClient } from "hooks/useClient";
import { useRepository } from "hooks/useRepository";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type { BuildStatus } from "models/BuildStatus";
import { PackageRow } from "models/PackageRow";
import { useMemo } from "react";
import { defaultInterval } from "utils";
export interface UsePackageDataResult {
rows: PackageRow[];
isLoading: boolean;
isAuthorized: boolean;
status: BuildStatus | undefined;
autoRefresh: ReturnType<typeof useAutoRefresh>;
}
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
const client = useClient();
const { current } = useRepository();
const { isAuthorized } = useAuth();
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
const { data: packages = [], isLoading } = useQuery({
queryKey: current ? QueryKeys.packages(current) : ["packages"],
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
});
const { data: status } = useQuery({
queryKey: current ? QueryKeys.status(current) : ["status"],
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
});
const rows = useMemo(() => packages.map(descriptor => new PackageRow(descriptor)), [packages]);
return {
rows,
isLoading,
isAuthorized,
status: status?.status.status,
autoRefresh,
};
}

View File

@@ -0,0 +1,86 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { GridFilterModel } from "@mui/x-data-grid";
import { usePackageActions } from "hooks/usePackageActions";
import { usePackageData } from "hooks/usePackageData";
import { useTableState } from "hooks/useTableState";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type { BuildStatus } from "models/BuildStatus";
import type { PackageRow } from "models/PackageRow";
import { useEffect } from "react";
export interface UsePackageTableResult {
rows: PackageRow[];
isLoading: boolean;
isAuthorized: boolean;
status: BuildStatus | undefined;
selectionModel: string[];
setSelectionModel: (model: string[]) => void;
dialogOpen: "dashboard" | "add" | "rebuild" | "keyImport" | null;
setDialogOpen: (dialog: "dashboard" | "add" | "rebuild" | "keyImport" | null) => void;
selectedPackage: string | null;
setSelectedPackage: (base: string | null) => void;
paginationModel: { pageSize: number; page: number };
setPaginationModel: (model: { pageSize: number; page: number }) => void;
columnVisibility: Record<string, boolean>;
setColumnVisibility: (model: Record<string, boolean>) => void;
filterModel: GridFilterModel;
setFilterModel: (model: GridFilterModel) => void;
searchText: string;
setSearchText: (text: string) => void;
autoRefreshInterval: number;
onAutoRefreshIntervalChange: (interval: number) => void;
handleReload: () => void;
handleUpdate: () => Promise<void>;
handleRefreshDatabase: () => Promise<void>;
handleRemove: () => Promise<void>;
}
export function usePackageTable(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageTableResult {
const { rows, isLoading, isAuthorized, status, autoRefresh } = usePackageData(autoRefreshIntervals);
const tableState = useTableState();
const actions = usePackageActions(tableState.selectionModel, tableState.setSelectionModel);
// Pause auto-refresh when dialog is open
const isDialogOpen = tableState.dialogOpen !== null || tableState.selectedPackage !== null;
const setPaused = autoRefresh.setPaused;
useEffect(() => {
setPaused(isDialogOpen);
}, [isDialogOpen, setPaused]);
return {
rows,
isLoading,
isAuthorized,
status,
...tableState,
autoRefreshInterval: autoRefresh.interval,
onAutoRefreshIntervalChange: autoRefresh.setInterval,
...actions,
};
}

View File

@@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { RepositoryContext, type RepositoryContextValue } from "contexts/RepositoryContext";
import { useContextNotNull } from "hooks/useContextNotNull";
export function useRepository(): RepositoryContextValue {
return useContextNotNull(RepositoryContext);
}

View File

@@ -0,0 +1,48 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import { useRepository } from "hooks/useRepository";
import type { RepositoryId } from "models/RepositoryId";
import { useState } from "react";
export interface SelectedRepositoryResult {
selectedKey: string;
setSelectedKey: (key: string) => void;
selectedRepository: RepositoryId | null;
reset: () => void;
}
export function useSelectedRepository(): SelectedRepositoryResult {
const { repositories, current } = useRepository();
const [selectedKey, setSelectedKey] = useState("");
let selectedRepository: RepositoryId | null = current;
if (selectedKey) {
const repository = repositories.find(repository => repository.key === selectedKey);
if (repository) {
selectedRepository = repository;
}
}
const reset: () => void = () => {
setSelectedKey("");
};
return { selectedKey, setSelectedKey, selectedRepository, reset };
}

View File

@@ -0,0 +1,82 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { GridFilterModel } from "@mui/x-data-grid";
import { useLocalStorage } from "hooks/useLocalStorage";
import { useState } from "react";
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
export interface UseTableStateResult {
selectionModel: string[];
setSelectionModel: (model: string[]) => void;
dialogOpen: DialogType | null;
setDialogOpen: (dialog: DialogType | null) => void;
selectedPackage: string | null;
setSelectedPackage: (base: string | null) => void;
paginationModel: { pageSize: number; page: number };
setPaginationModel: (model: { pageSize: number; page: number }) => void;
columnVisibility: Record<string, boolean>;
setColumnVisibility: (model: Record<string, boolean>) => void;
filterModel: GridFilterModel;
setFilterModel: (model: GridFilterModel) => void;
searchText: string;
setSearchText: (text: string) => void;
}
export function useTableState(): UseTableStateResult {
const [selectionModel, setSelectionModel] = useState<string[]>([]);
const [dialogOpen, setDialogOpen] = useState<DialogType | null>(null);
const [selectedPackage, setSelectedPackage] = useState<string | null>(null);
const [searchText, setSearchText] = useState("");
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {
pageSize: 10,
page: 0,
});
const [columnVisibility, setColumnVisibility] = useLocalStorage<Record<string, boolean>>(
"ahriman-packages-columns",
{ groups: false, licenses: false, packager: false },
);
const [filterModel, setFilterModel] = useLocalStorage<GridFilterModel>(
"ahriman-packages-filters",
{ items: [] },
);
return {
selectionModel,
setSelectionModel,
dialogOpen,
setDialogOpen,
selectedPackage,
setSelectedPackage,
paginationModel,
setPaginationModel,
columnVisibility,
setColumnVisibility,
filterModel,
setFilterModel,
searchText,
setSearchText,
};
}

31
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,31 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import "chartSetup";
import "utils";
import App from "App";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,23 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface AURPackage {
package: string;
description: string;
}

View File

@@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface AuthInfo {
control: string;
enabled: boolean;
external: boolean;
username?: string;
}

View File

@@ -0,0 +1,24 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface AutoRefreshInterval {
interval: number;
is_active: boolean;
text: string;
}

View File

@@ -0,0 +1,20 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export type BuildStatus = "unknown" | "pending" | "building" | "failed" | "success";

View File

@@ -0,0 +1,23 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface Changes {
changes?: string;
last_commit_sha?: string;
}

View File

@@ -0,0 +1,27 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface Counters {
building: number;
failed: number;
pending: number;
success: number;
total: number;
unknown: number;
}

View File

@@ -0,0 +1,22 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface Dependencies {
paths: Record<string, string[]>;
}

View File

@@ -0,0 +1,26 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface Event {
created: number;
data?: Record<string, unknown>;
event: string;
message?: string;
object_id: string;
}

View File

@@ -0,0 +1,31 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { AuthInfo } from "models/AuthInfo";
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
import type { RepositoryId } from "models/RepositoryId";
export interface InfoResponse {
auth: AuthInfo;
repositories: RepositoryId[];
version: string;
autorefresh_intervals: AutoRefreshInterval[];
docs_enabled: boolean;
index_url?: string;
}

View File

@@ -0,0 +1,31 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { Counters } from "models/Counters";
import type { RepositoryStats } from "models/RepositoryStats";
import type { Status } from "models/Status";
export interface InternalStatus {
architecture: string;
repository: string;
packages: Counters;
stats: RepositoryStats;
status: Status;
version: string;
}

View File

@@ -0,0 +1,25 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface LogRecord {
created: number;
message: string;
process_id: string;
version: string;
}

View File

@@ -0,0 +1,23 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface LoginRequest {
username: string;
password: string;
}

View File

@@ -0,0 +1,27 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { AlertColor } from "@mui/material";
export interface Notification {
id: string;
title: string;
message: string;
severity: AlertColor;
}

View File

@@ -0,0 +1,22 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface PGPKey {
key: string;
}

View File

@@ -0,0 +1,23 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface PGPKeyRequest {
key: string;
server: string;
}

View File

@@ -0,0 +1,29 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { PackageProperties } from "models/PackageProperties";
import type { Remote } from "models/Remote";
export interface Package {
base: string;
packager?: string;
packages: Record<string, PackageProperties>;
remote: Remote;
version: string;
}

View File

@@ -0,0 +1,29 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { Patch } from "models/Patch";
export interface PackageActionRequest {
packages: string[];
patches?: Patch[];
refresh?: boolean;
aur?: boolean;
local?: boolean;
manual?: boolean;
}

View File

@@ -0,0 +1,35 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface PackageProperties {
architecture?: string;
archive_size?: number;
build_date?: number;
check_depends?: string[];
depends?: string[];
description?: string;
filename?: string;
groups?: string[];
installed_size?: number;
licenses?: string[];
make_depends?: string[];
opt_depends?: string[];
provides?: string[];
url?: string;
}

View File

@@ -0,0 +1,58 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { BuildStatus } from "models/BuildStatus";
import type { PackageStatus } from "models/PackageStatus";
export class PackageRow {
id: string;
base: string;
webUrl?: string;
version: string;
packages: string[];
groups: string[];
licenses: string[];
packager: string;
timestamp: string;
timestampValue: number;
status: BuildStatus;
constructor(descriptor: PackageStatus) {
this.id = descriptor.package.base;
this.base = descriptor.package.base;
this.webUrl = descriptor.package.remote.web_url ?? undefined;
this.version = descriptor.package.version;
this.packages = Object.keys(descriptor.package.packages).sort();
this.groups = PackageRow.extractListProperties(descriptor.package, "groups");
this.licenses = PackageRow.extractListProperties(descriptor.package, "licenses");
this.packager = descriptor.package.packager ?? "";
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
this.timestampValue = descriptor.status.timestamp;
this.status = descriptor.status.status;
}
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
return [
...new Set(
Object.values(pkg.packages)
.flatMap(properties => properties[property] ?? []),
),
].sort();
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export type PackageSource = "auto" | "archive" | "aur" | "directory" | "local" | "remote" | "repository";

View File

@@ -0,0 +1,26 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { Package } from "models/Package";
import type { Status } from "models/Status";
export interface PackageStatus {
package: Package;
status: Status;
}

View File

@@ -0,0 +1,23 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
export interface Patch {
key: string;
value: string;
}

View File

@@ -0,0 +1,28 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import type { PackageSource } from "models/PackageSource";
export interface Remote {
branch?: string;
git_url?: string;
path?: string;
source: PackageSource;
web_url?: string;
}

Some files were not shown because too many files have changed in this diff Show More