views: Add a menu entry and view for showing logs of an app

Tests:

- View logs menu entry is shown only for apps with daemons. It is now shown for
others such as Backups. It does not add menu for apps such as power.

- View logs entry for Date & Time shows show logs for multiple units. View logs
entry for Nextcloud shows many units.

- The textarea occupies full width. It is not editable. It is always scrolled to
the bottom. Control-A and Control-C selects all the text in it. It is re-sizable
vertically.

- The header shows unit name and unit description correctly.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
Sunil Mohan Adapa 2025-09-11 08:25:18 -07:00 committed by Veiko Aasa
parent 0661d7da7c
commit e82d959c85
No known key found for this signature in database
GPG Key ID: 478539CAE680674E
6 changed files with 90 additions and 1 deletions

View File

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<h2>{% trans "Logs" %}: {{ app_info.name }}</h2>
<p>
{% blocktrans trimmed %}
These are the last lines of the logs for services involved in this app.
If you want to report a bug, please use the <a
href="https://salsa.debian.org/freedombox-team/freedombox/issues">bug
tracker</a> and attach this log to the bug report.
{% endblocktrans %}
</p>
<div class="alert alert-warning d-flex align-items-center">
<div class="me-2">
<span class="fa fa-exclamation-triangle" aria-hidden="true"></span>
<span class="visually-hidden">{% trans "Caution:" %}</span>
</div>
<div>
{% blocktrans trimmed %}
Please remove any personal information from the log before submitting
the bug report.
{% endblocktrans %}
</div>
</div>
{% for component in logs %}
<section id="logs-section-{{ component.unit }}">
<h3>{{ component.unit }}: {{ component.description }}</h3>
<p>
<textarea class="log" readonly rows="10">{{ component.logs }}</textarea>
</p>
</section>
{% endfor %}
{% endblock %}

View File

@ -21,7 +21,7 @@
</a>
{% endif %}
{% if has_diagnostics or show_uninstall or has_backup_restore %}
{% if has_diagnostics or has_logs or show_uninstall or has_backup_restore %}
<!-- Single button -->
<div class="btn-group button-secondary dropdown">
<button type="button" class="btn btn-default dropdown-toggle"
@ -33,6 +33,13 @@
{% if has_diagnostics %}
{% include "diagnostics_button.html" with app_id=app_id enabled=is_enabled %}
{% endif %}
{% if has_logs %}
<a class="dropdown-item view-logs-item"
href="{% url 'logs' app_id=app_id %}"
title="{% trans "View Logs" %}">
{% trans "View Logs" %}
</a>
{% endif %}
{% if has_backup_restore %}
<a class="dropdown-item backup-item"
href="{% url 'backups:create' app_id=app_id %}"

View File

@ -30,6 +30,8 @@ urlpatterns = [
views.is_available_view, name='is-available'),
re_path(r'^rerun-setup/(?P<app_id>[1-9a-z\-_]+)/$', views.rerun_setup_view,
name='rerun-setup'),
re_path(r'^logs/(?P<app_id>[1-9a-z\-_]+)/$', views.AppLogsView.as_view(),
name='logs'),
# captcha urls are public
re_path(r'^captcha/image/(?P<key>\w+)/$', public(cviews.captcha_image),

View File

@ -446,6 +446,7 @@ class AppView(FormView):
context['is_running'] = app_is_running(self.app)
context['app_info'] = self.app.info
context['has_diagnostics'] = self.app.has_diagnostics()
context['has_logs'] = self.app.has_logs()
context['port_forwarding_info'] = get_port_forwarding_info(self.app)
context['app_enable_disable_form'] = self.get_enable_disable_form()
context['show_rerun_setup'] = True
@ -647,6 +648,25 @@ class UninstallView(FormView):
return super().form_valid(form)
class AppLogsView(TemplateView):
"""View for an app's logs.
This view shows logs for all the daemon units involved the app for easy
debugging.
"""
template_name = 'app-logs.html'
def get_context_data(self, *args, **kwargs):
"""Add additional context data for template."""
context = super().get_context_data(*args, **kwargs)
app_id = self.kwargs['app_id']
app = app_module.App.get(app_id)
context['app_info'] = app.info
context['logs'] = app.get_logs()
return context
def notification_dismiss(request, id):
"""Dismiss a notification."""
from .notification import Notification

View File

@ -1026,3 +1026,9 @@ img.notification-icon {
.accordion-header {
margin: 0;
}
/* Logs */
textarea.log {
width: 100%;
text-wrap: nowrap;
}

View File

@ -263,3 +263,15 @@ document.addEventListener('DOMContentLoaded', async () => {
error();
}
});
/*
* Text areas showing log lines have special behavior.
*/
document.addEventListener('DOMContentLoaded', function(event) {
const logElements = document.querySelectorAll('textarea.log');
// Scroll the textarea to the bottom so that last lines are visible.
for (const element of logElements) {
element.scrollTop = element.scrollHeight;
}
});