From 888e1475e45661e1c5746f86a57e7d40e37504a5 Mon Sep 17 00:00:00 2001 From: Stefan Allius <122395479+s-allius@users.noreply.github.com> Date: Sun, 4 May 2025 18:50:31 +0200 Subject: [PATCH] S allius/issue397 (#405) * add Dashboards log handler to all known loggers * add list of last 3 warnings/errors to page * add note list to page * create LogHandler for the dashboard - simple memory log handler which stores the last 64 warnings/errors for the dashboard * render warnings/errors as note list * add page for warnings and errors * fix double defined build target * add well done message if no errors in the logs * translate page titles * more translations * add Notes page and table for important messages * add unit tests --- CHANGELOG.md | 1 + Makefile | 2 +- app/src/web/__init__.py | 7 ++ app/src/web/conn_table.py | 6 +- app/src/web/log_handler.py | 24 +++++++ app/src/web/mqtt_table.py | 6 ++ app/src/web/notes_list.py | 19 +++++ app/src/web/pages.py | 7 ++ app/src/web/templates/base.html.j2 | 3 +- app/src/web/templates/page_index.html.j2 | 2 +- app/src/web/templates/page_logging.html.j2 | 4 +- app/src/web/templates/page_mqtt.html.j2 | 3 +- app/src/web/templates/page_notes.html.j2 | 10 +++ .../web/templates/templ_notes_list.html.j2 | 23 ++++++ app/tests/test_web_route.py | 16 +++++ app/translations/de/LC_MESSAGES/messages.po | 71 +++++++++++-------- 16 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 app/src/web/log_handler.py create mode 100644 app/src/web/notes_list.py create mode 100644 app/src/web/templates/page_notes.html.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index eedb850..46be5df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Dashboard: add Notes page and table for important messages - Dashboard: add Log-File page - Dashboard: add Connection page - add web UI to add-on diff --git a/Makefile b/Makefile index b98463f..5964e6e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ babel: build: $(MAKE) -C ha_addons $@ -clean build: +clean: $(MAKE) -C app $@ $(MAKE) -C ha_addons $@ diff --git a/app/src/web/__init__.py b/app/src/web/__init__.py index 24b0e87..ef3728c 100644 --- a/app/src/web/__init__.py +++ b/app/src/web/__init__.py @@ -7,6 +7,8 @@ Usage: from quart import Quart, Blueprint from quart_babel import Babel from utils import load_modules +from .log_handler import LogHandler +import logging web = Blueprint('web', __name__) @@ -30,3 +32,8 @@ class Web: locale_selector=get_locale, timezone_selector=get_tz, default_translation_directories=translation_directories) + + h = LogHandler() + logging.getLogger().addHandler(h) + for name in logging.root.manager.loggerDict: + logging.getLogger(name).addHandler(h) diff --git a/app/src/web/conn_table.py b/app/src/web/conn_table.py index ae6fe38..4b81868 100644 --- a/app/src/web/conn_table.py +++ b/app/src/web/conn_table.py @@ -4,6 +4,7 @@ from quart_babel import format_datetime, _ from infos import Infos from . import web +from .log_handler import LogHandler def _get_device_icon(client_mode: bool): @@ -79,5 +80,8 @@ async def data_fetch(): data["conn-table"] = await render_template('templ_table.html.j2', table=get_table_data()) - data["notes-list"] = await render_template('templ_notes_list.html.j2') + data["notes-list"] = await render_template( + 'templ_notes_list.html.j2', + notes=LogHandler().get_buffer(3), + hide_if_empty=True) return data diff --git a/app/src/web/log_handler.py b/app/src/web/log_handler.py new file mode 100644 index 0000000..7565649 --- /dev/null +++ b/app/src/web/log_handler.py @@ -0,0 +1,24 @@ +from logging import Handler +from logging import LogRecord +import logging +from collections import deque + +from singleton import Singleton + + +class LogHandler(Handler, metaclass=Singleton): + def __init__(self, capacity=64): + super().__init__(logging.WARNING) + self.capacity = capacity + self.buffer = deque(maxlen=capacity) + + def emit(self, record: LogRecord): + self.buffer.append({ + 'ctime': record.created, + 'level': record.levelno, + 'lname': record.levelname, + 'msg': record.getMessage() + }) + + def get_buffer(self, elms=0) -> list: + return list(self.buffer)[-elms:] diff --git a/app/src/web/mqtt_table.py b/app/src/web/mqtt_table.py index fcd0477..8370c17 100644 --- a/app/src/web/mqtt_table.py +++ b/app/src/web/mqtt_table.py @@ -4,6 +4,7 @@ from quart_babel import format_datetime, _ from mqtt import Mqtt from . import web +from .log_handler import LogHandler def _get_row(inv: InverterBase): @@ -55,4 +56,9 @@ async def mqtt_fetch(): data["mqtt-table"] = await render_template('templ_table.html.j2', table=get_table_data()) + data["notes-list"] = await render_template( + 'templ_notes_list.html.j2', + notes=LogHandler().get_buffer(3), + hide_if_empty=True) + return data diff --git a/app/src/web/notes_list.py b/app/src/web/notes_list.py new file mode 100644 index 0000000..e96c319 --- /dev/null +++ b/app/src/web/notes_list.py @@ -0,0 +1,19 @@ +from quart import render_template +from quart_babel import format_datetime + +from . import web +from .log_handler import LogHandler + + +@web.route('/notes-fetch') +async def notes_fetch(): + data = { + "update-time": format_datetime(format="medium"), + } + + data["notes-list"] = await render_template( + 'templ_notes_list.html.j2', + notes=LogHandler().get_buffer(), + hide_if_empty=False) + + return data diff --git a/app/src/web/pages.py b/app/src/web/pages.py index f365239..49d720a 100644 --- a/app/src/web/pages.py +++ b/app/src/web/pages.py @@ -18,6 +18,13 @@ async def mqtt(): fetch_url=url_for('.mqtt_fetch')) +@web.route('/notes') +async def notes(): + return await render_template( + 'page_notes.html.j2', + fetch_url=url_for('.notes_fetch')) + + @web.route('/logging') async def logging(): return await render_template( diff --git a/app/src/web/templates/base.html.j2 b/app/src/web/templates/base.html.j2 index 34b6f57..0ef1f7e 100644 --- a/app/src/web/templates/base.html.j2 +++ b/app/src/web/templates/base.html.j2 @@ -57,7 +57,8 @@   {{_('Connections')}}   MQTT -   {{_('Log Files')}} +   {{_('Important Messages')}} +   {{_('Log Files')}} diff --git a/app/src/web/templates/page_index.html.j2 b/app/src/web/templates/page_index.html.j2 index f8364c5..65cae0d 100644 --- a/app/src/web/templates/page_index.html.j2 +++ b/app/src/web/templates/page_index.html.j2 @@ -1,6 +1,6 @@ {% extends 'base.html.j2' %} -{% block title %} TSUN Proxy - Connections {% endblock title%} +{% block title %}{{_("TSUN Proxy - Connections")}}{% endblock title %} {% block menu1_class %}w3-blue{% endblock %} {% block headline %}  {{_('Proxy Connection Overview')}}{% endblock headline %} diff --git a/app/src/web/templates/page_logging.html.j2 b/app/src/web/templates/page_logging.html.j2 index 720a1e6..f80763c 100644 --- a/app/src/web/templates/page_logging.html.j2 +++ b/app/src/web/templates/page_logging.html.j2 @@ -1,7 +1,7 @@ {% extends 'base.html.j2' %} -{% block title %} TSUN Proxy - Log Files {% endblock title%} -{% block menu3_class %}w3-blue{% endblock %} +{% block title %}{{_("TSUN Proxy - Log Files")}}{% endblock title %} +{% block menu4_class %}w3-blue{% endblock %} {% block headline %}  {{_('Log Files')}}{% endblock headline %} {% block content %}
diff --git a/app/src/web/templates/page_mqtt.html.j2 b/app/src/web/templates/page_mqtt.html.j2 index f1010de..0f23492 100644 --- a/app/src/web/templates/page_mqtt.html.j2 +++ b/app/src/web/templates/page_mqtt.html.j2 @@ -1,6 +1,6 @@ {% extends 'base.html.j2' %} -{% block title %} TSUN Proxy - MQTT Status {% endblock title%} +{% block title %}{{_("TSUN Proxy - MQTT Status")}}{% endblock title %} {% block menu2_class %}w3-blue{% endblock %} {% block headline %}  {{_('MQTT Overview')}}{% endblock headline %} {% block content %} @@ -45,6 +45,7 @@
+
{% endblock content%} diff --git a/app/src/web/templates/page_notes.html.j2 b/app/src/web/templates/page_notes.html.j2 new file mode 100644 index 0000000..495e5e2 --- /dev/null +++ b/app/src/web/templates/page_notes.html.j2 @@ -0,0 +1,10 @@ +{% extends 'base.html.j2' %} + +{% block title %}{{_("TSUN Proxy - Important Messages")}}{% endblock title %} +{% block menu3_class %}w3-blue{% endblock %} +{% block headline %}  {{_('Important Messages')}}{% endblock headline %} +{% block content %} +
+{% endblock content%} + +{% block footer %}{% endblock footer %} diff --git a/app/src/web/templates/templ_notes_list.html.j2 b/app/src/web/templates/templ_notes_list.html.j2 index e69de29..6d5d98e 100644 --- a/app/src/web/templates/templ_notes_list.html.j2 +++ b/app/src/web/templates/templ_notes_list.html.j2 @@ -0,0 +1,23 @@ +{% if notes|length > 0 %} +
+
{{_("Warnings and error messages")}}
+ +
+{% elif not hide_if_empty %} +
+
+
+

{{_("Well done!")}}

+

{{_("No warnings or errors have been logged since the last proxy start.")}}

+
+
+
+{% endif %} \ No newline at end of file diff --git a/app/tests/test_web_route.py b/app/tests/test_web_route.py index cf5f307..4ed9b36 100644 --- a/app/tests/test_web_route.py +++ b/app/tests/test_web_route.py @@ -68,6 +68,13 @@ async def test_rel_page(client): assert response.mimetype == 'text/html' web.build_relative_urls = False +@pytest.mark.asyncio +async def test_notes(client): + """Test the notes page route.""" + response = await client.get('/notes') + assert response.status_code == 200 + assert response.mimetype == 'text/html' + @pytest.mark.asyncio async def test_logging(client): """Test the logging page route.""" @@ -185,6 +192,15 @@ async def test_mqtt_fetch(client, create_inverter): assert response.status_code == 200 +@pytest.mark.asyncio +async def test_notes_fetch(client, config_conn): + """Test the notes-fetch route.""" + _ = create_inverter + + response = await client.get('/notes-fetch') + assert response.status_code == 200 + + @pytest.mark.asyncio async def test_file_fetch(client, config_conn): """Test the data-fetch route.""" diff --git a/app/translations/de/LC_MESSAGES/messages.po b/app/translations/de/LC_MESSAGES/messages.po index f4284ab..8daf333 100644 --- a/app/translations/de/LC_MESSAGES/messages.po +++ b/app/translations/de/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: tsun-gen3-proxy 0.14.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-05-03 21:59+0200\n" +"POT-Creation-Date: 2025-05-04 18:16+0200\n" "PO-Revision-Date: 2025-04-18 16:24+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -19,39 +19,39 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" -#: src/web/conn_table.py:51 src/web/templates/base.html.j2:58 +#: src/web/conn_table.py:52 src/web/templates/base.html.j2:58 msgid "Connections" msgstr "Verbindungen" -#: src/web/conn_table.py:58 +#: src/web/conn_table.py:59 msgid "Device-IP:Port" msgstr "Geräte-IP:Port" -#: src/web/conn_table.py:58 +#: src/web/conn_table.py:59 msgid "Device-IP" msgstr "Geräte-IP" -#: src/web/conn_table.py:59 src/web/mqtt_table.py:33 +#: src/web/conn_table.py:60 src/web/mqtt_table.py:34 msgid "Serial-No" msgstr "Seriennummer" -#: src/web/conn_table.py:60 +#: src/web/conn_table.py:61 msgid "Cloud-IP:Port" msgstr "Cloud-IP:Port" -#: src/web/conn_table.py:60 +#: src/web/conn_table.py:61 msgid "Cloud-IP" msgstr "Cloud-IP" -#: src/web/mqtt_table.py:26 +#: src/web/mqtt_table.py:27 msgid "MQTT devices" msgstr "MQTT Geräte" -#: src/web/mqtt_table.py:34 +#: src/web/mqtt_table.py:35 msgid "Node-ID" msgstr "" -#: src/web/mqtt_table.py:35 +#: src/web/mqtt_table.py:36 msgid "HA-Area" msgstr "" @@ -63,10 +63,18 @@ msgstr "Aktualisiert:" msgid "Version:" msgstr "" -#: src/web/templates/base.html.j2:60 src/web/templates/page_logging.html.j2:5 +#: src/web/templates/base.html.j2:60 src/web/templates/page_notes.html.j2:5 +msgid "Important Messages" +msgstr "Wichtige Hinweise" + +#: src/web/templates/base.html.j2:61 src/web/templates/page_logging.html.j2:5 msgid "Log Files" msgstr "Log Dateien" +#: src/web/templates/page_index.html.j2:3 +msgid "TSUN Proxy - Connections" +msgstr "TSUN Proxy - Verbindungen" + #: src/web/templates/page_index.html.j2:5 msgid "Proxy Connection Overview" msgstr "Proxy Verbindungen" @@ -103,6 +111,10 @@ msgstr "Emu Modus" msgid "Emulation sends data to cloud" msgstr "Emulation sendet in die Cloud" +#: src/web/templates/page_logging.html.j2:3 +msgid "TSUN Proxy - Log Files" +msgstr "TSUN Proxy - Log Dateien" + #: src/web/templates/page_logging.html.j2:10 msgid "Do you really want to delete the log file" msgstr "Soll die Datei wirklich gelöscht werden" @@ -115,6 +127,10 @@ msgstr "File löschen" msgid "Abort" msgstr "Abbruch" +#: src/web/templates/page_mqtt.html.j2:3 +msgid "TSUN Proxy - MQTT Status" +msgstr "" + #: src/web/templates/page_mqtt.html.j2:5 msgid "MQTT Overview" msgstr "MQTT Überblick" @@ -143,6 +159,10 @@ msgstr "Empfangene Topics" msgid "Number of topics received" msgstr "Anzahl der empfangenen Topics" +#: src/web/templates/page_notes.html.j2:3 +msgid "TSUN Proxy - Important Messages" +msgstr "TSUN Proxy - Wichtige Hinweise" + #: src/web/templates/templ_log_files_list.html.j2:11 msgid "Created" msgstr "Erzeugt" @@ -159,24 +179,17 @@ msgstr "Größe" msgid "Download File" msgstr "Datei Download" -#~ msgid "MQTT Server" -#~ msgstr "" +#: src/web/templates/templ_notes_list.html.j2:3 +msgid "Warnings and error messages" +msgstr "Warnungen und Fehlermeldungen" -#~ msgid "MQTT User" -#~ msgstr "" +#: src/web/templates/templ_notes_list.html.j2:18 +msgid "Well done!" +msgstr "Gut gemacht!" -#~ msgid "MQTT Connected" -#~ msgstr "" - -#~ msgid "Home Assistant Status" -#~ msgstr "" - -#~ msgid "MQTT Publish Count" -#~ msgstr "" - -#~ msgid "MQTT Reveiced Count" -#~ msgstr "" - -#~ msgid "MQTT Connect Time" -#~ msgstr "MQTT Verbindungszeit" +#: src/web/templates/templ_notes_list.html.j2:19 +msgid "No warnings or errors have been logged since the last proxy start." +msgstr "" +"Seit dem letzten Proxystart wurden keine Warnungen oder Fehler " +"protokolliert."