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
This commit is contained in:
Stefan Allius
2025-05-04 18:50:31 +02:00
committed by GitHub
parent e15db8c92a
commit 888e1475e4
16 changed files with 168 additions and 36 deletions

View File

@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased] ## [unreleased]
- Dashboard: add Notes page and table for important messages
- Dashboard: add Log-File page - Dashboard: add Log-File page
- Dashboard: add Connection page - Dashboard: add Connection page
- add web UI to add-on - add web UI to add-on

View File

@@ -6,7 +6,7 @@ babel:
build: build:
$(MAKE) -C ha_addons $@ $(MAKE) -C ha_addons $@
clean build: clean:
$(MAKE) -C app $@ $(MAKE) -C app $@
$(MAKE) -C ha_addons $@ $(MAKE) -C ha_addons $@

View File

@@ -7,6 +7,8 @@ Usage:
from quart import Quart, Blueprint from quart import Quart, Blueprint
from quart_babel import Babel from quart_babel import Babel
from utils import load_modules from utils import load_modules
from .log_handler import LogHandler
import logging
web = Blueprint('web', __name__) web = Blueprint('web', __name__)
@@ -30,3 +32,8 @@ class Web:
locale_selector=get_locale, locale_selector=get_locale,
timezone_selector=get_tz, timezone_selector=get_tz,
default_translation_directories=translation_directories) default_translation_directories=translation_directories)
h = LogHandler()
logging.getLogger().addHandler(h)
for name in logging.root.manager.loggerDict:
logging.getLogger(name).addHandler(h)

View File

@@ -4,6 +4,7 @@ from quart_babel import format_datetime, _
from infos import Infos from infos import Infos
from . import web from . import web
from .log_handler import LogHandler
def _get_device_icon(client_mode: bool): 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', data["conn-table"] = await render_template('templ_table.html.j2',
table=get_table_data()) 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 return data

View File

@@ -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:]

View File

