Skip to content

Commit 8aea5fe

Browse files
committed
v1.6.0 — Home Tab & Daily Workflow Refactor
The main window layout has been redesigned around daily use instead of internal module structure. A fixed top area is now always visible — connection status, archive stats, and the device table stay in view regardless of which tab is active. Daily actions (Sync, Mirror, Timer) are permanently accessible without switching tabs. **What's new:** - **Fixed top area** — connection indicators, archive status (fail / recheck / missing / range / coverage / last sync), device table, and Daily Actions always visible - **Daily Sync button** — one click runs Garmin Sync → Context Sync → Create All in sequence; gap detection included - **Mirror button** — context-aware: shows path setup dialog if no mirror is configured, export/import dialog if one is set - **Tab order** — Home (dashboard viewer) / Files (Excel viewer) / Settings; `health_garmin` and `overview_garmin` load as defaults - **Settings tab redesigned** — two-column layout: Settings panel left (fixed width), action panels right; cleaner separation of configuration and controls - **Device table** — width now auto-adjusts to content; no more horizontal scrolling or empty space to the right - **Settings sidebar removed** — all panels consolidated into the scrollable Settings tab; full window width now available for dashboard content - **Log height reduced** — less screen space consumed; session log files retain full detail - **ruff linting** — codebase cleaned to 0 errors; `ruff.toml` added; `test_static.py` enforces this as a gate **Downloads:** - `Garmin_Local_Archive_Standalone.zip` — recommended, no Python required - `Garmin_Local_Archive.zip` — standard, Python 3.10+ required [Full changelog](docs/CHANGELOG.md)
1 parent 8e92510 commit 8aea5fe

55 files changed

Lines changed: 1074 additions & 1892 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,15 @@ The project is structured into five focused layers. Each layer has a single resp
310310

311311
| Script | What it does |
312312
|---|---|
313-
| `garmin_app_base.py` | Assembler — wires panel Mixins together, holds shared state, defines abstract hooks. tkinter exclusive. |
314-
| `app/garmin_app_settings.py` | Settings persistence, keyring helpers, constants. No tkinter — importable in any context. |
315-
| `app/garmin_app_controller.py` | Application logic — ENV construction, archive stats, connection checks, timer calculations. No tkinter. |
316-
| `app/panel_settings.py` | Settings panel Mixin — credentials, paths, sync config, context location. |
317-
| `app/panel_connection.py` | Connection panel Mixin — connection test, indicators, prompts, token reset, archive info. |
318-
| `app/panel_archive.py` | Archive panel Mixin — integrity check, restore, clean archive, mirror operation. |
319-
| `app/panel_timer.py` | Timer panel Mixin — background timer UI, loop, controller delegates. |
320-
| `app/panel_outputs.py` | Outputs panel Mixin — sync, import, context sync, dashboard popup, output buttons. |
313+
| `garmin_app_base.py` | Assembler — fixed top (panel_home) + QTabWidget: Home / Files / Settings. PyQt6 QMainWindow. |
314+
| `app/garmin_app_settings.py` | Settings persistence, keyring helpers, constants. No GUI — importable in any context. |
315+
| `app/garmin_app_controller.py` | Application logic — ENV construction, archive stats, connection checks, timer calculations. No GUI. |
316+
| `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+) |
317+
| `app/panel_settings.py` | Settings panel — credentials, paths, sync config, context location. |
318+
| `app/panel_connection.py` | Connection panel — connection test, dialogs, token reset. Indicators delegated to panel_home. |
319+
| `app/panel_archive.py` | Archive panel — integrity check, restore, clean archive, mirror operation. |
320+
| `app/panel_timer.py` | Timer panel — background timer UI, loop, controller delegates. |
321+
| `app/panel_outputs.py` | Outputs panel — sync, import, context sync, dashboard build, output buttons. |
321322
| `garmin_app.py` + `build.py` | Desktop GUI entry point + standard EXE build (Python required on target) |
322323
| `garmin_app_standalone.py` + `build_standalone.py` | Desktop GUI entry point + standalone EXE build (no Python required) |
323324
| `daily_update.py` / `daily_update.exe` | Headless daily sync — runs without the GUI, designed for Windows Task Scheduler automation |
@@ -586,7 +587,8 @@ See `info/MAINTENANCE.md` for full technical documentation, how to add new field
586587

587588
## Testing
588589

589-
Five test suites cover the full pipeline — no network, no API, no GUI required:
590+
Six test suites cover the full pipeline — no network, no API required:
591+
590592

591593
```bash
592594
python tests/test_local.py # Garmin pipeline

app/garmin_app_settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,4 @@ def _open_url(url: str):
116116
try:
117117
os.startfile(url)
118118
except Exception:
119-
pass
119+
pass

