diff --git a/ANALYSIS_SUMMARY.md b/ANALYSIS_SUMMARY.md new file mode 100644 index 0000000..bd26be5 --- /dev/null +++ b/ANALYSIS_SUMMARY.md @@ -0,0 +1,226 @@ +# Config Struct Analysis: cargo run vs Standalone EXE + +## QUICK ANSWER + +**The config.toml modifications work in `cargo run` but not standalone EXE because:** + +1. **Both behave identically** - neither reloads config during runtime +2. **The difference is file location** - where config.toml is searched for +3. **cargo run appears to work** because users restart the process frequently +4. **Standalone EXE appears broken** because users expect live reloading + +The real issue: **Config is frozen at startup and never reloaded.** + +--- + +## THE CORE PROBLEM + +### Configuration Loading (One-Time Only) + +**File: `src/main.rs`, Line 54** +```rust +let app_config = config::load_config(); // Loaded ONCE, never again +``` + +### Configuration Freezing Points + +**Freeze Point 1: BackgroundRenderer (`src/app.rs:28-34`)** +```rust +let background_renderer = Some(Arc::new(BackgroundRenderer::new( + &cc.egui_ctx, + &config.appearance.background_image, + config.appearance.background_opacity, // ← COPIED + config.appearance.fallback_color, // ← COPIED + config.appearance.fallback_color_opacity, // ← COPIED +))); +``` +- Appearance values **copied** into struct once +- Stored in `Arc` (immutable) +- **Never updated** during application lifetime + +**Freeze Point 2: Config Field (`src/app.rs:49`)** +```rust +Self { + config, // ← Stored as owned field, never mutated + // ... +} +``` +- Config struct **stored once** at initialization +- Colors, text sizing, window settings **frozen** +- **Never reloaded** from disk +- **Never updated** at runtime + +--- + +## WHY CHANGES DON'T APPEAR + +### Background Changes (opacity, color) + +1. User modifies `config.toml` (e.g., changes `background_opacity`) +2. Application still has original value in `BackgroundRenderer` memory +3. Every frame, `render_fullscreen()` uses **frozen opacity value** +4. No code to reload config or recreate `BackgroundRenderer` +5. **Change never takes effect** + +### Color Changes + +1. User modifies `config.toml` (e.g., changes text color) +2. Application still has original `Config` struct in memory +3. Every frame, `update()` reads from `self.config` +4. But `self.config` was never updated +5. `ctx.set_visuals()` applies **same frozen colors** +6. **Change never takes effect** + +--- + +## cargo run vs Standalone: The REAL Story + +### cargo run Process + +``` +User runs: cargo run +├─ New process created +├─ load_config() reads config.toml from repo root (fallback path) +├─ Application starts with current config +├─ User modifies config.toml while app is running +│ └─ Change DOESN'T take effect (no reload mechanism) +├─ User exits app +├─ User runs: cargo run again +│ ├─ New process created +│ ├─ load_config() reads UPDATED config.toml +│ └─ Application shows new values ✓ +└─ User thinks "config changes work in cargo run!" +``` + +### Standalone EXE Process + +``` +User runs: Create-App-Secret.exe +├─ Process created +├─ load_config() looks in EXE directory +│ └─ If config.toml exists there, loads it +│ └─ Otherwise falls back to ./config.toml +├─ Application starts with current config +├─ User modifies config.toml while app is running +│ └─ Change DOESN'T take effect (no reload mechanism) +├─ User expects changes to appear +│ └─ Nothing happens ✗ +├─ User closes app +├─ User runs: Create-App-Secret.exe again +│ ├─ New process created +│ ├─ load_config() reads UPDATED config.toml +│ └─ Application shows new values ✓ +└─ User thinks "config changes don't work in standalone!" +``` + +### The Actual Difference + +| Aspect | cargo run | Standalone | +|--------|-----------|-----------| +| Config loading code | Identical | Identical | +| Config freezing | Yes, identical | Yes, identical | +| Hot-reload | No | No | +| Updates on restart | Yes ✓ | Yes ✓ | +| File location | Repo root (fallback) | EXE directory | + +--- + +## CODE PATH: From Config Loading to Display + +``` +main.rs:54 + ↓ +config::load_config() ← Reads config.toml from disk [ONCE] + ↓ +Returns Config struct + ↓ +main.rs:76 + ↓ +Moved into AzureAppManager::new() + ↓ +app.rs:28-34: BackgroundRenderer created + └─ opacity, fallback_color COPIED from config [FROZEN] + └─ Stored in Arc + ↓ +app.rs:49: Config stored + └─ self.config = config [FROZEN] + ↓ +eframe event loop (every frame ~60x/sec): + ├─ app.rs:375-457: update() called + │ ├─ app.rs:380: Read colors from self.config [frozen values] + │ ├─ app.rs:467: Read text sizing from self.config [frozen values] + │ ├─ app.rs:489: Read position offset from self.config [frozen values] + │ └─ app.rs:505-507: Render background with frozen renderer + │ + └─ Every frame: Same frozen values used again + (Because no code reloads or updates them) +``` + +--- + +## What Would Be Needed to Fix + +### 1. File Watching +```rust +// Add to Cargo.toml +notify = "6.0" // File system event watcher +``` + +### 2. Config Reload Mechanism +```rust +pub fn reload_config_from_disk() -> Config { + config::load_config() // Re-read from disk +} +``` + +### 3. Mutable Config Storage +```rust +// Change from: +config: Config + +// To: +config: Arc> +``` + +### 4. Updatable Background Renderer +```rust +// Either: +// - Store as Arc> +// - Or recreate it every frame with current config values +``` + +### 5. Hot Reload Trigger +```rust +if config_file_changed { + new_config = reload_config_from_disk(); + update_background_renderer(&new_config); + ctx.request_repaint(); +} +``` + +--- + +## Summary Table + +| Component | Location | Issue | Status | +|-----------|----------|-------|--------| +| Config loading | `main.rs:54` | Loaded once | FROZEN | +| Config movement | `main.rs:76` | Moved via ownership | FROZEN | +| Background values | `app.rs:28-34` | Copied into struct | FROZEN | +| Config storage | `app.rs:49` | Stored as owned field | FROZEN | +| Color application | `app.rs:380-444` | Reads frozen config | EVERY FRAME | +| Background rendering | `background.rs:106` | Uses frozen values | EVERY FRAME | +| Reload mechanism | *Not found* | No file watching | ✗ MISSING | + +--- + +## Key Insight + +**The application architecture assumes configuration is set once at startup and never changes.** This is a design choice, not a bug. The same frozen-config behavior exists in both `cargo run` and standalone EXE. + +The perceived difference comes from: +- **cargo run**: Users frequently restart processes → see updated config +- **Standalone EXE**: Users expect live updates → see frozen config + +Both are executing identical code with identical behavior. + diff --git a/CONFIG_ANALYSIS_REFERENCE.txt b/CONFIG_ANALYSIS_REFERENCE.txt new file mode 100644 index 0000000..00380fb --- /dev/null +++ b/CONFIG_ANALYSIS_REFERENCE.txt @@ -0,0 +1,243 @@ +================================================================================ +COMPLETE REFERENCE: Config Struct Analysis +================================================================================ + +Two comprehensive analysis documents have been created: + +1. analysis.txt (212 lines) + - Complete technical breakdown + - All code paths with exact line numbers + - Detailed freezing point mechanisms + - Complete execution timeline + - Comparison tables + +2. ANALYSIS_SUMMARY.md (226 lines) + - Executive summary + - Quick-reference guide + - Visual diagrams and flows + - What needs to be fixed + - Implementation requirements + +ABSOLUTE PATHS: +C:\Users\DG2210\OneDrive - Gemeente Vught\Documenten\Git\I-SecretUpdate\analysis.txt +C:\Users\DG2210\OneDrive - Gemeente Vught\Documenten\Git\I-SecretUpdate\ANALYSIS_SUMMARY.md + +================================================================================ +THE PROBLEM IN ONE SENTENCE +================================================================================ + +Config is loaded ONCE at startup and frozen in immutable data structures, +with NO code to reload it during runtime. + +================================================================================ +THE SOLUTION IN ONE SENTENCE +================================================================================ + +Implement file watching, config reloading, and mutable storage patterns to +enable hot-reload of configuration changes. + +================================================================================ +CRITICAL FINDINGS CHECKLIST +================================================================================ + +[✓] Config loaded once in main.rs:54 +[✓] Config values COPIED into BackgroundRenderer (app.rs:28-34) +[✓] BackgroundRenderer stored in Arc (immutable) +[✓] Config stored in AzureAppManager.config field (app.rs:49) +[✓] Every frame reads same frozen values (app.rs:375-457) +[✓] No reload mechanism exists (zero file watching code) +[✓] No polling of config.toml (no modification tracking) +[✓] Both cargo run and standalone behave identically +[✓] File location is the ONLY difference between them +[✓] Users perceive difference due to process restart frequency + +================================================================================ +CODE CHANGES NEEDED FOR HOT-RELOAD +================================================================================ + +1. Add to Cargo.toml: + notify = "6.0" + +2. Modify src/app.rs: + - Change: config: Config + - To: config: Arc> + + - Change: state.background_renderer storage + - To: Arc> or recreate every frame + +3. Add config reload function in src/config/window_config.rs: + - pub fn reload_config_from_disk() -> Config + +4. Add file watcher in main loop: + - Detect config.toml changes + - Call reload_config_from_disk() + - Update Arc> + - Call ctx.request_repaint() + +5. Add synchronization: + - Lock config before reading + - Lock config before writing + - Handle lock contention + +================================================================================ +EVIDENCE FOR ONE-TIME INITIALIZATION +================================================================================ + +SEARCH 1: "load_config" calls + Result: Found in main.rs:54 only + Conclusion: Called ONCE during startup + +SEARCH 2: "config =" mutations + Result: Configuration field never reassigned after app.rs:49 + Conclusion: Never updated after initialization + +SEARCH 3: "notify", "watch", "polling", "reload" + Result: NOT FOUND in codebase + Conclusion: No hot-reload mechanism exists + +SEARCH 4: "Arc>" + Result: NOT FOUND + Conclusion: No mutable shared access pattern used + +SEARCH 5: "file.metadata", "modification_time" + Result: NOT FOUND + Conclusion: No file change detection exists + +================================================================================ +EVIDENCE FOR FROZEN BACKGROUND RENDERER +================================================================================ + +FREEZE POINT: src/app.rs:28-34 + +let background_renderer = Some(Arc::new(BackgroundRenderer::new( + &cc.egui_ctx, + &config.appearance.background_image, + config.appearance.background_opacity, // ← COPIED + config.appearance.fallback_color, // ← COPIED + config.appearance.fallback_color_opacity, // ← COPIED +))); + +RESULT: +- Values copied into struct at this point +- Stored in Arc (prevents mutation) +- Never recreated +- Never updated + +USAGE: src/app.rs:506 +if let Some(bg) = &self.state.background_renderer { + bg.render_fullscreen(ctx); // Uses frozen values +} + +CALLED: Every frame in update() method +RESULT: Same frozen values rendered 60+ times per second + +================================================================================ +EVIDENCE FOR FROZEN CONFIG FIELD +================================================================================ + +STORAGE POINT: src/app.rs:49 + +pub struct AzureAppManager { + config: Config, // ← Stored as owned field + // ... +} + +Self { + config, // ← Stored once, never updated + // ... +} + +RESULT: +- Config struct stored as owned value +- Not Arc, not Mutex, not mutable reference +- Can only be read, never mutated +- Never reloaded from disk + +USAGE POINTS: +- app.rs:380 - Read colors: let colors = self.config.colors.as_ref(); +- app.rs:467 - Read text sizing: &self.config.text_sizing +- app.rs:489 - Read position: self.config.window.position_offset_x + +CALLED: Every frame in update() method +RESULT: Same frozen config values read 60+ times per second + +================================================================================ +PATH RESOLUTION FOR BOTH ENVIRONMENTS +================================================================================ + +Code Location: src/config/window_config.rs:234-240 + +let config_path = if let Ok(exe_path) = std::env::current_exe() { + exe_path + .parent() + .map(|p| p.join("config.toml")) + .unwrap_or_else(|| Path::new("./config.toml").to_path_buf()) +} else { + Path::new("./config.toml").to_path_buf() +}; + +CARGO RUN: +Step 1: std::env::current_exe() returns target\debug\create-app-secret.exe +Step 2: parent() = target\debug\ +Step 3: Look for target\debug\config.toml → NOT FOUND +Step 4: Fall back to ./config.toml +Step 5: Resolve in repo root → FOUND ✓ +Step 6: Load config from repo root + +Result: Config found and loaded from repo root + +STANDALONE EXE: +Step 1: std::env::current_exe() returns exe directory path +Step 2: parent() = exe directory +Step 3: Look for config.toml in exe directory → MAYBE FOUND +Step 4: If not found, fall back to ./config.toml +Step 5: Resolve relative path +Step 6: Load config if found + +Result: Config found in exe directory OR user's working directory + +KEY POINT: Different file locations, but same code executes in both cases + +================================================================================ +WHY CARGO RUN APPEARS TO WORK +================================================================================ + +User Perspective: + +1. cargo run + - Application starts + - Config loaded from repo root + - Application displays settings + +2. User modifies config.toml + - Still running in background + - Changes have NO effect (no reload mechanism) + +3. cargo run (run command again) + - OLD PROCESS EXITS + - NEW PROCESS STARTS + - load_config() called in new process + - Reads UPDATED config.toml + - Application displays new settings + +4. User thinks: "Config changes work with cargo run!" + +Reality: The application didn't reload. The user created a new process. + +================================================================================ +WHY STANDALONE EXE APPEARS BROKEN +================================================================================ + +User Perspective: + +1. Run Create-App-Secret.exe + - Application starts + - Config loaded + - Application displays settings + +2. User modifies config.toml + - Application still running + - Changes have NO effect (no reload mechanism) + - User expects application to update automatically + +3. Application does NOT update diff --git a/analysis.txt b/analysis.txt new file mode 100644 index 0000000..0c9470b --- /dev/null +++ b/analysis.txt @@ -0,0 +1,212 @@ +================================================================================ +DETAILED ANALYSIS: Config Struct Usage - cargo run vs Standalone EXE +================================================================================ + +EXECUTIVE SUMMARY +================================================================================ + +Config modifications don't work in either cargo run OR standalone EXE after +startup. They only appear to work in cargo run because each run is a NEW +process that reloads the config file from disk. + +ROOT CAUSE: One-time initialization with frozen values, no reload mechanism. + +KEY FINDINGS: +1. config.toml is loaded ONCE in main.rs:54 +2. Config values COPIED into BackgroundRenderer (immutable Arc) +3. Config STORED in AzureAppManager (never updated) +4. Every frame reads SAME frozen values +5. ZERO reload/hot-update mechanism exists +6. Working directory only affects WHERE file is looked for, not HOW changes propagate + +================================================================================ +1. CONFIG LOADING - src/main.rs Lines 35-80 +================================================================================ + +#[tokio::main] +async fn main() -> Result<(), Box> { + // LINE 54: LOADED ONCE + let app_config = config::load_config(); + + // LINES 60-64: VALUES APPLIED TO EFRAME + let viewport_builder = egui::ViewportBuilder::default() + .with_inner_size([app_config.window.width, app_config.window.height]) + .with_min_inner_size([app_config.window.min_width, app_config.window.min_height]) + .with_transparent(app_config.window.use_transparency) + .with_icon(icon_data); + + // LINES 73-77: MOVED INTO MANAGER (ownership transferred, never accessed in main again) + eframe::run_native( + "Create App Secret", + options, + Box::new(move |cc| Ok(Box::new(AzureAppManager::new(cc, auth, app_config)))), + )?; + + Ok(()) +} + +CRITICAL: The 'move' keyword transfers ownership to the closure. After this +point, no other code in main can access app_config. + +================================================================================ +2. CONFIG FILE PATH RESOLUTION - src/config/window_config.rs Lines 232-295 +================================================================================ + +pub fn load_config() -> Config { + let config_path = if let Ok(exe_path) = std::env::current_exe() { + exe_path + .parent() + .map(|p| p.join("config.toml")) + .unwrap_or_else(|| Path::new("./config.toml").to_path_buf()) + } else { + Path::new("./config.toml").to_path_buf() + }; + + if config_path.exists() { + // Load and parse TOML + } else { + // Create defaults + } + + Config::default() // Fallback +} + +CARGO RUN PATH: + current_exe() = C:\...\I-SecretUpdate\target\debug\create-app-secret.exe + parent = C:\...\I-SecretUpdate\target\debug\ + looks for C:\...\I-SecretUpdate\target\debug\config.toml ❌ NOT FOUND + falls back to ./config.toml + resolves to C:\...\I-SecretUpdate\config.toml ✓ FOUND + +STANDALONE EXE PATH: + current_exe() = C:\wherever\user\puts\Create-App-Secret.exe + parent = C:\wherever\user\puts\ + looks for C:\wherever\user\puts\config.toml ✓ OR ❌ + if not found, falls back to ./config.toml + +KEY POINT: Both use identical code and path resolution logic. The file +location is the ONLY difference, not how changes are handled. + +================================================================================ +3. FIRST INITIALIZATION - FREEZE POINT 1 & 2 - src/app.rs Lines 20-52 +================================================================================ + +pub struct AzureAppManager { + state: AppState, + auth: Arc, + graph_client: GraphApiClient, + keyvault_client: KeyVaultClient, + vault_discovery: VaultDiscovery, + config: Config, // ← FREEZE POINT 2: Stored, never mutated + position_applied: bool, +} + +impl AzureAppManager { + pub fn new(cc: &eframe::CreationContext<'_>, + auth: Arc, + config: Config) -> Self { + + let mut state = AppState::new(); + + // FREEZE POINT 1: Create BackgroundRenderer with COPIED config values + let background_renderer = Some(Arc::new(BackgroundRenderer::new( + &cc.egui_ctx, + &config.appearance.background_image, + config.appearance.background_opacity, // ← COPIED HERE + config.appearance.fallback_color, // ← COPIED HERE + config.appearance.fallback_color_opacity, // ← COPIED HERE + ))); + state.background_renderer = background_renderer; + + Self { + state, + auth, + graph_client, + keyvault_client, + vault_discovery, + config, // ← FREEZE POINT 2: Stored as owned, never updated + position_applied: false, + } + } +} + +WHAT GETS FROZEN: + +FREEZE POINT 1 (BackgroundRenderer): + - opacity value COPIED + - fallback_color value COPIED + - fallback_color_opacity value COPIED + - These are stored in Arc (immutable) + - Used every frame via render_fullscreen() + - NEVER RECALCULATED OR UPDATED + +FREEZE POINT 2 (Config Field): + - Entire Config struct stored + - Not mutable reference, not Arc> + - Owned field that cannot be changed + - Read every frame in update() + - NEVER RELOADED FROM DISK + +================================================================================ +4. BACKGROUND RENDERING - EVERY FRAME - src/ui/background.rs Lines 106-152 +================================================================================ + +pub fn render_fullscreen(&self, ctx: &Context) { + let screen_rect = ctx.screen_rect(); + let painter = ctx.layer_painter(egui::LayerId::background()); + + // Render with FROZEN fallback_color_opacity + let fallback_with_opacity = Color32::from_rgba_unmultiplied( + self.fallback_color.r(), + self.fallback_color.g(), + self.fallback_color.b(), + (self.fallback_color_opacity * 255.0) as u8, // ← FROZEN VALUE + ); + painter.rect_filled(screen_rect, 0.0, fallback_with_opacity); + + if let Some(texture) = &self.texture { + // Render with FROZEN opacity + let tint = Color32::from_rgba_unmultiplied( + 255, 255, 255, + (self.opacity * 255.0) as u8 // ← FROZEN VALUE + ); + painter.image(texture.id(), screen_rect, uv_rect, tint); + } +} + +CALLED EVERY FRAME FROM: src/app.rs Line 506 + + if let Some(bg) = &self.state.background_renderer { + bg.render_fullscreen(ctx); // Uses same frozen values + } + +RESULT: Background renders with identical opacity and colors every frame. +User modifies config.toml, but application still renders with original values. + +================================================================================ +5. COLOR APPLICATION - EVERY FRAME - src/app.rs Lines 375-457 +================================================================================ + +fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let mut visuals = egui::Visuals::dark(); + + // LINE 380: READ FROM FROZEN CONFIG + let colors = self.config.colors.as_ref(); + + if let Some(color_config) = colors { + let slider = &color_config.slider; + let text = &color_config.text; + + // Apply colors + visuals.widgets.noninteractive.fg_stroke.color = + egui::Color32::from_rgb(text.normal[0], text.normal[1], text.normal[2]); + visuals.widgets.inactive.fg_stroke.color = + egui::Color32::from_rgb(text.inactive[0], text.inactive[1], text.inactive[2]); + visuals.widgets.hovered.fg_stroke.color = + egui::Color32::from_rgb(text.hover[0], text.hover[1], text.hover[2]); + // ... more color applications ... + + // Apply slider colors + visuals.widgets.noninteractive.bg_fill = + egui::Color32::from_rgb(slider.inactive[0], slider.inactive[1], slider.inactive[2]); + // ... more slider col