From 6d0d8493aa42b4938820588bf3af6316a3a58a90 Mon Sep 17 00:00:00 2001 From: sstent Date: Fri, 12 Sep 2025 11:08:31 -0700 Subject: [PATCH] sync --- .../dashboard_service.cpython-313.pyc | Bin 6683 -> 7649 bytes tui/services/dashboard_service.py | 16 + .../__pycache__/dashboard.cpython-313.pyc | Bin 10708 -> 19613 bytes tui/views/__pycache__/rules.cpython-313.pyc | Bin 22163 -> 22882 bytes tui/views/dashboard.py | 406 ++++++++++++------ tui/views/rules.py | 9 + tui/views/test_view.py | 18 + 7 files changed, 314 insertions(+), 135 deletions(-) create mode 100644 tui/views/test_view.py diff --git a/tui/services/__pycache__/dashboard_service.cpython-313.pyc b/tui/services/__pycache__/dashboard_service.cpython-313.pyc index aa677527e6b7dc7d0ab2dfa2a35d78044aeadef3..755953c6ef81ac03671ad3736ad9f32e090008ed 100644 GIT binary patch delta 3471 zcmbtXTWlOx89rxjyR$d1z22K0d&f7NP2zQ2-x6Qytldpxx899Z_R@PNms)=wxPI>G()ZR~9LDi%3A0q2%?14H@OyX*Q6E2|sCabKJdafFB#+P; z{2HGXeaOIhoD-?yZmX&M#4&_=ksD>BU8o>A$+SHP8C-#7wVEmvWLwU2cIp5c-ja2! zat4>?mhcjq<`ZZMkMm17DP9VjL%JfE3+Q5nkKWAnJs^~^2_sx0%8w!EU3CzPZ}n!&T~($DFo z)v8{X%#yaEk-D5YSJF?jFV=apN|HQjWquN=>E`N6wT^WYmIo<{Lc+|@#L^a>an_Y* zH1p)~ibl$+PR=Zs^wbWI2!xq01G$I3hBgF2Uceu=C*Bx;ef*7?*JrLx+-@Jc6CV54 z(GNrME7RYZrZYd(J_z-%*L?l=c@!D{$Hz^vhX_N|M=kB^oip$8@1$=?zc{zP=h*F* z`E}p?CmTGo{>vPKZ(PLbKKyFu6#5aK5Plq)Lce5+U*ReAYfrkHzb-}79sKnUk;&aW zm>xSPUdUjblZXrXQ4dnkYp4_DQ4U)t0uBq_epvQ`J8NZ#E3j3vn!0V*AylyS4$p0! z97pGzpn`P?n8iLxIltxC)a^)ie}@Xq(s_+4fmbHCnFwvd}Zx zZg(B7O7^5}zH`wHKF{a+*sz38HLTA(IV?~$+W#{wF)v!f@=JxJolkYqGg9&e_W&G_ z7qBWD?mQPlpOZ~)5Sbtys{)Y7c{#Vwj&L|=4>&zr0&Rf)Md}I%Y>mTngRo~E7*oS_ zAO(&ij>(olJ7&ucS3UFxuC9UI1|j4QIexa37joS&h(r{)t#f!|7&2D9DIO+`=}~ur zK6HmJyZrRNJIrkj(~Yb=>)0~B5p<)e$~aZCv~Eeaa#6G;VJs2ue&zNjBMlU^0HZ3 zs*qD!l?*T?+g);)kqJg78DTp>W*9lbNE%3;U(pSc04eEh6o|mt3VEfnS}hW*PLK@q z@}6GN3|n*5(wOIs(tca@EK>!lm8!Nx0`Z277h8lIo>>k$Yd*Xf7S{af5$L>+@nTGP zpF70liS*=RyYQRQxj6K023V$>O_<3o77bf8X3=($=@m~_;&1MYW-)&Y%OZ4cNqknO zcZ1R3225&aU76eoarTY)&vm6>R~4FW4pXJs``p$8#DRUlag^ICCa?^fZL^%%U5gjC zic29#e=GEPowCwOf6}~vM}^?>TrS$E5b?_`9kneaSH(7z=R=5{IM!?wS0#plhEQ-k z;Z3)zr-hb=Sm|@WUZi>&+`^S1cu2mz3!+Hd5XD#MqF)LzM3K_c4n*h6v!r9navj)Vb7xa2z}EZrZ2Z9Fri;vb5_@1DVkM{REf%b+?yC=T9`t!kr?ix zCnEDhC9s+c@HB4VM_iu%@9^<4y%G7V>p=&g#(fWcJ33H%2{K&7nK3|(j2}=V;}))_ zp9b;W2+nwgccZfhptHs^mo03z&;XOkhaLO+SEVS`?e$M7nx R$De94FrIz@e|~Vo^WOztRfYfn delta 2297 zcma)7U2GIp6ux)QI?{oBbZ$kZ6$a>9<99#+F>IcQwF}U8W+}yyzzZowKXxXHp|Q zDXID<*T=*r*N>BI)4M;W4+G)Nx;wKWmfrJhqL(GD-Q?Z%Tse+RVTMQO4Sa!5i&e*9Gq-z_~@)mDIAuZFJjY6XXOyyLL-Wf+X3mP0gc{J zd|0N}0y^Hxw$vmFmTYvEe7Z{5UGfYo8fVIMQi&$Fxalj>5{#aT-u%z}?DpQ4!B^iD6T~*h-Pw~5f$0~hO zjV}mhRhIy(SYCU`&1iA%wVPhk*TtLuE}<29(LOGHzJwA(oPIygMNpn|19m&g2(6~Ujgr+-}nyU7TWG_V;7Y6e;iU;z%u}FiKG_*u7K=f zP~>N`PEj5^J(WFKI6mhT`Fw%UzpLxU24Ge_Ih%2gl5Bo{wm=3;O#X~Bwuc(N#^hixwuVs4v0}i2O^cGqXQu5FbE%EC=)*Rq)Kke623-iq3 zEZ*OY&y6%q^rD+lJP{Xe#zr`1EVbf^4q>TnxQ7|HLwF)7+^+9q`j?xB2PV3OuY7w$ z%(}A$;_t*U=p7|Ymatm{rdQeDz~31P?yu$V){0EO+rTsbatotkpr-B;+g}TcpbMxO z*=#@MVQ}<_)U>eH%qmTYH4kL0wKmrup?_^|aDAm=vsYan?h7)iU8W){UPcYDG^EEh zenacxEY8qMJ;u;M_#NI3x*1r#E$Tr{gvQ_q>v#$Ha>H;$J4Em6+CqaHWeKLYmz@F( zyTXbU*5cQyvu=ZrV28?atY+8RF05XOW%Vhqi?4U%4@02EHuGNzSSt}*ktvs@7p1SU zf?aFKs|73BO>Q1jwZrt=+NjPu(y;VIX;x7+7*PrQO9DC4bRd{*jxsHQ%caxG^gNtK zBtqZ=K;ra^U_XA9{uyk+3EEh9q7B?h4B%;JU@Zny^lDvOFac%ga|YmA^<@76X;`=(kFYsOahDoi~;Ne7-vuv$=tj%O`Zqh1qKNZ2>`F0%4T0b znlEHXflScPLml`4eGnSgUSX5@ICcwCms9uXWc{z)ZyG(=kbL<$HhmV4srcM*$7l>K zcHz;8u()lclNn15FtOCggB~vF5e#}v8T0Z>oxw4lzr~A8zvbnbC(&54bV8-SG>ofI zM&u=cNxHAGHw7nh9(^ZtuLxMwFAhFNOsq&l;?R}W$B2oQ5iTgEt_Xi1CJ1|Th(Nid r6B^0U#l~CpGeDJixTB(X@WkBo{A~7FGDkmb+FpGG None: + """Test database connection by running a simple query.""" + try: + result = await self.db.execute("SELECT 1") + return result.scalar() == 1 + except Exception as e: + raise Exception(f"Database connection test failed: {str(e)}") + async def get_dashboard_data(self) -> Dict: """Get consolidated dashboard data.""" try: + # Test database connection first + if not await self.test_connection(): + raise Exception("Database connection test failed") + # Recent workouts (last 7 days) workout_result = await self.db.execute( select(Workout) @@ -87,6 +99,10 @@ class DashboardService: async def get_weekly_stats(self) -> Dict: """Get weekly workout statistics.""" try: + # Test database connection first + if not await self.test_connection(): + raise Exception("Database connection test failed") + week_start = datetime.now() - timedelta(days=7) workout_result = await self.db.execute( diff --git a/tui/views/__pycache__/dashboard.cpython-313.pyc b/tui/views/__pycache__/dashboard.cpython-313.pyc index 8b7ccc8a5069302e7b65e9cb939ee06c1e80bc95..3d77cd0d3246172b639d4caeeb5cb348d7ea31f9 100644 GIT binary patch literal 19613 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 C1AgHE literal 10708 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%0DPZ;D4nA&M-q!t4#40P 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 Au>b%7 diff --git a/tui/views/__pycache__/rules.cpython-313.pyc b/tui/views/__pycache__/rules.cpython-313.pyc index 15a8ed8199af33dac4a513f11bfa77f12bcb3ee5..52454add59469eadf22edb5af88024b24b126e61 100644 GIT binary patch delta 2051 zcmaJ?YiyHM7(S=pWxagux@)(Ut>5mwtX(_U1;Q?cuB&8B*hh^DI=b!KRx?^?w-ILI z3>am}2*L^B2a3@U*bfbAG5W(q$IA8(XJ}+jeu%#`MjQb{#Or&$ZeytNd^tVudC&7+ z&N=UMO5VfYeTWVB^m;9V_I@xqbfWnK!<`D;Oa7>^=pkF`S!lJ8%r;9yL1#47 zFUCfP<+ylwNREgHqN9-!x!*OOvy)2U$e`G*td51{!(BGj5;GhfBt4ZaPnLnBAJEI_ zTeWafJ*i&Es!FM=7Y*4bqm$7a2Fp#g<%ZhwYg_=>(aKtU2|L+oz83ebqfrA?zitafgRB~L%|m|$x)=J!QdEPe17ZnOsJa5= zT(uc%$j8+-((B8q3bK+`(rs3W$O1tljF;(pDLoG00o2S`&;Tw~lK!u7q{FJ8FpL+j zyWoLX^nlnO8VU7=;xgQ@G!lv_H=MrzVJL+?SUR@<`tht(3H%M`0Lv>~ zaULd1In?`8P;V{@?oqsc4GZyBsGWb8oXLEgoe~zI424Ny%CO8DHReUZcrq{gZ5Q=Qa%=Va%JuDitmyQ@YeTW=dZ4_0izY{6vz?Y@DS?M)N^Xa0Nq z3T*4B&0mb)wfO4Mj1K#9_!)tra;6vi%*<6I_T}+cv%D-!X7jMm#Lwm{a*+;Dvz6Fa z$j(-=RJKz`$s7!F5|CylX~Gk9C{$Et-dC5AEXBSGAz3C+*+l~-J-FSCl68t)uWffS z$<~T?D|4-!1N}8C2byb6hRSZczlfPDVj#?8G^QL^Xsl)tDn}wZgGzu|-Edu`&5^BH zVFBeEkKYCKoR0GBvg;)6WpO=iX#6a{g22Jp3r{M^b>EU@vq!vXww=x{Ji zX||GZjZPKBWM)%tuB4NM^jFV8KF~yk9D4mDpAhA8RX&-j(PS7R47>=Lc8|)jBa~tT(bx(lf%3z$csSfYD36c_t~&EO5R~$H zSPsS3*eEJ6;;JnzF7x&)EIl|J>XV0}!~JqhaVH-*IC6vpYOj|oyj{{9qQQEi5jjTx zbe6b2>M?c+P!K4-q~wsYp?rxVBa{Drgja@c%)>=#7Gm>tl612i)q?*IS* delta 1342 zcmZuwZ){Ul6u;-acVE};wcXZj?I_#YZESm+U0b@+4Z_%XrGq#c=%bJ*vMkVb#?ggN zSOgOZ5JjV4IQ~gQj7rczvSiS&CQ7@pnCQe$!v&+$FB%dV0fGjMo>xE<<7>{nzwK_|2Q+0;bGD4o+_@tm$vVEiTyYEF4cQq1n_r?ypMx;+{; zGgte(vu&x8E%zRezCLhGDLXmv>cAWETTY5B)~;DB3*LK_M0r=8ZTIh3@Me40OaIA# zFg`^ZM_kbeoVIBmb{^LE$QMLHll7V`URFS3@?~qEL{7dOG(pZ+6mfMa<>bAfNm9N+ zGH|m|B;Nys3k)c~AQV@r$jGu*Ybq$aK(i|at714chD>2OMBS`VTf`nq)Fl?$nxYPI z+Qx~Wc5q^*U7~^AHPKFSx>F>X-4E}VTBlDTJ4$x@V-bIkMKI$l!`;meMHh5Ymy9b* zbNt-bXpn*~<2_%yV)|!BnCDc76Ay1P^A^UEfcv&qI`k6V{O}j%h5}-qe$!JW=~mr_Tm357@sQtVDmRLR0l)Rv;`?$b&ikEW zrCy4+{3?FsFI65FO^Uh`gMlie(K_Arm^QS$q?iMd&5M1*_#1huB-0`$Y+hd z&!DOWU@bnSU8iUy8UhWUi!Kq~xgiLf@Wh5+2v5X ComposeResult: """Create dashboard layout.""" + self.log(f"[DashboardView] compose called | debug_id={self.debug_id} | loading={self.loading} | error={self.error_message}") yield Static("AI Cycling Coach Dashboard", classes="view-title") - - if self.loading: + # Always show the structure - use conditional content + if self.error_message: + with Container(classes="error-container"): + yield Static(f"Error: {self.error_message}", classes="error-title") + yield Static("Possible causes:", classes="error-subtitle") + yield Static("- Database connection issue", classes="error-item") + yield Static("- Service dependency missing", classes="error-item") + yield Static("- Invalid configuration", classes="error-item") + yield Static("", classes="error-spacer") + yield Static("Troubleshooting steps:", classes="error-subtitle") + yield Static("- Check database configuration", classes="error-item") + yield Static("- Verify backend services are running", classes="error-item") + yield Static("- View logs for details", classes="error-item") + yield Static("", classes="error-spacer") + yield Static("Click Refresh to try again", classes="error-action") + elif self.loading and not self.dashboard_data: + # Initial load - full screen loader yield LoadingIndicator(id="dashboard-loader") else: - with ScrollableContainer(): - with Horizontal(): - # Left column - Recent workouts - with Vertical(classes="dashboard-column"): - yield Static("Recent Workouts", classes="section-title") - workout_table = DataTable(id="recent-workouts") - workout_table.add_columns("Date", "Type", "Duration", "Distance", "Avg HR") - yield workout_table + # Show content with optional refresh indicator + if self.loading: + with Container(classes="loading-overlay"): + yield LoadingIndicator() + yield Static("Refreshing...") + yield from self._compose_dashboard_content() + + + def _compose_dashboard_content(self) -> ComposeResult: + """Compose the main dashboard content.""" + with ScrollableContainer(): + with Horizontal(): + # Left column - Recent workouts + with Vertical(classes="dashboard-column"): + yield Static("Recent Workouts", classes="section-title") + workout_table = DataTable(id="recent-workouts") + workout_table.add_columns("Date", "Type", "Duration", "Distance", "Avg HR") + yield workout_table + + # Right column - Quick stats and current plan + with Vertical(classes="dashboard-column"): + # Weekly stats + with Container(classes="stats-container"): + yield Static("This Week", classes="section-title") + yield Static("Workouts: 0", id="week-workouts", classes="stat-item") + yield Static("Distance: 0 km", id="week-distance", classes="stat-item") + yield Static("Time: 0h 0m", id="week-time", classes="stat-item") - # Right column - Quick stats and current plan - with Vertical(classes="dashboard-column"): - # Weekly stats - with Container(classes="stats-container"): - yield Static("This Week", classes="section-title") - yield Static("Workouts: 0", id="week-workouts", classes="stat-item") - yield Static("Distance: 0 km", id="week-distance", classes="stat-item") - yield Static("Time: 0h 0m", id="week-time", classes="stat-item") - - # Active plan - with Container(classes="stats-container"): - yield Static("Current Plan", classes="section-title") - yield Static("No active plan", id="active-plan", classes="stat-item") - - # Sync status - with Container(classes="stats-container"): - yield Static("Garmin Sync", classes="section-title") - yield Static("Never synced", id="sync-status", classes="stat-item") - yield Static("", id="last-sync", classes="stat-item") + # Active plan + with Container(classes="stats-container"): + yield Static("Current Plan", classes="section-title") + yield Static("No active plan", id="active-plan", classes="stat-item") + + # Sync status + with Container(classes="stats-container"): + yield Static("Garmin Sync", classes="section-title") + yield Static("Never synced", id="sync-status", classes="stat-item") + yield Static("", id="last-sync", classes="stat-item") +def on_mount(self) -> None: + """Load dashboard data when mounted.""" + # Generate unique debug ID for this instance + import uuid + self.debug_id = str(uuid.uuid4())[:8] + self.log(f"[DashboardView] on_mount called | debug_id={self.debug_id}") + self._mounted = True - async def on_mount(self) -> None: - """Load dashboard data when mounted.""" - try: - await self.load_dashboard_data() - except Exception as e: - self.log(f"Dashboard loading error: {e}", severity="error") - # Show error state instead of loading indicator - self.loading = False - self.refresh() + # Use @work decorator for async operations + self.load_dashboard_data() + + @work(exclusive=True) async def load_dashboard_data(self) -> None: """Load and display dashboard data.""" + self.log(f"[DashboardView] load_dashboard_data started | debug_id={self.debug_id}") try: + self.loading = True + self.error_message = "" + + try: + # Explicitly check imports again in case of lazy loading + from backend.app.database import AsyncSessionLocal + from tui.services.dashboard_service import DashboardService + except ImportError as e: + self.log(f"[DashboardView] Import error in load_dashboard_data: {e} | debug_id={self.debug_id}", severity="error") + self.error_message = f"Service dependency error: {e}" + self.loading = False + return + async with AsyncSessionLocal() as db: + self.log(f"[DashboardView] Database session opened | debug_id={self.debug_id}") dashboard_service = DashboardService(db) - self.dashboard_data = await dashboard_service.get_dashboard_data() + + # Log service initialization + self.log(f"[DashboardView] DashboardService created | debug_id={self.debug_id}") + + dashboard_data = await dashboard_service.get_dashboard_data() weekly_stats = await dashboard_service.get_weekly_stats() - # Update the reactive data and stop loading - self.loading = False - self.refresh() + # Log data retrieval + self.log(f"[DashboardView] Data retrieved | debug_id={self.debug_id} | workouts={len(dashboard_data.get('recent_workouts', []))} | weekly_stats={weekly_stats}") - # Populate the dashboard with data - await self.populate_dashboard(weekly_stats) + # Update reactive data + self.dashboard_data = dashboard_data + self.loading = False + + # Force recomposition after data is loaded + await self.call_after_refresh(self.populate_dashboard, weekly_stats) except Exception as e: - self.log(f"Error loading dashboard data: {e}", severity="error") + self.log(f"[DashboardView] Error loading dashboard data: {e} | debug_id={self.debug_id}", severity="error") + self.error_message = str(e) self.loading = False - self.refresh() + finally: + self.log(f"[DashboardView] load_dashboard_data completed | debug_id={self.debug_id}") async def populate_dashboard(self, weekly_stats: dict) -> None: """Populate dashboard widgets with loaded data.""" + self.log(f"[DashboardView] populate_dashboard started | debug_id={self.debug_id}") + if self.loading or self.error_message: + self.log(f"[DashboardView] populate_dashboard skipped - loading={self.loading}, error={self.error_message} | debug_id={self.debug_id}") + return + try: # Update recent workouts table - workout_table = self.query_one("#recent-workouts", DataTable) - workout_table.clear() - - for workout in self.dashboard_data.get("recent_workouts", []): - # Format datetime for display - start_time = "N/A" - if workout.get("start_time"): - try: - dt = datetime.fromisoformat(workout["start_time"].replace('Z', '+00:00')) - start_time = dt.strftime("%m/%d %H:%M") - except: - start_time = workout["start_time"][:10] # Fallback to date only + try: + self.log(f"[DashboardView] Updating workout table | debug_id={self.debug_id}") + workout_table = self.query_one("#recent-workouts", DataTable) + workout_table.clear() - # Format duration - duration = "N/A" - if workout.get("duration_seconds"): - minutes = workout["duration_seconds"] // 60 - duration = f"{minutes}min" - - # Format distance - distance = "N/A" - if workout.get("distance_m"): - distance = f"{workout['distance_m'] / 1000:.1f}km" - - # Format heart rate - avg_hr = workout.get("avg_hr", "N/A") - if avg_hr != "N/A": - avg_hr = f"{avg_hr}bpm" - - workout_table.add_row( - start_time, - workout.get("activity_type", "Unknown") or "Unknown", - duration, - distance, - str(avg_hr) - ) + for workout in self.dashboard_data.get("recent_workouts", []): + # Format datetime for display + start_time = "N/A" + if workout.get("start_time"): + try: + dt = datetime.fromisoformat(workout["start_time"].replace('Z', '+00:00')) + start_time = dt.strftime("%m/%d %H:%M") + except Exception: + start_time = workout["start_time"][:10] # Fallback to date only + + # Format duration + duration = "N/A" + if workout.get("duration_seconds"): + minutes = workout["duration_seconds"] // 60 + duration = f"{minutes}min" + + # Format distance + distance = "N/A" + if workout.get("distance_m"): + distance = f"{workout['distance_m'] / 1000:.1f}km" + + # Format heart rate + avg_hr = workout.get("avg_hr", "N/A") + if avg_hr != "N/A": + avg_hr = f"{avg_hr}bpm" + + workout_table.add_row( + start_time, + workout.get("activity_type", "Unknown") or "Unknown", + duration, + distance, + str(avg_hr) + ) + self.log(f"[DashboardView] Workout table updated with {len(self.dashboard_data.get('recent_workouts', []))} rows | debug_id={self.debug_id}") + except Exception as e: + self.log(f"[DashboardView] Error updating workout table: {e} | debug_id={self.debug_id}", severity="error") # Update weekly stats - self.query_one("#week-workouts", Static).update( - f"Workouts: {weekly_stats.get('workout_count', 0)}" - ) - self.query_one("#week-distance", Static).update( - f"Distance: {weekly_stats.get('total_distance_km', 0)}km" - ) - self.query_one("#week-time", Static).update( - f"Time: {weekly_stats.get('total_time_hours', 0):.1f}h" - ) + try: + self.log(f"[DashboardView] Updating weekly stats | debug_id={self.debug_id}") + self.query_one("#week-workouts", Static).update( + f"Workouts: {weekly_stats.get('workout_count', 0)}" + ) + self.query_one("#week-distance", Static).update( + f"Distance: {weekly_stats.get('total_distance_km', 0)}km" + ) + self.query_one("#week-time", Static).update( + f"Time: {weekly_stats.get('total_time_hours', 0):.1f}h" + ) + self.log(f"[DashboardView] Weekly stats updated | debug_id={self.debug_id}") + except Exception as e: + self.log(f"[DashboardView] Error updating stats: {e} | debug_id={self.debug_id}", severity="error") # Update current plan - current_plan = self.dashboard_data.get("current_plan") - if current_plan: - plan_text = f"Plan v{current_plan.get('version', 'N/A')}" - if current_plan.get("created_at"): - try: - dt = datetime.fromisoformat(current_plan["created_at"].replace('Z', '+00:00')) - plan_text += f" ({dt.strftime('%m/%d/%Y')})" - except: - pass - self.query_one("#active-plan", Static).update(plan_text) - else: - self.query_one("#active-plan", Static).update("No active plan") + try: + self.log(f"[DashboardView] Updating current plan | debug_id={self.debug_id}") + current_plan = self.dashboard_data.get("current_plan") + if current_plan: + plan_text = f"Plan v{current_plan.get('version', 'N/A')}" + if current_plan.get("created_at"): + try: + dt = datetime.fromisoformat(current_plan["created_at"].replace('Z', '+00:00')) + plan_text += f" ({dt.strftime('%m/%d/%Y')})" + except Exception: + pass + self.query_one("#active-plan", Static).update(plan_text) + else: + self.query_one("#active-plan", Static).update("No active plan") + self.log(f"[DashboardView] Current plan updated | debug_id={self.debug_id}") + except Exception as e: + self.log(f"[DashboardView] Error updating plan: {e} | debug_id={self.debug_id}", severity="error") # Update sync status - last_sync = self.dashboard_data.get("last_sync") - if last_sync: - status = last_sync.get("status", "unknown") - activities_count = last_sync.get("activities_synced", 0) - - self.query_one("#sync-status", Static).update( - f"Status: {status.title()}" - ) - - if last_sync.get("last_sync_time"): - try: - dt = datetime.fromisoformat(last_sync["last_sync_time"].replace('Z', '+00:00')) - sync_time = dt.strftime("%m/%d %H:%M") - self.query_one("#last-sync", Static).update( - f"Last: {sync_time} ({activities_count} activities)" - ) - except: - self.query_one("#last-sync", Static).update( - f"Activities: {activities_count}" - ) + try: + self.log(f"[DashboardView] Updating sync status | debug_id={self.debug_id}") + last_sync = self.dashboard_data.get("last_sync") + if last_sync: + status = last_sync.get("status", "unknown") + activities_count = last_sync.get("activities_synced", 0) + + self.query_one("#sync-status", Static).update( + f"Status: {status.title()}" + ) + + if last_sync.get("last_sync_time"): + try: + dt = datetime.fromisoformat(last_sync["last_sync_time"].replace('Z', '+00:00')) + sync_time = dt.strftime("%m/%d %H:%M") + self.query_one("#last-sync", Static).update( + f"Last: {sync_time} ({activities_count} activities)" + ) + except Exception: + self.query_one("#last-sync", Static).update( + f"Activities: {activities_count}" + ) + else: + self.query_one("#last-sync", Static).update("") else: + self.query_one("#sync-status", Static).update("Never synced") self.query_one("#last-sync", Static).update("") - else: - self.query_one("#sync-status", Static).update("Never synced") - self.query_one("#last-sync", Static).update("") + self.log(f"[DashboardView] Sync status updated | debug_id={self.debug_id}") + except Exception as e: + self.log(f"[DashboardView] Error updating sync status: {e} | debug_id={self.debug_id}", severity="error") except Exception as e: - self.log(f"Error populating dashboard: {e}", severity="error") + self.log(f"[DashboardView] Error populating dashboard: {e} | debug_id={self.debug_id}", severity="error") + self.error_message = f"Failed to populate dashboard: {e}" + finally: + self.log(f"[DashboardView] populate_dashboard completed | debug_id={self.debug_id}") def watch_loading(self, loading: bool) -> None: """React to loading state changes.""" - # Trigger recomposition when loading state changes - if hasattr(self, '_mounted') and self._mounted: - self.refresh() \ No newline at end of file + self.log(f"[DashboardView] watch_loading: loading={loading} | debug_id={self.debug_id}") + # Force recomposition when loading state changes + if self.is_mounted: + self.call_after_refresh(self._refresh_view) + + def watch_error_message(self, error_message: str) -> None: + """React to error message changes.""" + self.log(f"[DashboardView] watch_error_message: error_message={error_message} | debug_id={self.debug_id}") + if self.is_mounted: + self.call_after_refresh(self._refresh_view) + + async def _refresh_view(self) -> None: + """Force view refresh by recomposing.""" + self.log(f"[DashboardView] _refresh_view called | debug_id={self.debug_id}") + self.refresh(layout=True) \ No newline at end of file diff --git a/tui/views/rules.py b/tui/views/rules.py index 1fea4f6..1445935 100644 --- a/tui/views/rules.py +++ b/tui/views/rules.py @@ -211,16 +211,21 @@ class RuleView(Widget): async def on_mount(self) -> None: """Load rules when mounted.""" + self.log("Mounting Rules view") await self.load_rules() async def load_rules(self) -> None: """Load rules from database.""" + self.log("Starting rules load") self.loading = True self.refresh() try: + self.log("Creating database session") async with AsyncSessionLocal() as db: + self.log("Creating RuleService") rule_service = RuleService(db) + self.log("Fetching rules from database") self.rules = await rule_service.get_rules() self.log(f"Loaded {len(self.rules)} rules from database") await self.populate_rules_table() @@ -229,22 +234,26 @@ class RuleView(Widget): self.error_message = error_msg self.log(error_msg, severity="error") finally: + self.log("Finished loading rules") self.loading = False self.refresh() async def populate_rules_table(self) -> None: """Populate rules table with data.""" try: + self.log("Querying for rules table widget") rules_table = self.query_one("#rules-table", DataTable) if not rules_table: self.log("Rules table widget not found", severity="error") return + self.log("Clearing rules table") rules_table.clear() self.log(f"Populating table with {len(self.rules)} rules") if not self.rules: # Add placeholder row when no rules exist + self.log("Adding placeholder for empty rules") rules_table.add_row("No rules found", "", "", "", "") self.log("No rules to display") return diff --git a/tui/views/test_view.py b/tui/views/test_view.py new file mode 100644 index 0000000..68c2d5d --- /dev/null +++ b/tui/views/test_view.py @@ -0,0 +1,18 @@ +""" +Simple test view to verify TUI framework functionality. +""" +from textual.app import ComposeResult +from textual.widgets import Static +from textual.widget import Widget + +class TestView(Widget): + """Test view with static content to verify TUI rendering.""" + + def compose(self) -> ComposeResult: + yield Static("TUI Framework Test", classes="view-title") + yield Static("This is a simple test view to verify TUI functionality.") + yield Static("If you see this text, the TUI framework is working correctly.") + yield Static("Check marks: [✓] Text rendering [✓] Basic layout") + + def on_mount(self) -> None: + self.log("TestView mounted successfully")