diff --git a/package/share/ahriman/templates/build-status/package-info-modal.jinja2 b/package/share/ahriman/templates/build-status/package-info-modal.jinja2
index 4a968736..315cf05f 100644
--- a/package/share/ahriman/templates/build-status/package-info-modal.jinja2
+++ b/package/share/ahriman/templates/build-status/package-info-modal.jinja2
@@ -97,7 +97,7 @@
-
+
{% endif %}
{% if autorefresh_intervals %}
@@ -315,6 +315,69 @@
}
function loadLogs(packageBase, onFailure) {
+ const sortFn = (left, right) => left.process_id.localeCompare(right.process_id) || left.version.localeCompare(right.version);
+ const compareFn = (left, right) => left.process_id === right.process_id && left.version === right.version;
+
+ makeRequest(
+ `/api/v2/packages/${packageBase}/logs`,
+ {
+ query: {
+ architecture: repository.architecture,
+ head: true,
+ repository: repository.repository,
+ },
+ convert: response => response.json(),
+ },
+ data => {
+ const currentVersions = Array.from(packageInfoLogsVersions.children)
+ .map(el => {
+ return {
+ process_id: el.dataset.processId,
+ version: el.dataset.version,
+ };
+ })
+ .sort(sortFn);
+ const newVersions = data
+ .map(el => {
+ return {
+ process_id: el.process_id,
+ version: el.version,
+ };
+ })
+ .sort(sortFn);
+
+ if (currentVersions.equals(newVersions, compareFn))
+ loadLogsActive(packageBase);
+ else
+ loadLogsAll(packageBase, onFailure);
+ },
+ )
+ }
+
+ function loadLogsActive(packageBase) {
+ const activeLogSelector = packageInfoLogsVersions.querySelector(".active");
+
+ if (activeLogSelector) {
+ makeRequest(
+ `/api/v2/packages/${packageBase}/logs`,
+ {
+ query: {
+ architecture: repository.architecture,
+ repository: repository.repository,
+ version: activeLogSelector.dataset.version,
+ process_id: activeLogSelector.dataset.processId,
+ },
+ convert: response => response.json(),
+ },
+ data => {
+ activeLogSelector.dataset.logs = convertLogs(data);
+ activeLogSelector.click();
+ },
+ );
+ }
+ }
+
+ function loadLogsAll(packageBase, onFailure) {
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
@@ -440,29 +503,6 @@
packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked});
}
- function reloadActiveLogs(packageBase) {
- const activeLogSelector = packageInfoLogsVersions.querySelector(".active");
-
- if (activeLogSelector) {
- makeRequest(
- `/api/v2/packages/${packageBase}/logs`,
- {
- query: {
- architecture: repository.architecture,
- repository: repository.repository,
- version: activeLogSelector.dataset.version,
- process_id: activeLogSelector.dataset.processId,
- },
- convert: response => response.json(),
- },
- data => {
- activeLogSelector.dataset.logs = convertLogs(data);
- activeLogSelector.click();
- },
- );
- }
- }
-
function showPackageInfo(packageBase) {
const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet) {
@@ -502,7 +542,7 @@
const packageBase = packageInfoModal.dataset.package;
// we only poll status and logs here
loadPackage(packageBase);
- reloadActiveLogs(packageBase);
+ loadLogs(packageBase);
}
});
}
diff --git a/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 b/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2
index ebc35469..2a47bdd3 100644
--- a/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2
+++ b/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2
@@ -218,6 +218,21 @@
});
}
+ Array.prototype.equals = function (right, comparator) {
+ let index = this.length;
+ if (index !== right.length) {
+ return false;
+ }
+
+ while (index--) {
+ if (!comparator(this[index], right[index])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
Date.prototype.toISOStringShort = function () {
const pad = number => String(number).padStart(2, "0");
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
diff --git a/src/ahriman/web/schemas/logs_search_schema.py b/src/ahriman/web/schemas/logs_search_schema.py
index 9b570d2e..4dc293a9 100644
--- a/src/ahriman/web/schemas/logs_search_schema.py
+++ b/src/ahriman/web/schemas/logs_search_schema.py
@@ -27,6 +27,9 @@ class LogsSearchSchema(PaginationSchema):
request log search schema
"""
+ head = fields.Boolean(metadata={
+ "description": "Return versions only without fetching logs themselves",
+ })
version = fields.String(metadata={
"description": "Package version to search",
"example": __version__,
diff --git a/src/ahriman/web/views/v2/packages/logs.py b/src/ahriman/web/views/v2/packages/logs.py
index 0454422d..fdb2ce43 100644
--- a/src/ahriman/web/views/v2/packages/logs.py
+++ b/src/ahriman/web/views/v2/packages/logs.py
@@ -17,7 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
+import itertools
+
from aiohttp.web import Response, json_response
+from dataclasses import replace
from typing import ClassVar
from ahriman.models.user_access import UserAccess
@@ -28,7 +31,8 @@ from ahriman.web.views.status_view_guard import StatusViewGuard
class LogsView(StatusViewGuard, BaseView):
- """
+ """ else:
+
package logs web view
Attributes:
@@ -66,5 +70,14 @@ class LogsView(StatusViewGuard, BaseView):
logs = self.service(package_base=package_base).package_logs_get(package_base, version, process, limit, offset)
+ head = self.request.query.get("head", "false")
+ # pylint: disable=protected-access
+ if self.configuration._convert_to_boolean(head): # type: ignore[attr-defined]
+ # logs should be sorted already
+ logs = [
+ replace(next(log_records), message="") # remove messages
+ for _, log_records in itertools.groupby(logs, lambda log_record: log_record.log_record_id)
+ ]
+
response = [log_record.view() for log_record in logs]
return json_response(response)
diff --git a/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py b/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py
index bf7a9905..c1b8b14c 100644
--- a/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py
+++ b/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py
@@ -139,3 +139,41 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs")
assert response.status == 404
assert not response_schema.validate(await response.json())
+
+
+async def test_get_head(client: TestClient, package_ahriman: Package) -> None:
+ """
+ must return only versions if head parameter is set
+ """
+ await client.post(f"/api/v1/packages/{package_ahriman.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
+ await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
+ json={"created": 42.0, "message": "message 1", "version": "42"})
+ await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
+ json={"created": 43.0, "message": "message 2", "version": "42"})
+ await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
+ json={"created": 44.0, "message": "message 3", "version": "43"})
+ request_schema = pytest.helpers.schema_request(LogsView.get, location="querystring")
+ response_schema = pytest.helpers.schema_response(LogsView.get)
+
+ payload = {"head": "true"}
+ assert not request_schema.validate(payload)
+ response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params=payload)
+ assert response.status == 200
+
+ logs = await response.json()
+ assert not response_schema.validate(logs)
+ assert logs == [
+ {
+ "created": 42.0,
+ "message": "",
+ "version": "42",
+ "process_id": LogRecordId.DEFAULT_PROCESS_ID,
+ },
+ {
+ "created": 44.0,
+ "message": "",
+ "version": "43",
+ "process_id": LogRecordId.DEFAULT_PROCESS_ID,
+ },
+ ]