app/panel_archive.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,26 +172,26 @@ def _refresh_archive_info(self):
172172
last_api = stats["last_api"] or "—"
173173
last_bulk = stats["last_bulk"] or "—"
174174

175-
pc = self._app._panel_connection
175+
ph = self._app._panel_home
176176
integrity_warnings = stats.get("integrity_warnings", [])
177177
integrity_text = (
178178
"⚠ " + ", ".join(integrity_warnings)
179179
if integrity_warnings else ""
180180
)
181181

182182
def _update():
183-
pc._info_qdots["failed"].setText(f"fail {counts['failed']}")
184-
pc._info_recheck.setText(f"Recheck: {recheck}")
185-
pc._info_missing.setText(f"Missing: {missing}")
186-
pc._info_range.setText(f"Range: {rng}")
187-
pc._info_coverage.setText(f"Coverage: {coverage}")
188-
pc._info_last_api.setText(f"Last API: {last_api}")
189-
pc._info_last_bulk.setText(f"Last Bulk: {last_bulk}")
190-
pc._integrity_warning_lbl.setText(integrity_text)
183+
ph._info_qdots["failed"].setText(f"fail {counts['failed']}")
184+
ph._info_recheck.setText(f"Recheck: {recheck}")
185+
ph._info_missing.setText(f"Missing: {missing}")
186+
ph._info_range.setText(f"Range: {rng}")
187+
ph._info_coverage.setText(f"Coverage: {coverage}")
188+
ph._info_last_api.setText(f"Last API: {last_api}")
189+
ph._info_last_bulk.setText(f"Last Bulk: {last_bulk}")
190+
ph._integrity_warning_lbl.setText(integrity_text)
191191

192192
# Device table — __total__ row is in device_table.json but
193193
# we render it separately for formatting control.
194-
tbl = pc._info_device_table
194+
tbl = ph._info_device_table
195195
tbl.setRowCount(0)
196196
total_high = total_std = total_all = 0
197197
data_rows = [r for r in device_table if r.get("device_id") != "__total__"]

app/panel_connection.py

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

1919
from PyQt6.QtWidgets import (
2020
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
21-
QDialog, QLineEdit, QFrame, QSizePolicy,
22-
QTableWidget, QHeaderView,
21+
QDialog, QLineEdit, QFrame,
2322
)
2423
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
2524
from PyQt6.QtGui import QFont
@@ -345,25 +344,8 @@ def _build_ui(self):
345344
conn_row = QHBoxLayout()
346345
conn_row.setSpacing(0)
347346

348-
self._conn_indicators = {}
349-
ind_widget = QWidget()
350-
ind_lay = QHBoxLayout(ind_widget)
351-
ind_lay.setContentsMargins(0, 0, 0, 0)
352-
ind_lay.setSpacing(0)
353-
for key, label in [("token", "Token"), ("login", "Login"),
354-
("api", "API Access"), ("data", "Data")]:
355-
dot = QLabel("●")
356-
dot.setFont(QFont("Segoe UI", 10))
357-
dot.setStyleSheet(f"color: {self._app.TEXT2};")
358-
lbl = QLabel(label)
359-
lbl.setFont(QFont("Segoe UI", 9))
360-
lbl.setStyleSheet(f"color: {self._app.TEXT2};")
361-
ind_lay.addWidget(dot)
362-
ind_lay.addWidget(lbl)
363-
ind_lay.addSpacing(14)
364-
self._conn_indicators[key] = dot
365-
366-
conn_row.addWidget(ind_widget)
347+
# _conn_indicators lives in panel_home — panel_connection delegates
348+
# all indicator writes via _set_indicator() → panel_home._conn_indicators.
367349
conn_row.addStretch()
368350

369351
def _btn(text, color, fg):
@@ -403,96 +385,8 @@ def _btn(text, color, fg):
403385
conn_row.addSpacing(4)
404386

