Skip to content

Commit 9e84484

Browse files
committed
v1.6.0.6 — Documentation, Tooltips & UI Polish
Adds a Documentation button with in-app access to Quickstart Guide, User Guide, and README. All buttons and input fields now show tooltips. Mirror button labels are unified throughout the app. Settings left panel widened to 400px for cleaner Full HD display. **What's new:** - 📖 Documentation button — opens Quickstart, User Guide or README directly in the app - 💬 Tooltips on all buttons and input fields (Settings + Home panel) - 🔁 Mirror labels unified: "Export to Mirror" used consistently throughout - ⬛ Settings left column widened: 340px → 400px (no scrollbar at Full HD) - ⬛ DATA MANAGEMENT buttons left-aligned with equal width - 📄 QUICKSTART.txt + USER_GUIDE.txt included in all build targets **Downloads:** - `Garmin_Local_Archive_Standalone.zip` — recommended, no Python required - `Garmin_Local_Archive.zip` — standard, Python 3.10+ required [Full changelog](CHANGELOG.md)
1 parent 758ea5a commit 9e84484

15 files changed

Lines changed: 512 additions & 46 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,9 @@ The project is structured into five focused layers. Each layer has a single resp
331331
| `garmin_app_base.py` | Assembler — fixed top (panel_home) + QTabWidget: Home / Files / Settings. PyQt6 QMainWindow. |
332332
| `app/garmin_app_settings.py` | Settings persistence, keyring helpers, constants. No GUI — importable in any context. |
333333
| `app/garmin_app_controller.py` | Application logic — ENV construction, archive stats, connection checks, timer calculations. No GUI. |
334-
| `app/panel_home.py` | Fixed top area: connection indicators, archive status, device table, Daily Actions (Daily Sync / Mirror / Timer). Home tab: Dashboard viewer. (v1.6.0+) |
334+
| `app/panel_home.py` | Fixed top area: connection indicators, archive status, device table, Daily Actions (Daily Sync / Mirror / Timer / Documentation). Home tab: Dashboard viewer. (v1.6.0+) |
335335
| `app/panel_settings.py` | Settings panel — credentials, paths, sync config, context location. |
336-
| `app/panel_connection.py` | Connection panel — connection test, dialogs, token reset. Indicators delegated to panel_home. |
336+
| `app/panel_connection.py` | Connection panel — connection test, data management (Export to Mirror / Import from Mirror / Restore / Silo-Check / Repair / Reset Token), dialogs. Indicators delegated to panel_home. |
337337
| `app/panel_archive.py` | Archive panel — integrity check, restore, clean archive, mirror operation. |
338338
| `app/panel_timer.py` | Timer panel — background timer UI, loop, controller delegates. |
339339
| `app/panel_outputs.py` | Outputs panel — sync, import, context sync, dashboard build, output buttons. |

requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ PyQt6
88
PyQt6-WebEngine
99
pytest
1010
pytest-qt
11-
pip-audit
11+
pytest-cov
12+
pip-audit
13+
ruff
14+
bandit

src/app/panel_archive.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,7 @@ def _check():
872872

873873
self._app._dispatch(
874874
lambda: self._app._panel_connection.set_mirror_button_state(
875-
reachable, text="🔁 Data Mirror"))
875+
reachable, text="⬡ Export to Mirror"))
876876

877877
threading.Thread(target=_check, daemon=True).start()
878878