@@ -4,6 +4,7 @@ from quart_babel import format_datetime, _
from mqtt import Mqtt from mqtt import Mqtt
from . import web from . import web
from .log_handler import LogHandler
def _get_row(inv: InverterBase): def _get_row(inv: InverterBase):
@@ -55,4 +56,9 @@ async def mqtt_fetch():
data["mqtt-table"] = await render_template('templ_table.html.j2', data["mqtt-table"] = await render_template('templ_table.html.j2',
table=get_table_data()) 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 return data

19
app/src/web/notes_list.py Normal file
View File

@@ -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

View File

@@ -18,6 +18,13 @@ async def mqtt():
fetch_url=url_for('.mqtt_fetch')) 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') @web.route('/logging')
async def logging(): async def logging():
return await render_template( return await render_template(

View File

@@ -57,7 +57,8 @@
<button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i>  Close Menu</button> <button href="#" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i>  Close Menu</button>
<a href="{{ url_for('.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-network-wired fa-fw"></i>  {{_('Connections')}}</a> <a href="{{ url_for('.index')}}" class="w3-bar-item w3-button w3-padding {% block menu1_class %}{% endblock %}"><i class="fa fa-network-wired fa-fw"></i>  {{_('Connections')}}</a>
<a href="{{ url_for('.mqtt')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-database fa-fw"></i>  MQTT</a> <a href="{{ url_for('.mqtt')}}" class="w3-bar-item w3-button w3-padding {% block menu2_class %}{% endblock %}"><i class="fa fa-database fa-fw"></i>  MQTT</a>
<a href="{{ url_for('.logging')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-file-export fa-fw"></i>  {{_('Log Files')}}</a> <a href="{{ url_for('.notes')}}" class="w3-bar-item w3-button w3-padding {% block menu3_class %}{% endblock %}"><i class="fa fa-exclamation-triangle fa-fw"></i>  {{_('Important Messages')}}</a>
<a href="{{ url_for('.logging')}}" class="w3-bar-item w3-button w3-padding {% block menu4_class %}{% endblock %}"><i class="fa fa-file-export fa-fw"></i>  {{_('Log Files')}}</a>
</div> </div>
</nav> </nav>

View File

@@ -1,6 +1,6 @@
{% extends 'base.html.j2' %} {% 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 menu1_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-network-wired"></i>  {{_('Proxy Connection Overview')}}{% endblock headline %} {% block headline %}<i class="fa fa-network-wired"></i>  {{_('Proxy Connection Overview')}}{% endblock headline %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.j2' %} {% extends 'base.html.j2' %}
{% block title %} TSUN Proxy - Log Files {% endblock title%} {% block title %}{{_("TSUN Proxy - Log Files")}}{% endblock title %}
{% block menu3_class %}w3-blue{% endblock %} {% block menu4_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-file-export fa-fw"></i>  {{_('Log Files')}}{% endblock headline %} {% block headline %}<i class="fa fa-file-export fa-fw"></i>  {{_('Log Files')}}{% endblock headline %}
{% block content %} {% block content %}
<div id="id01" class="w3-modal"> <div id="id01" class="w3-modal">

View File

@@ -1,6 +1,6 @@
{% extends 'base.html.j2' %} {% 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 menu2_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-database"></i>  {{_('MQTT Overview')}}{% endblock headline %} {% block headline %}<i class="fa fa-database"></i>  {{_('MQTT Overview')}}{% endblock headline %}
{% block content %} {% block content %}
@@ -45,6 +45,7 @@
</div> </div>
</div> </div>
</div> </div>
<div id="notes-list"></div>
<div id="mqtt-table"></div> <div id="mqtt-table"></div>
{% endblock content%} {% endblock content%}

View File

@@ -0,0 +1,10 @@
{% extends 'base.html.j2' %}
{% block title %}{{_("TSUN Proxy - Important Messages")}}{% endblock title %}
{% block menu3_class %}w3-blue{% endblock %}
{% block headline %}<i class="fa fa-exclamation-triangle fa-fw"></i>  {{_('Important Messages')}}{% endblock headline %}
{% block content %}
<div id="notes-list"></div>
{% endblock content%}
{% block footer %}{% endblock footer %}

View File

@@ -0,0 +1,23 @@
{% if notes|length > 0 %}
<div class="w3-container w3-margin-bottom">
<h5>{{_("Warnings and error messages")}} </h5>
<ul class="w3-ul w3-card-4">
{% for note in notes %}
<li class="{% if note.level is le(30) %}w3-leftbar w3-rightbar w3-pale-blue w3-border-blue{% else %}w3-leftbar w3-rightbar w3-pale-red w3-border-red{% endif %}">
<span class="w3-col" style="width:150px">{{note.ctime|datetimeformat(format='short')}}</span>
<span class="w3-col w3-hide-small" style="width:100px">{{note.lname|e}}</span>
<span class="w3-rest">{{note.msg|e}}</span>
</li>
{% endfor %}
</ul>
</div>
{% elif not hide_if_empty %}
<div class="w3-container w3-margin-bottom">
<div class="w3-leftbar w3-rightbar w3-pale-green w3-border-green">
<div class="w3-container">
<h2>{{_("Well done!")}}</h2>
<p>{{_("No warnings or errors have been logged since the last proxy start.")}}</p>
</div>
</div>
</div>
{% endif %}

View File

@@ -68,6 +68,13 @@ async def test_rel_page(client):
assert response.mimetype == 'text/html' assert response.mimetype == 'text/html'
web.build_relative_urls = False 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 @pytest.mark.asyncio
async def test_logging(client): async def test_logging(client):
"""Test the logging page route.""" """Test the logging page route."""
@@ -185,6 +192,15 @@ async def test_mqtt_fetch(client, create_inverter):
assert response.status_code == 200 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 @pytest.mark.asyncio
async def test_file_fetch(client, config_conn): async def test_file_fetch(client, config_conn):
"""Test the data-fetch route.""" """Test the data-fetch route."""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: tsun-gen3-proxy 0.14.0\n" "Project-Id-Version: tsun-gen3-proxy 0.14.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: 2025-04-18 16:24+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n" "Language: de\n"
@@ -19,39 +19,39 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\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" msgid "Connections"
msgstr "Verbindungen" msgstr "Verbindungen"
#: src/web/conn_table.py:58 #: src/web/conn_table.py:59
msgid "Device-IP:Port" msgid "Device-IP:Port"
msgstr "Geräte-IP:Port" msgstr "Geräte-IP:Port"
#: src/web/conn_table.py:58 #: src/web/conn_table.py:59
msgid "Device-IP" msgid "Device-IP"
msgstr "Geräte-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" msgid "Serial-No"
msgstr "Seriennummer" msgstr "Seriennummer"
#: src/web/conn_table.py:60 #: src/web/conn_table.py:61
msgid "Cloud-IP:Port" msgid "Cloud-IP:Port"
msgstr "Cloud-IP:Port" msgstr "Cloud-IP:Port"
#: src/web/conn_table.py:60 #: src/web/conn_table.py:61
msgid "Cloud-IP" msgid "Cloud-IP"
msgstr "Cloud-IP" msgstr "Cloud-IP"
#: src/web/mqtt_table.py:26 #: src/web/mqtt_table.py:27
msgid "MQTT devices" msgid "MQTT devices"
msgstr "MQTT Geräte" msgstr "MQTT Geräte"
#: src/web/mqtt_table.py:34 #: src/web/mqtt_table.py:35
msgid "Node-ID" msgid "Node-ID"
msgstr "" msgstr ""
#: src/web/mqtt_table.py:35 #: src/web/mqtt_table.py:36
msgid "HA-Area" msgid "HA-Area"
msgstr "" msgstr ""
@@ -63,10 +63,18 @@ msgstr "Aktualisiert:"
msgid "Version:" msgid "Version:"
msgstr "" 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" msgid "Log Files"
msgstr "Log Dateien" 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 #: src/web/templates/page_index.html.j2:5
msgid "Proxy Connection Overview" msgid "Proxy Connection Overview"
msgstr "Proxy Verbindungen" msgstr "Proxy Verbindungen"
@@ -103,6 +111,10 @@ msgstr "Emu Modus"
msgid "Emulation sends data to cloud" msgid "Emulation sends data to cloud"
msgstr "Emulation sendet in die 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 #: src/web/templates/page_logging.html.j2:10
msgid "Do you really want to delete the log file" msgid "Do you really want to delete the log file"
msgstr "Soll die Datei wirklich gelöscht werden" msgstr "Soll die Datei wirklich gelöscht werden"
@@ -115,6 +127,10 @@ msgstr "File löschen"
msgid "Abort" msgid "Abort"
msgstr "Abbruch" msgstr "Abbruch"
#: src/web/templates/page_mqtt.html.j2:3
msgid "TSUN Proxy - MQTT Status"
msgstr ""
#: src/web/templates/page_mqtt.html.j2:5 #: src/web/templates/page_mqtt.html.j2:5
msgid "MQTT Overview" msgid "MQTT Overview"
msgstr "MQTT Überblick" msgstr "MQTT Überblick"
@@ -143,6 +159,10 @@ msgstr "Empfangene Topics"
msgid "Number of topics received" msgid "Number of topics received"
msgstr "Anzahl der empfangenen Topics" 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 #: src/web/templates/templ_log_files_list.html.j2:11
msgid "Created" msgid "Created"
msgstr "Erzeugt" msgstr "Erzeugt"
@@ -159,24 +179,17 @@ msgstr "Größe"
msgid "Download File" msgid "Download File"
msgstr "Datei Download" msgstr "Datei Download"
#~ msgid "MQTT Server" #: src/web/templates/templ_notes_list.html.j2:3
#~ msgstr "" msgid "Warnings and error messages"
msgstr "Warnungen und Fehlermeldungen"
#~ msgid "MQTT User" #: src/web/templates/templ_notes_list.html.j2:18
#~ msgstr "" msgid "Well done!"
msgstr "Gut gemacht!"
#~ msgid "MQTT Connected" #: src/web/templates/templ_notes_list.html.j2:19
#~ msgstr "" msgid "No warnings or errors have been logged since the last proxy start."
msgstr ""
#~ msgid "Home Assistant Status" "Seit dem letzten Proxystart wurden keine Warnungen oder Fehler "
#~ msgstr "" "protokolliert."
#~ msgid "MQTT Publish Count"
#~ msgstr ""
#~ msgid "MQTT Reveiced Count"
#~ msgstr ""
#~ msgid "MQTT Connect Time"
#~ msgstr "MQTT Verbindungszeit"