405387
lay.addLayout(conn_row)
406-
lay.addSpacing(6)
407-
408-
# Archive info — row 1: fail indicator + recheck + missing
409-
row1 = QHBoxLayout()
410-
row1.setSpacing(0)
411-
412-
self._info_qdots = {}
413-
dot = QLabel("●")
414-
dot.setFont(QFont("Segoe UI", 9))
415-
dot.setStyleSheet(f"color: {self._app.ACCENT};")
416-
lbl = QLabel("fail —")
417-
lbl.setFont(QFont("Segoe UI", 8))
418-
lbl.setStyleSheet(f"color: {self._app.TEXT2};")
419-
row1.addWidget(dot)
420-
row1.addSpacing(2)
421-
row1.addWidget(lbl)
422-
row1.addSpacing(10)
423-
self._info_qdots["failed"] = lbl
424-
425-
self._info_recheck = QLabel("Recheck: —")
426-
self._info_recheck.setFont(QFont("Segoe UI", 8))
427-
self._info_recheck.setStyleSheet(f"color: {self._app.TEXT2};")
428-
row1.addWidget(self._info_recheck)
429-
row1.addSpacing(10)
430-
431-
self._info_missing = QLabel("Missing: —")
432-
self._info_missing.setFont(QFont("Segoe UI", 8))
433-
self._info_missing.setStyleSheet(f"color: {self._app.TEXT2};")
434-
row1.addWidget(self._info_missing)
435-
row1.addStretch()
436-
lay.addLayout(row1)
437-
438-
# Archive info — row 2
439-
row2 = QHBoxLayout()
440-
row2.setSpacing(0)
441-
for attr, text in [
442-
("_info_range", "Range: —"),
443-
("_info_coverage", "Coverage: —"),
444-
("_info_last_api", "Last API: —"),
445-
("_info_last_bulk","Last Bulk: —"),
446-
]:
447-
lbl = QLabel(text)
448-
lbl.setFont(QFont("Segoe UI", 8))
449-
lbl.setStyleSheet(f"color: {self._app.TEXT2};")
450-
setattr(self, attr, lbl)
451-
row2.addWidget(lbl)
452-
row2.addSpacing(14)
453-
row2.addStretch()
454-
lay.addLayout(row2)
455-
456-
# Archive info — row 3: device table
457-
_HDR = ["From", "To", "Device", "High", "Standard", "Total"]
458-
self._info_device_table = QTableWidget(0, len(_HDR))
459-
self._info_device_table.setHorizontalHeaderLabels(_HDR)
460-
for col in range(6):
461-
self._info_device_table.horizontalHeader().setSectionResizeMode(
462-
col, QHeaderView.ResizeMode.ResizeToContents)
463-
self._info_device_table.horizontalHeader().setStretchLastSection(False)
464-
self._info_device_table.verticalHeader().setVisible(False)
465-
self._info_device_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
466-
self._info_device_table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
467-
self._info_device_table.setShowGrid(True)
468-
self._info_device_table.setAlternatingRowColors(True)
469-
self._info_device_table.setSizeAdjustPolicy(
470-
QTableWidget.SizeAdjustPolicy.AdjustToContents)
471-
self._info_device_table.setSizePolicy(
472-
QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
473-
self._info_device_table.setFont(QFont("Segoe UI", 8))
474-
self._info_device_table.setStyleSheet(f"""
475-
QTableWidget {{
476-
background: {self._app.BG2};
477-
alternate-background-color: {self._app.BG3};
478-
color: {self._app.TEXT2};
479-
gridline-color: {self._app.BG3};
480-
border: none;
481-
}}
482-
QHeaderView::section {{
483-
background: {self._app.BG3};
484-
color: {self._app.TEXT};
485-
font-size: 8pt;
486-
border: none;
487-
padding: 2px 4px;
488-
}}
489-
""")
490-
lay.addWidget(self._info_device_table)
491-
492-
self._integrity_warning_lbl = QLabel("")
493-
self._integrity_warning_lbl.setFont(QFont("Segoe UI", 8, QFont.Weight.Bold))
494-
self._integrity_warning_lbl.setStyleSheet(f"color: {self._app.YELLOW};")
495-
lay.addWidget(self._integrity_warning_lbl)
388+
# Archive info widgets (fail/recheck/missing/range/device-table/integrity)
389+
# live in panel_home — not duplicated here.
496390

497391
# ── Accessors — sole authorised write-path for mirror/restore buttons ──────
498392

@@ -549,7 +443,7 @@ def set_restore_button_state(self, enabled: bool,
549443
# ── Indicator ──────────────────────────────────────────────────────────────
550444

551445
def _set_indicator(self, key: str, state: str):
552-
dot = self._conn_indicators.get(key)
446+
dot = self._app._panel_home._conn_indicators.get(key)
553447
if not dot:
554448
return
555449
colors = {
@@ -567,7 +461,7 @@ def _run_connection_test(self, on_success=None):
567461
self._app._log("✗ Connection test: email or password missing.")
568462
return
569463

570-
for key in self._conn_indicators:
464+
for key in self._app._panel_home._conn_indicators:
571465
self._set_indicator(key, "reset")
572466
self._app._log("\n🔌 Testing connection ...")
573467

@@ -691,6 +585,6 @@ def _cb(value):
691585
def _reset_token(self):
692586
import garmin_security
693587
garmin_security.clear_token()
694-
self._set_indicator("token", "reset")
588+
self._app._panel_home._set_indicator("token", "reset")
695589
self._app._connection_verified = False
696590
self._app._log("🔑 Token reset — next sync will require a new login.")

0 commit comments

Comments
 (0)