@@ -995,21 +995,21 @@ def _on_mirror(self):
995995
s = self._app._panel_settings._collect_settings()
996996
mirror_path = s.get("mirror_dir", "").strip()
997997
if not mirror_path:
998-
QMessageBox.warning(self._app, "Data Mirror",
998+
QMessageBox.warning(self._app, "Export to Mirror",
999999
"No mirror target configured.")
10001000
return
10011001
base_dir = Path(s.get("base_dir", "")).expanduser()
10021002

10031003
if self._app._is_running():
1004-
QMessageBox.warning(self._app, "Data Mirror",
1004+
QMessageBox.warning(self._app, "Export to Mirror",
10051005
"A Garmin sync is currently running.\nPlease wait until it finishes.")
10061006
return
10071007
if self._app._timer_active:
1008-
QMessageBox.warning(self._app, "Data Mirror",
1008+
QMessageBox.warning(self._app, "Export to Mirror",
10091009
"Background timer is active.\nStop the timer before mirroring.")
10101010
return
10111011
if self._app._ctx_running:
1012-
QMessageBox.warning(self._app, "Data Mirror",
1012+
QMessageBox.warning(self._app, "Export to Mirror",
10131013
"Context sync is running.\nPlease wait until it finishes.")
10141014
return
10151015

@@ -1044,7 +1044,7 @@ def _do_mirror():
10441044
self._mirror_running = False
10451045
self._app._dispatch(
10461046
lambda: self._app._panel_connection.set_mirror_button_state(
1047-
True, text="🔁 Data Mirror"))
1047+
True, text="⬡ Export to Mirror"))
10481048

10491049
threading.Thread(target=_do_mirror, daemon=True).start()
10501050

src/app/panel_connection.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from PyQt6.QtWidgets import (
2020
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
21-
QDialog, QLineEdit, QFrame,
21+
QDialog, QLineEdit, QFrame, QSizePolicy,
2222
)
2323
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
2424
from PyQt6.QtGui import QFont
@@ -342,11 +342,10 @@ def _build_ui(self):
342342

343343
# Connection indicators + buttons row
344344
conn_row = QHBoxLayout()
345-
conn_row.setSpacing(0)
345+
conn_row.setSpacing(4)
346346

347347
# _conn_indicators lives in panel_home — panel_connection delegates
348348
# all indicator writes via _set_indicator() → panel_home._conn_indicators.
349-
conn_row.addStretch()
350349

351350
def _btn(text, color, fg):
352351
b = QPushButton(text)
@@ -358,42 +357,60 @@ def _btn(text, color, fg):
358357
f"QPushButton:disabled {{ color: {self._app.TEXT2}; "
359358
f"background: {self._app.BG3}; }}")
360359
b.setCursor(Qt.CursorShape.PointingHandCursor)
360+
b.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
361361
return b
362362

363-
self._mirror_btn = _btn("🔁 Data Mirror", self._app.BG3, self._app.TEXT2)
363+
self._mirror_btn = _btn("⬡ Export to Mirror", self._app.BG3, self._app.TEXT2)
364364
self._mirror_btn.setEnabled(False)
365+
self._mirror_btn.setToolTip(
366+
"Create an encrypted backup of the archive (.gla container).\n"
367+
"Available after a successful connection.")
365368
self._mirror_btn.clicked.connect(
366369
lambda: self._app._panel_archive._on_mirror())
367370

368371
self._import_mirror_btn = _btn("📥 Import from Mirror",
369372
self._app.BG3, self._app.TEXT2)
370373
self._import_mirror_btn.setEnabled(False)
374+
self._import_mirror_btn.setToolTip(
375+
"Restore data from an existing .gla mirror container.\n"
376+
"Existing data with higher quality is not overwritten.")
371377
self._import_mirror_btn.clicked.connect(
372378
lambda: self._app._panel_archive._on_import_mirror())
373379

374380
self._restore_btn = _btn("Restore Data", self._app.BG3, self._app.TEXT2)
375381
self._restore_btn.setEnabled(False)
382+
self._restore_btn.setToolTip(
383+
"Restore raw data from the backup folder.\n"
384+
"Enabled after a Silo-Check detects recoverable files.")
376385
self._restore_btn.clicked.connect(
377386
lambda: self._app._panel_archive._on_restore_data())
378387

379388
reset_btn = _btn("🔑 Reset Token", self._app.BG3, self._app.TEXT2)
389+
reset_btn.setToolTip(
390+
"Delete the saved Garmin session token.\n"
391+
"The next sync will require a full SSO login.")
380392
reset_btn.clicked.connect(self._reset_token)
381393

382394
self._silo_check_btn = _btn("🔍 Silo-Check", self._app.BG3, self._app.TEXT2)
383395
self._silo_check_btn.setEnabled(True)
396+
self._silo_check_btn.setToolTip(
397+
"Check raw/, summary/ and source/ for consistency.\n"
398+
"Detects missing or mismatched files across silos.")
384399
self._silo_check_btn.clicked.connect(
385400
lambda: self._app._panel_archive._on_silo_check())
386401

387402
self._silo_repair_btn = _btn("🔧 Repair", self._app.BG3, self._app.TEXT2)
388403
self._silo_repair_btn.setEnabled(False)
404+
self._silo_repair_btn.setToolTip(
405+
"Repair silo inconsistencies found by Silo-Check.\n"
406+
"Enabled after a completed check with findings.")
389407
self._silo_repair_btn.clicked.connect(
390408
lambda: self._app._panel_archive._on_silo_repair())
391409

392410
for b in [self._mirror_btn, self._import_mirror_btn,
393411
self._restore_btn, reset_btn,
394412
self._silo_check_btn, self._silo_repair_btn]:
395413
conn_row.addWidget(b)
396-
conn_row.addSpacing(4)
397414

398415
lay.addLayout(conn_row)
399416
# Archive info widgets (fail/recheck/missing/range/device-table/integrity)

src/app/panel_home.py

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"""
2222

2323
import json
24+
import os
25+
import sys
2426
from datetime import date
2527
from pathlib import Path
2628

@@ -244,14 +246,24 @@ def _action_btn(text, accent=False):
244246
return b
245247

246248
self._daily_sync_btn = _action_btn("▶ Daily Sync", accent=True)
249+
self._daily_sync_btn.setToolTip(
250+
"Download missing days from Garmin Connect.\n"
251+
"Includes intraday data, daily summaries, weather and pollen.")
247252
self._daily_sync_btn.clicked.connect(self._on_daily_sync)
248253
right.addWidget(self._daily_sync_btn)
249254

250255
self._mirror_btn = _action_btn("⬡ Mirror")
256+
self._mirror_btn.setToolTip(
257+
"Create an encrypted backup of the archive (.gla container).\n"
258+
"Also allows importing data from an existing mirror.")
251259
self._mirror_btn.clicked.connect(self._on_mirror)
252260
right.addWidget(self._mirror_btn)
253261

254262
self._timer_btn = _action_btn("⏱ Timer: Off")
263+
self._timer_btn.setToolTip(
264+
"Start the background timer.\n"
265+
"Automatically syncs missing days at the configured interval\n"
266+
"while the app is open.")
255267
self._timer_btn.setStyleSheet(
256268
f"QPushButton {{ background: {self._app.BG3}; color: {self._app.TEXT2}; "
257269
f"border: none; padding: 7px 14px; }}"
@@ -260,6 +272,12 @@ def _action_btn(text, accent=False):
260272
lambda: self._app._panel_timer._toggle_timer())
261273
right.addWidget(self._timer_btn)
262274

275+
self._docs_btn = _action_btn("📖 Documentation")
276+
self._docs_btn.setToolTip(
277+
"Open Quickstart, User Guide or README.")
278+
self._docs_btn.clicked.connect(self._home_docs_dialog)
279+
right.addWidget(self._docs_btn)
280+
263281
right.addStretch()
264282
cols.addLayout(right, stretch=1)
265283

@@ -423,6 +441,81 @@ def _on_garmin_done():
423441

424442
# ── Mirror ─────────────────────────────────────────────────────────────────
425443

444+
def _home_docs_dialog(self):
445+
"""Documentation button handler — opens Quickstart, User Guide or README."""
446+
def _open(filename: str):
447+
# Dev: src/docs/ | Build T2/T3: info/ next to EXE
448+
if getattr(sys, "frozen", False):
449+
base = Path(sys.executable).parent / "info"
450+
else:
451+
base = Path(__file__).parent.parent / "docs"
452+
path = base / filename
453+
if not path.exists():
454+
QMessageBox.warning(
455+
self._app, "Documentation",
456+
f"File not found:\n{path}")
457+
return
458+
try:
459+
os.startfile(path)
460+
except Exception as e:
461+
QMessageBox.critical(
462+
self._app, "Documentation",
463+
f"Could not open file:\n{e}")
464+
465+
dlg = QDialog(self._app)
466+
dlg.setWindowTitle("Documentation")
467+
dlg.setModal(True)
468+
dlg.setFixedWidth(360)
469+
dlg.setStyleSheet(
470+
f"background: {self._app.BG}; color: {self._app.TEXT};")
471+
lay = QVBoxLayout(dlg)
472+
lay.setContentsMargins(20, 16, 20, 16)
473+
lay.setSpacing(6)
474+
475+
title = QLabel("Documentation")
476+
title.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold))
477+
title.setStyleSheet(f"color: {self._app.TEXT};")
478+
lay.addWidget(title)
479+
480+
sep = QFrame()
481+
sep.setFrameShape(QFrame.Shape.HLine)
482+
sep.setStyleSheet(f"color: {self._app.ACCENT};")
483+
sep.setFixedHeight(1)
484+
lay.addWidget(sep)
485+
486+
for label, filename in [
487+
("📋 Quickstart", "QUICKSTART.txt"),
488+
("📖 User Guide", "USER_GUIDE.txt"),
489+
("📄 README App", "README_APP.md"),
490+
]:
491+
btn = QPushButton(label)
492+
btn.setFont(QFont("Segoe UI", 9))
493+
btn.setStyleSheet(
494+
f"QPushButton {{ background: {self._app.BG3}; color: {self._app.TEXT}; "
495+
f"border: none; padding: 8px 14px; text-align: left; }}"
496+
f"QPushButton:hover {{ background: {self._app.ACCENT2}; }}")
497+
btn.setCursor(Qt.CursorShape.PointingHandCursor)
498+
_fn = filename
499+
btn.clicked.connect(lambda checked, fn=_fn: [dlg.accept(), _open(fn)])
500+
lay.addWidget(btn)
501+
502+
sep2 = QFrame()
503+
sep2.setFrameShape(QFrame.Shape.HLine)
504+
sep2.setStyleSheet(f"color: {self._app.BG3};")
505+
lay.addWidget(sep2)
506+
507+
cancel_btn = QPushButton("Cancel")
508+
cancel_btn.setFont(QFont("Segoe UI", 9))
509+
cancel_btn.setStyleSheet(
510+
f"QPushButton {{ background: {self._app.BG3}; color: {self._app.TEXT2}; "
511+
f"border: none; padding: 7px 14px; }}"
512+
f"QPushButton:hover {{ background: {self._app.ACCENT2}; }}")
513+
cancel_btn.setCursor(Qt.CursorShape.PointingHandCursor)
514+
cancel_btn.clicked.connect(dlg.reject)
515+
lay.addWidget(cancel_btn)
516+
517+
dlg.exec()
518+
426519
def _on_mirror(self):
427520
"""Mirror button handler. Main Thread only."""
428521
s = self._app._panel_settings._collect_settings()
@@ -435,7 +528,7 @@ def _on_mirror(self):
435528
def _home_mirror_dialog_no_target(self):
436529
"""Dialog shown when no mirror target is configured."""
437530
dlg = QDialog(self._app)
438-
dlg.setWindowTitle("Mirror")
531+
dlg.setWindowTitle("Export to Mirror")
439532
dlg.setModal(True)
440533
dlg.setFixedWidth(460)
441534
dlg.setStyleSheet(
@@ -549,7 +642,7 @@ def _save_and_continue():
549642
def _home_mirror_dialog_with_target(self, mirror_dir: str):
550643
"""Dialog shown when mirror target is already configured."""
551644
dlg = QDialog(self._app)
552-
dlg.setWindowTitle("Mirror")
645+
dlg.setWindowTitle("Export to Mirror")
553646
dlg.setModal(True)
554647
dlg.setFixedWidth(380)
555648
dlg.setStyleSheet(
@@ -558,7 +651,7 @@ def _home_mirror_dialog_with_target(self, mirror_dir: str):
558651
lay.setContentsMargins(20, 16, 20, 16)
559652
lay.setSpacing(6)
560653

561-
title = QLabel("Mirror")
654+
title = QLabel("Export to Mirror")
562655
title.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold))
563656
title.setStyleSheet(f"color: {self._app.TEXT};")
564657
lay.addWidget(title)

src/app/panel_outputs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def _tip(self, text: str) -> QLabel:
208208
lbl = QLabel(text)
209209
lbl.setFont(QFont("Segoe UI", 8))
210210
lbl.setStyleSheet(f"color: {self._app.TEXT2};")
211+
lbl.setFixedWidth(300)
211212
return lbl
212213

213214
# ── Sync actions ───────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)