Compare commits

...

8 Commits

Author SHA1 Message Date
Stefan Allius
1c6ebcd486 Merge branch 'main' of https://github.com/s-allius/tsun-gen3-proxy into s-allius/issue396 2025-05-13 22:51:39 +02:00
Stefan Allius
2a96fd2d3c improve translation of delete modal 2025-05-13 22:48:32 +02:00
Stefan Allius
4371f3dadb S allius/issue396 (#412)
* add title to table icons

* optimize datetime formatting

* change icons

* translate n/a
2025-05-13 21:38:33 +02:00
Stefan Allius
caf88e2849 translate n/a 2025-05-13 21:33:45 +02:00
Stefan Allius
b7f7dd0441 change icons 2025-05-13 21:33:24 +02:00
Stefan Allius
207bc51c42 optimize datetime formatting 2025-05-13 21:32:56 +02:00
Stefan Allius
a0afe71654 add title to table icons 2025-05-13 21:32:17 +02:00
Stefan Allius
907dcb1623 S allius/issue409 (#411)
* scan log files for timestamp as creating timestamp

* increase test coverage

* add an empty file for unit tests

- the empty file is needed for unit tests to force
  an exception on the try to scan the first line
  for an timestamp

* set timezone of scanned creation time
2025-05-13 00:38:06 +02:00
8 changed files with 95 additions and 34 deletions

View File

@@ -10,39 +10,40 @@ from .log_handler import LogHandler
def _get_device_icon(client_mode: bool): def _get_device_icon(client_mode: bool):
'''returns the icon for the device conntection''' '''returns the icon for the device conntection'''
if client_mode: if client_mode:
return 'fa-download fa-rotate-180' return 'fa-download fa-rotate-180', 'Server Mode'
return 'fa-upload fa-rotate-180' return 'fa-upload fa-rotate-180', 'Client Mode'
def _get_cloud_icon(emu_mode: bool): def _get_cloud_icon(emu_mode: bool):
'''returns the icon for the cloud conntection''' '''returns the icon for the cloud conntection'''
if emu_mode: if emu_mode:
return 'fa-cloud-arrow-up-alt' return 'fa-cloud-arrow-up-alt', 'Emu Mode'
return 'fa-cloud' return 'fa-cloud', 'Proxy Mode'
def _get_row(inv: InverterBase): def _get_row(inv: InverterBase):
'''build one row for the connection table''' '''build one row for the connection table'''
client_mode = inv.client_mode client_mode = inv.client_mode
inv_serial = inv.local.stream.inv_serial inv_serial = inv.local.stream.inv_serial
icon1 = _get_device_icon(client_mode) icon1, descr1 = _get_device_icon(client_mode)
ip1, port1 = inv.addr ip1, port1 = inv.addr
icon2 = '' icon2 = ''
descr2 = ''
ip2 = '--' ip2 = '--'
port2 = '--' port2 = '--'
if inv.remote.ifc: if inv.remote.ifc:
ip2, port2 = inv.remote.ifc.r_addr ip2, port2 = inv.remote.ifc.r_addr
icon2 = _get_cloud_icon(client_mode) icon2, descr2 = _get_cloud_icon(client_mode)
row = [] row = []
row.append(f'<i class="fa {icon1}"></i> {ip1}:{port1}') row.append(f'<i class="fa {icon1}" title="{_(descr1)}"></i> {ip1}:{port1}')
row.append(f'<i class="fa {icon1}"></i> {ip1}') row.append(f'<i class="fa {icon1}" title="{_(descr1)}"></i> {ip1}')
row.append(inv_serial) row.append(inv_serial)
row.append(f'<i class="fa {icon2}"></i> {ip2}:{port2}') row.append(f'<i class="fa {icon2}" title="{_(descr2)}"></i> {ip2}:{port2}')
row.append(f'<i class="fa {icon2}"></i> {ip2}') row.append(f'<i class="fa {icon2}" title="{_(descr2)}"></i> {ip2}')
return row return row

View File

@@ -1,26 +1,58 @@
from quart import render_template from quart import render_template
from quart_babel import format_datetime, format_decimal from quart_babel import format_datetime, format_decimal, _
from quart.helpers import send_from_directory from quart.helpers import send_from_directory
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from cnf.config import Config from cnf.config import Config
from datetime import datetime
from os import DirEntry
import os import os
from dateutil import tz
from . import web from . import web
def _get_file(file): def _get_birth_from_log(path: str) -> None | datetime:
'''read timestamp from the first line of a log file'''
dt = None
try:
with open(path) as f:
first_line = f.readline()
first_line = first_line.lstrip("'")
fmt = "%Y-%m-%d %H:%M:%S" if first_line[4] == '-' \
else "%d-%m-%Y %H:%M:%S"
dt = datetime.strptime(first_line[0:19], fmt). \
replace(tzinfo=tz.tzlocal())
except Exception:
pass
return dt
def _get_file(file: DirEntry) -> dict:
'''build one row for the connection table''' '''build one row for the connection table'''
entry = {} entry = {}
entry['name'] = file.name entry['name'] = file.name
stat = file.stat() stat = file.stat()
entry['size'] = format_decimal(stat.st_size) entry['size'] = format_decimal(stat.st_size)
entry['date'] = stat.st_mtime try:
entry['created'] = format_datetime(stat.st_ctime, format="short") dt = stat.st_birthtime
except Exception:
dt = _get_birth_from_log(file.path)
if dt:
entry['created'] = format_datetime(dt, format="short")
# sort by creating date, if available
entry['date'] = dt if isinstance(dt, float) else dt.timestamp()
else:
entry['created'] = _('n/a')
entry['date'] = stat.st_mtime
entry['modified'] = format_datetime(stat.st_mtime, format="short") entry['modified'] = format_datetime(stat.st_mtime, format="short")
return entry return entry
def get_list_data(): def get_list_data() -> list:
'''build the connection table''' '''build the connection table'''
file_list = [] file_list = []
with os.scandir(Config.get_log_path()) as it: with os.scandir(Config.get_log_path()) as it:

View File

@@ -46,10 +46,13 @@ def get_table_data():
@web.route('/mqtt-fetch') @web.route('/mqtt-fetch')
async def mqtt_fetch(): async def mqtt_fetch():
mqtt = Mqtt(None) mqtt = Mqtt(None)
ctime = format_datetime(dt=mqtt.ctime, format='short') cdatetime = format_datetime(dt=mqtt.ctime, format='d.MM. HH:mm')
data = { data = {
"update-time": format_datetime(format="medium"), "update-time": format_datetime(format="medium"),
"mqtt-ctime": f"<h3>{ctime}</h3>", "mqtt-ctime": f"""
<h3 class="w3-hide-small w3-hide-medium">{cdatetime}</h3>
<h4 class="w3-hide-large">{cdatetime}</h4>
""",
"mqtt-tx": f"<h3>{mqtt.published}</h3>", "mqtt-tx": f"<h3>{mqtt.published}</h3>",
"mqtt-rx": f"<h3>{mqtt.received}</h3>", "mqtt-rx": f"<h3>{mqtt.received}</h3>",
} }

View File

@@ -7,9 +7,9 @@
<div id="id01" class="w3-modal"> <div id="id01" class="w3-modal">
<div class="w3-modal-content" style="width:600px"> <div class="w3-modal-content" style="width:600px">
<div class="w3-container w3-padding-24"> <div class="w3-container w3-padding-24">
<h2>{{_("Do you really want to delete the log file")}}:<br><b><span id="id03"></span></b> ?</h2> <h2>{{_('Do you really want to delete the log file: <br>%(file)s ?', file='<b><span id="id03"></span></b>')}}</h2>
<div class="w3-bar"> <div class="w3-bar">
<button id="id02" class="w3-button w3-red" onclick="deleteFile(); document.getElementById('id01').style.display='none'">{{_('Delete File</button')}}> <button id="id02" class="w3-button w3-red" onclick="deleteFile(); document.getElementById('id01').style.display='none'">{{_('Delete File')}}</button>
<button class="w3-button w3-grey w3-right" onclick="document.getElementById('id01').style.display='none'">{{_('Abort')}}</button> <button class="w3-button w3-grey w3-right" onclick="document.getElementById('id01').style.display='none'">{{_('Abort')}}</button>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@
<div class="w3-third"> <div class="w3-third">
<div class="w3-card-4"> <div class="w3-card-4">
<div class="w3-container w3-indigo w3-padding-16"> <div class="w3-container w3-indigo w3-padding-16">
<div class="w3-left"><i class="fa fa-link w3-xxxlarge"></i></div> <div class="w3-left"><i class="fa fa-business-time w3-xxxlarge"></i></div>
<div id = "mqtt-ctime" class="w3-right"> <div id = "mqtt-ctime" class="w3-right">
<h3>-</h3> <h3>-</h3>
</div> </div>
@@ -21,7 +21,7 @@
<div class="w3-third"> <div class="w3-third">
<div class="w3-card-4"> <div class="w3-card-4">
<div class="w3-container w3-purple w3-padding-16"> <div class="w3-container w3-purple w3-padding-16">
<div class="w3-left"><i class="fa fa-server w3-xxxlarge"></i></div> <div class="w3-left"><i class="fa fa-angle-double-right w3-xxxlarge"></i></div>
<div id = "mqtt-tx" class="w3-right"> <div id = "mqtt-tx" class="w3-right">
<h3>-</h3> <h3>-</h3>
</div> </div>
@@ -34,7 +34,7 @@
<div class="w3-third"> <div class="w3-third">
<div class="w3-card-4"> <div class="w3-card-4">
<div class="w3-container w3-orange w3-text-white w3-padding-16"> <div class="w3-container w3-orange w3-text-white w3-padding-16">
<div class="w3-left"><i class="fa fa-user w3-xxxlarge"></i></div> <div class="w3-left"><i class="fa fa-angle-double-left w3-xxxlarge"></i></div>
<div id = "mqtt-rx" class="w3-right"> <div id = "mqtt-rx" class="w3-right">
<h3>-</h3> <h3>-</h3>
</div> </div>

0
app/tests/log/empty.txt Normal file
View File

View File

@@ -9,6 +9,8 @@ from cnf.config import Config
from mock import patch from mock import patch
from proxy import Proxy from proxy import Proxy
import os, errno import os, errno
from os import DirEntry, stat_result
import datetime
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@@ -201,14 +203,33 @@ async def test_notes_fetch(client, config_conn):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_file_fetch(client, config_conn): async def test_file_fetch(client, config_conn, monkeypatch):
"""Test the data-fetch route.""" """Test the data-fetch route."""
_ = config_conn _ = config_conn
assert Config.log_path == 'app/tests/log/' assert Config.log_path == 'app/tests/log/'
def my_stat1(*arg):
stat = stat_result
stat.st_size = 20
stat.st_birthtime = datetime.datetime(2024, 1, 31, 10, 30, 15)
stat.st_mtime = datetime.datetime(2024, 1, 1, 1, 30, 15).timestamp()
return stat
monkeypatch.setattr(DirEntry, "stat", my_stat1)
response = await client.get('/file-fetch') response = await client.get('/file-fetch')
assert response.status_code == 200 assert response.status_code == 200
def my_stat2(*arg):
stat = stat_result
stat.st_size = 20
stat.st_mtime = datetime.datetime(2024, 1, 1, 1, 30, 15).timestamp()
return stat
monkeypatch.setattr(DirEntry, "stat", my_stat2)
monkeypatch.delattr(stat_result, "st_birthtime")
response = await client.get('/file-fetch')
assert response.status_code == 200
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_send_file(client, config_conn): async def test_send_file(client, config_conn):
"""Test the send-file route.""" """Test the send-file 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-04 18:16+0200\n" "POT-Creation-Date: 2025-05-13 22:34+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,30 +19,34 @@ 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:52 src/web/templates/base.html.j2:58 #: src/web/conn_table.py:53 src/web/templates/base.html.j2:58
msgid "Connections" msgid "Connections"
msgstr "Verbindungen" msgstr "Verbindungen"
#: src/web/conn_table.py:59 #: src/web/conn_table.py:60
msgid "Device-IP:Port" msgid "Device-IP:Port"
msgstr "Geräte-IP:Port" msgstr "Geräte-IP:Port"
#: src/web/conn_table.py:59 #: src/web/conn_table.py:60
msgid "Device-IP" msgid "Device-IP"
msgstr "Geräte-IP" msgstr "Geräte-IP"
#: src/web/conn_table.py:60 src/web/mqtt_table.py:34 #: src/web/conn_table.py:61 src/web/mqtt_table.py:34
msgid "Serial-No" msgid "Serial-No"
msgstr "Seriennummer" msgstr "Seriennummer"
#: src/web/conn_table.py:61 #: src/web/conn_table.py:62
msgid "Cloud-IP:Port" msgid "Cloud-IP:Port"
msgstr "Cloud-IP:Port" msgstr "Cloud-IP:Port"
#: src/web/conn_table.py:61 #: src/web/conn_table.py:62
msgid "Cloud-IP" msgid "Cloud-IP"
msgstr "Cloud-IP" msgstr "Cloud-IP"
#: src/web/log_files.py:48
msgid "n/a"
msgstr "keine Angabe"
#: src/web/mqtt_table.py:27 #: src/web/mqtt_table.py:27
msgid "MQTT devices" msgid "MQTT devices"
msgstr "MQTT Geräte" msgstr "MQTT Geräte"
@@ -116,12 +120,12 @@ msgid "TSUN Proxy - Log Files"
msgstr "TSUN Proxy - Log Dateien" 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: <br>%(file)s ?"
msgstr "Soll die Datei wirklich gelöscht werden" msgstr "Soll die Datei: <br>%(file)s<br>wirklich gelöscht werden?"
#: src/web/templates/page_logging.html.j2:12 #: src/web/templates/page_logging.html.j2:12
msgid "Delete File</button" msgid "Delete File"
msgstr "File löschen" msgstr "Datei löschen"
#: src/web/templates/page_logging.html.j2:13 #: src/web/templates/page_logging.html.j2:13
msgid "Abort" msgid "Abort"