Repository navigation
Many new features and UX improvements - #30
Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughAdds community-contributed switch submission tooling (issue template, processing workflow, DB, and CLI script), integrates CommunityDB lookups into analysis, expands the modern GUI with many new views and UI features, refactors Addon and Notification services, and extends Intune service APIs and model fields. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User / Issue Creator
participant GitHub as GitHub Issues
participant Workflow as GitHub Actions
participant Script as process_issue_ops.py
participant DB as switches.json
participant PR as Create PR action
User->>GitHub: Submit issue using add_switch form
GitHub->>Workflow: Trigger on issue opened/labeled (automation)
Workflow->>Workflow: Checkout repo, setup Python
Workflow->>Script: Run script with issue body path
Script->>Script: Parse form fields, validate required fields
Script->>DB: Load existing switches.json
Script->>DB: Check for duplicates and append new entry
Script->>Workflow: Exit status indicates success
Workflow->>PR: Create pull request with changes
PR->>User: PR created referencing issue
sequenceDiagram
participant Analyzer as AnalysisController
participant MSI as MSI Analyzer
participant DB as CommunityDBService
participant Local as switches.json
participant Result as AnalysisResult
Analyzer->>MSI: Extract MSI properties from installer
MSI-->>Analyzer: Return properties (including product_code)
Analyzer->>DB: Request lookup by file hash or name
DB->>Local: Read/consult switches.json
Local-->>DB: Return matching switch entries (if any)
DB-->>Analyzer: Return community switches or null
alt Community match found
Analyzer->>Result: Populate install_switches and set community_match=true
else No match
Analyzer->>Result: Leave install_switches empty and community_match=false
end
Analyzer-->>Result: Return final AnalysisResult
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (30)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/switchcraft/gui_modern/views/home_view.py (1)
59-82: Card navigation indices don't match app.py navigation rail destinations.The card indices here don't align with the navigation rail in
app.py:
- Line 60: Analyzer card uses index 1, but Analyzer is at index 2 in app.py
- Line 61: AI Helper uses index 2, but Helper/Generate is at index 3
- Line 62: Winget uses index 3, but Winget/Apps is at index 1
- Line 74: Stacks uses index 10, but Stacks is at index 12 in app.py
- Line 80: Settings uses index 7, but Settings is at index 9
This will cause cards to navigate to wrong tabs.
🤖 Fix all issues with AI agents
In @.github/workflows/process_switch_issue.yml:
- Around line 25-26: The workflow step named "Extract Issue Body" directly
interpolates github.event.issue.body into the shell which allows command
injection; instead, pass the issue body through an environment variable (set
ISSUE_BODY to github.event.issue.body) and write it to issue_body.txt using a
safe, quoted print (e.g., use printf '%s' or similar with the quoted ISSUE_BODY)
so the contents are treated as data, not executable shell commands.
In @src/switchcraft/data/community/switches.json:
- Around line 1-12: CommunityDBService is reading from
data/community_switches.json while process_issue_ops.py writes to
src/switchcraft/data/community/switches.json, causing script-added entries to
never be seen; update either CommunityDBService to read from
"src/switchcraft/data/community/switches.json" or change process_issue_ops.py to
write to "data/community_switches.json" so both use the identical canonical
path, and ensure the write routine in process_issue_ops.py that creates new
entries also sets the contributor field (use issue author if available or a
sensible default like "SwitchCraftBot") to match the schema shown in the seed
file.
In @src/switchcraft/gui_modern/app.py:
- Around line 107-112: The class assigns self.window_event to
self.page.on_window_event but never defines it; add a method named
window_event(self, event) on the same class that matches the GUI framework's
window event handler signature, inspects the incoming event (e.g., close/closing
events), implements the intended behavior (e.g., prompt/confirm close, set
self.page.window_prevent_close = False to allow closing, or call any existing
close/cleanup routines), and ensure any references to
self.page.window_prevent_close or cleanup logic used in setup are invoked from
this method so no AttributeError occurs when a window event fires.
In @src/switchcraft/gui_modern/views/home_view.py:
- Around line 16-26: The lambda in _create_card currently references an
undefined name on_navigate; change it to use the instance attribute
self.on_navigate so the button calls the correct handler. Locate the
ElevatedButton in _create_card and replace the on_click lambda to reference
self.on_navigate (and guard it the same way, e.g., call
self.on_navigate(target_idx) if self.on_navigate else None) so the closure uses
the bound method set in __init__.
In @src/switchcraft/gui_modern/views/macos_wizard_view.py:
- Around line 84-90: The file has mis-indented blocks that will prevent import;
correct the indentation so control blocks align properly (e.g., in method
_generate_script ensure the "if not url or not name:" block and its body lines
(self._show_snack and return) are indented one level inside the method), and fix
similar mis-indentations in other methods referenced (adjust the blocks inside
the methods that contain the snack/show or return statements around the app
name/url handling and any subsequent nested blocks so they are indented
consistently under their def). Locate and fix indentation for the methods named
_generate_script and the nearby methods that contain the snack/return blocks
(ensure all conditional and loop bodies are indented exactly one level under
their respective def or control statement).
In @src/switchcraft/gui_modern/views/packaging_wizard_view.py:
- Around line 1-13: The module is missing an import for subprocess which causes
a NameError when _sign_script calls subprocess.run(); add "import subprocess" to
the top-level imports in this file so _sign_script can invoke subprocess.run()
without error, ensuring the import is placed alongside the other stdlib imports
(e.g., threading, tempfile) to keep imports organized.
- Around line 216-248: The function currently returns early with return
ft.Tabs(...), making subsequent code unreachable and leaving tabs undefined;
replace the early return by assigning that Tabs object to a local variable
(e.g., tabs = ft.Tabs(...)) before creating self.autopilot_btn and then return
the final ft.Column that includes self.mode_radio, tabs, and the autopilot
button wired to self._run_autopilot; ensure you reference the same local_content
and url_content when building the Tabs and keep the ft.Column return as the
single return at the end of the function.
In @src/switchcraft/gui_modern/views/script_upload_view.py:
- Around line 77-90: The credential-check block in _upload_ps_script has extra
leading indentation before the if-body lines causing a syntax/indentation error;
remove the extra space so the two statements under "if not all([tenant, client,
secret]):" (the calls to self._show_snack(...) and the subsequent return) are
indented exactly one level inside the if, and apply the same fix to the
analogous credential-validation block around lines 169-182 (align the
_show_snack and return to the if body).
In @src/switchcraft/models.py:
- Around line 9-13: Remove the duplicate product_version field declaration in
the dataclass (the repeated "product_version: Optional[str] = None") so only one
product_version attribute remains; locate the duplicate occurrence near the
other fields (product_version, manufacturer, product_code, install_path) in the
class in src/switchcraft/models.py and delete the redundant line, leaving a
single product_version definition and verifying no other duplicated field names
exist.
In @src/switchcraft/services/addon_service.py:
- Around line 83-122: The install_addon method currently calls
z.extractall(target) which is vulnerable to Zip Slip; replace the blind extract
with per-member path validation: iterate over each ZipInfo from z.infolist(),
compute the final path by joining target with the member name and resolving it
(use Path(...) .resolve()), and ensure the resolved path starts with
target.resolve() to reject absolute or ../ escapes; only extract members that
pass this check (create parent dirs as needed and write the file), and log/raise
an error if any entry is invalid. Reference install_addon, z.extractall,
z.infolist(), target, and manifest.json when locating the code to change.
In @src/switchcraft/services/intune_service.py:
- Around line 630-648: There are two definitions of search_apps which causes the
later one to override the earlier; consolidate by keeping a single
implementation: either merge the behavior of the direct API caller into the
original search_apps that delegates to list_apps, or rename the second
implementation (e.g., search_apps_direct) and update any call sites that should
use it (packaging_wizard_view.py, intune_store_view.py) to avoid silent
override; ensure the final search_apps signature matches callers, remove the
duplicate definition, and run tests/linters to confirm no remaining references
to the removed symbol.
- Around line 500-604: The upload_mobile_lob_app method dereferences app_info
without a null check; change the signature or start of upload_mobile_lob_app to
ensure app_info is a dict (e.g., app_info = app_info or {}) so all
app_info.get(...) calls in default_info are safe, and implement the merge block
(currently pass) to update default_info with sanitized keys from app_info
(whitelist allowed fields like displayName, description, publisher, productCode,
productVersion, installCommandLine) while removing incompatible keys (e.g., any
keys not in that whitelist); also modify the commit payload for the file commit
in upload_mobile_lob_app so that fileEncryptionInfo is omitted entirely when
unencrypted instead of sending {"fileEncryptionInfo": None} (only include
fileEncryptionInfo when you have a non-null value).
🟠 Major comments (16)
src/switchcraft/gui_modern/views/detection_tester_view.py-6-7 (1)
6-7: Guard Windows-specific imports for cross-platform compatibility.Top-level imports of
winregandwin32apiwill causeImportErroron non-Windows platforms, preventing the entire module from loading. While this is a Windows-specific tool, graceful handling would improve maintainability.🐛 Proposed fix with lazy imports or platform check
import flet as ft import logging import subprocess import os +import sys from pathlib import Path -import winreg -import win32api + +if sys.platform == "win32": + import winreg + import win32api +else: + winreg = None + win32api = None logger = logging.getLogger(__name__)Then add a check at the start of methods using these modules:
def _check_registry(self, key_path, value_name, expected_value): if winreg is None: return False, "Registry checks only available on Windows" # ... rest of methodsrc/switchcraft/gui_modern/views/detection_tester_view.py-260-298 (1)
260-298: Ensure temp file cleanup on all exit paths.If
subprocess.runraises an exception, the temp file created at line 261 won't be deleted. Usetry/finallyto guarantee cleanup.🐛 Proposed fix
def _check_script(self, script_content): # Run script in temp file import tempfile + temp_path = None try: with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".ps1") as f: f.write(script_content) temp_path = f.name cmd = ["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", temp_path] startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW res = subprocess.run(cmd, capture_output=True, text=True, startupinfo=startupinfo) - os.remove(temp_path) - stdout = res.stdout.strip() stderr = res.stderr.strip() exit_code = res.returncode if exit_code == 0: if stdout: return True, f"Detected (Exit 0 + Stdout): {stdout[:100]}..." else: return False, "Not Detected (Exit 0 but Empty Stdout)" else: return False, f"Not Detected (Exit Code {exit_code}). Err: {stderr[:100]}" except Exception as e: return False, f"Script Execution Error: {e}" + finally: + if temp_path and os.path.exists(temp_path): + os.remove(temp_path)scripts/process_issue_ops.py-66-72 (1)
66-72: Exit code 0 on duplicate will create an empty PR.When a duplicate entry is found, the script exits with code 0, which signals success to GitHub Actions. The workflow will then proceed to create a PR with no changes. Exit with a non-zero code to fail the workflow gracefully, or use GitHub Actions outputs to conditionally skip PR creation.
🐛 Proposed fix
# Check for duplicate (naive check) for entry in db: if (entry.get("app_name") == new_entry.get("app_name") and entry.get("version") == new_entry.get("version")): print("Entry already exists for this version.") - # We might want to update it instead? For now, skip. - sys.exit(0) + sys.exit(1)scripts/process_issue_ops.py-14-16 (1)
14-16: Regex incorrectly captures subsequent headers when a field is empty.The pattern
(.+?)requires at least one character, causing it to match across empty fields. When a field is truly empty (e.g.,### Application Name\n\n### Version), the regex captures the next header and its value instead—e.g.,"Application Name": "### Version\n\n1.0.0"instead of an empty string. This corrupts the parsed data.The pattern needs to handle optional values. Consider using
(.*)with a modified lookahead or restructure the regex to explicitly match empty lines followed by the next###.src/switchcraft/gui_modern/app.py-168-194 (1)
168-194: Navigation index comments are inconsistent with actual positions.The inline comments indicate index numbers that don't match the actual list positions. For example:
- Line 169: "History" is at index 8, but comment says "# 8 History"
- Line 173: "Settings" comment says "# 7 Settings" but it's at index 9
- Lines 175-188: Comments show indices 8, 9, 10, 11, 14, 15, 16 but actual indices are 10-16
This inconsistency will cause navigation bugs when
goto_tabor card clicks use hardcoded indices.src/switchcraft/services/addon_service.py-69-81 (1)
69-81: Dynamic code execution from untrusted addons allows arbitrary code execution.The
load_addon_viewmethod usesimportlibto execute arbitrary Python code from addon files. The addon manager UI explicitly allows users to upload custom ZIP files via file picker, and theinstall_addonmethod performs no signature verification or integrity checks. While ADDONS.md documents a warning to use only trusted sources, this is insufficient—there are no technical safeguards preventing execution of malicious addon code.Implement addon signing with signature verification, or add sandboxing to restrict addon capabilities in future iterations. For now, the upload feature should prominently warn users that addons execute with full app privileges.
src/switchcraft/gui_modern/views/group_manager_view.py-10-23 (1)
10-23: Initialize and gateself.tokenbefore create/delete (avoid race/AttributeError).
Create/delete assumeself.tokenexists and is valid; users can trigger dialogs before_load_data()finishes or succeeds.Proposed fix
class GroupManagerView(ft.Column): def __init__(self, page: ft.Page): @@ self.filtered_groups = [] + self.token = None @@ def _load_data(self): @@ def _bg(): try: self.token = self.intune_service.authenticate(tenant, client, secret) @@ finally: @@ def _show_create_dialog(self, e): + if not self.token: + self._show_snack("Load groups / authenticate first", ft.Colors.RED) + return @@ def _confirm_delete(self, e): if not self.selected_group: return + if not self.token: + self._show_snack("Load groups / authenticate first", ft.Colors.RED) + returnAlso applies to: 83-109, 152-184, 185-216
src/switchcraft/gui_modern/views/stack_manager_view.py-69-82 (1)
69-82: Handle JSON read/write failures explicitly (avoid crashing UI callbacks).
Right now_save_stacks()can raise and break the UI, and_load_stacks()silently hides corruption without leaving diagnostics.Proposed fix
def _load_stacks(self): if not self.stacks_file.exists(): return {} try: - with open(self.stacks_file, "r") as f: - return json.load(f) - except Exception: - return {} + with open(self.stacks_file, "r", encoding="utf-8") as f: + data = json.load(f) + # minimal schema validation + if not isinstance(data, dict): + raise ValueError("stacks.json must be a JSON object") + for k, v in data.items(): + if not isinstance(k, str) or not isinstance(v, list): + raise ValueError("stacks.json must map string -> list") + return data + except Exception as ex: + logger.warning("Failed to load stacks DB; starting empty: %s", ex) + return {} def _save_stacks(self): self.stacks_file.parent.mkdir(parents=True, exist_ok=True) - with open(self.stacks_file, "w") as f: - json.dump(self.stacks, f, indent=4) + try: + with open(self.stacks_file, "w", encoding="utf-8") as f: + json.dump(self.stacks, f, indent=4) + except Exception as ex: + logger.error("Failed to save stacks DB: %s", ex) + raiseAlso applies to: 83-86
src/switchcraft/services/notification_service.py-8-70 (1)
8-70: Add synchronization + maketimestampserializable (threaded UI elsewhere makes this risky).
This service is very likely to be called across threads in this PR, but it has no locking; alsodatetime.now()is timezone-naive and not JSON-serializable.Proposed fix
import logging import uuid -from datetime import datetime +from datetime import datetime, timezone +import threading from typing import List, Dict, Callable @@ class NotificationService: _instance = None _listeners: List[Callable] = [] @@ def __new__(cls): if cls._instance is None: cls._instance = super(NotificationService, cls).__new__(cls) cls._instance.notifications = [] + cls._instance._lock = threading.RLock() return cls._instance - def add_notification(self, title: str, message: str, type: str = "info"): + def add_notification(self, title: str, message: str, type: str = "info"): @@ - notif = { + notif = { "id": str(uuid.uuid4()), "title": title, "message": message, "type": type, - "timestamp": datetime.now(), + "timestamp": datetime.now(timezone.utc).isoformat(), "read": False } - self.notifications.insert(0, notif) - self._notify_listeners() + with self._lock: + self.notifications.insert(0, notif) + self._notify_listeners() return notif @@ def _notify_listeners(self): - for callback in self._listeners: + # Copy to avoid issues if listeners are added/removed during callbacks + for callback in list(self._listeners): try: callback() except Exception as e: logger.error(f"Error in notification listener: {e}")src/switchcraft/services/community_db_service.py-39-46 (1)
39-46: Add error handling to_get_hash()to make hash lookup best-effort (graceful fallback on unreadable files).The method can raise exceptions on file access errors, preventing the fallback to name-based lookup in
analysis_controller.py. ReturnNoneon hash calculation failure so the caller can proceed to alternative strategies.Proposed fix
def _get_hash(self, filepath): - h = hashlib.sha256() - with open(filepath, "rb") as f: - while chunk := f.read(8192): - h.update(chunk) - return h.hexdigest() + try: + h = hashlib.sha256() + with open(filepath, "rb") as f: + while chunk := f.read(8192): + h.update(chunk) + return h.hexdigest() + except Exception as ex: + logger.debug("Failed to hash %s: %s", filepath, ex) + return Nonedef get_switches_by_hash(self, file_path): """Calculate hash and lookup.""" if not Path(file_path).exists(): return None sha256 = self._get_hash(file_path) + if not sha256: + return None return self.db.get("hash_map", {}).get(sha256)Also applies to: 56-61
src/switchcraft/gui_modern/views/script_upload_view.py-95-117 (1)
95-117: Refactor to use Flet's thread-safe UI update pattern.Both upload flows (lines 95-117 and 187-212) directly mutate controls and call
self.update()from a rawthreading.Thread, which violates Flet's threading guidelines. Flet documentation explicitly recommends either:
- Async tasks (preferred): Use
async/awaitwithpage.run_task()for background I/O- Thread-safe execution: Use
page.run_thread()and marshal UI updates back viapage.run_task()to avoid concurrentpage.update()callsCalling
update()directly from arbitrary threads can cause intermittent errors. Refactor both locations to use one of the documented patterns.src/switchcraft/gui_modern/views/addon_manager_view.py-63-78 (1)
63-78: Usepage.run_task()orpage.run_thread()instead of plainthreading.Thread()for UI updates.Flet enforces a single-threaded async UI model—mutating control state (e.g.,
DataTable.rows) and callingupdate()from arbitrary worker threads is unsafe and can fail intermittently. Both_load_data()and_update_table()violate this constraint.Move background work to a task scheduled via the page's event loop so that all control mutations and
update()calls happen on the UI thread. Alternatively, collect results in the worker, then usepage.run_task()to perform the mutation and update on the main context.This pattern also appears in
_install()(lines 109–118).src/switchcraft/gui_modern/views/group_manager_view.py-95-108 (1)
95-108: Refactor to marshal UI updates from background thread to page event loop.The code calls
_update_table()andself.update()from a background thread spawned withthreading.Thread(). Flet requires all UI mutations (includingDataTable.rowsmodifications andControl.update()calls) to occur on the page event loop. Direct thread-based updates can cause race conditions and intermittent errors.Use
page.run_task()orpage.run_thread()with proper result marshalling instead. Pattern: background work updates the data model; schedule the UI update on the page viapage.run_task()so that_update_table()andupdate()execute in the page context.Also applies to: 110-126
src/switchcraft/gui_modern/views/macos_wizard_view.py-147-181 (1)
147-181: Replace daemon thread with Flet'spage.run_thread()and marshal UI updates back to the page event loop.The code directly mutates control properties and calls
self.update()from a background thread, which is unsafe in Flet. Flet's UI state is managed on the page event loop; arbitrary thread access causes intermittent errors and crashes.Use
page.run_thread()for the blocking Intune operation and schedule UI updates back to the page viapage.run_task():async def _upload_bg(): try: token = await page.run_thread( self.intune_service.authenticate, tenant, client, secret ) await page.run_thread( self.intune_service.upload_macos_shell_script, token, f"Install {self.app_name.value}", f"Auto-generated installer for {self.app_name.value}", self.preview_field.value ) self.status_txt.value = "Success! Script Uploaded to MacOS > Shell Scripts." self.status_txt.color = ft.Colors.GREEN except Exception as ex: self.status_txt.value = f"Error: {ex}" self.status_txt.color = ft.Colors.RED finally: self.upload_btn.disabled = False self.update() # In _upload_to_intune: self.status_txt.value = "Uploading..." self.upload_btn.disabled = True self.update() page.run_task(_upload_bg)src/switchcraft/services/intune_service.py-605-629 (1)
605-629: Payload has undocumented field and is missing required property; fix before shipping.The endpoint and
@odata.typeare correct per Microsoft Graph API, but the payload has issues:
"isSupersedence": Trueis not a documented field in the Graph API spec and will likely be rejected or ignored."targetType"field is missing; the API expects this to be set to"parent"or"child".Remove
isSupersedenceand add"targetType": "parent"(or"child"depending on your relationship model).Also improve exception handling by using
raiseinstead ofraise eto preserve the original traceback:Fix traceback preservation
except Exception as e: logger.error(f"Failed to add supersedence: {e}") - raise e + raisesrc/switchcraft/services/intune_service.py-663-688 (1)
663-688: Major: Mutable default arg + payload configuration breaks M365 group creation.
Mutable default argument (
group_types=[]): While the current code doesn't mutate it, this is a Python anti-pattern; usegroup_types=Nonewith a default assignment instead.API payload inconsistency for M365 groups: The docstring documents
group_types=["Unified"]for M365 groups, but the code always setsmailEnabled=FalseandsecurityEnabled=Trueregardless of the group type. Per Microsoft Graph API requirements, M365 (Unified) groups requiremailEnabled=trueandsecurityEnabled=false. The current payload will fail or produce incorrect results when called with["Unified"].Fix: Conditionally set
mailEnabledandsecurityEnabledbased on whether"Unified"is ingroup_types:Proposed fix
- def create_group(self, token, name, description, group_types=[]): + def create_group(self, token, name, description, group_types=None): """ Creates a new group. group_types: ["Unified"] for M365, [] for Security. """ + group_types = group_types or [] + is_m365 = "Unified" in group_types + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} url = "https://graph.microsoft.com/v1.0/groups" payload = { "displayName": name, "description": description, - "mailEnabled": False, - "securityEnabled": True, + "mailEnabled": True if is_m365 else False, + "securityEnabled": False if is_m365 else True, "mailNickname": name.replace(" ", "").lower(), "groupTypes": group_types }
🟡 Minor comments (10)
src/switchcraft/gui_modern/views/detection_tester_view.py-236-247 (1)
236-247: Version comparison may behave unexpectedly with different segment counts.List comparison treats
[1, 0] < [1, 0, 0], so"1.0" >= "1.0.0.0"would returnFalse. Consider normalizing version lists to equal length by padding with zeros.💡 Suggested improvement
def parse_ver(v_str): - return [int(x) for x in v_str.split('.')] + parts = [int(x) for x in v_str.split('.')] + # Normalize to 4 segments for consistent comparison + while len(parts) < 4: + parts.append(0) + return partssrc/switchcraft/controllers/analysis_controller.py-137-140 (1)
137-140: Type mismatch:get_switches_by_nameexpects a filename string, not a Path.Looking at
CommunityDBService.get_switches_by_name(), it expects a filename string and usesPath(filename).stem.lower()internally. Passing aPathobject works due to implicit string conversion, butget_switches_by_hashalready handles the path, so passingpath(aPathobject) toget_switches_by_namewill use the full path as the stem, not just the filename.🐛 Proposed fix
db_switches = db.get_switches_by_hash(path) if not db_switches: # Fallback to name - db_switches = db.get_switches_by_name(path) + db_switches = db.get_switches_by_name(path.name)src/switchcraft/gui_modern/views/library_view.py-36-45 (1)
36-45: Status filter dropdown is wired but filtering logic is disabled.The
filter_dddropdown is visible and wired to_on_filter_change, which updatesself.status_filter, but the actual filtering logic in_refresh_grid(lines 84-88) is commented out. This creates a confusing UX where the dropdown appears functional but does nothing.Consider either implementing the status filtering or hiding the dropdown until it's ready.
Also applies to: 84-88
src/switchcraft/gui_modern/views/packaging_wizard_view.py-204-204 (1)
204-204: Typo: "Download form Web" should be "Download from Web".📝 Proposed fix
- ft.Text("Download form Web", size=24, weight=ft.FontWeight.BOLD), + ft.Text("Download from Web", size=24, weight=ft.FontWeight.BOLD),src/switchcraft/gui_modern/views/packaging_wizard_view.py-510-515 (1)
510-515: Duplicateself.txt_descin UI layout.
self.txt_descis added twice to the Column controls (lines 513-514), causing the description field to render twice.🧹 Proposed fix
self.txt_app_name, self.txt_publisher, self.txt_desc, - self.txt_desc, ft.Divider(),src/switchcraft/gui_modern/views/packaging_wizard_view.py-28-33 (1)
28-33: Remove duplicate assignments.
self.package_path(lines 28-29) andself.signing_cert(lines 31-32) are each assigned twice consecutively. These appear to be copy-paste errors.🧹 Proposed fix
self.generated_script_path = None self.package_path = None - self.package_path = None self.upload_info = {} self.signing_cert = SwitchCraftConfig.get_value("SigningCertThumbprint") - self.signing_cert = SwitchCraftConfig.get_value("SigningCertThumbprint") self.packaging_mode = "win32" # win32 or lobsrc/switchcraft/gui_modern/views/packaging_wizard_view.py-609-612 (1)
609-612: Remove duplicate stub method definition.
_run_uploadis defined twice. This stub definition (lines 609-611) is immediately shadowed by the full implementation below. Remove the stub to avoid confusion.🧹 Proposed fix
- def _run_upload(self, e): - # Override to include supersedence logic at the end - pass # Replaced by below logic via tool - def _run_upload(self, e):src/switchcraft/gui_modern/views/addon_manager_view.py-120-138 (1)
120-138: Handle “delete returned False” as a user-visible failure.
AddonService.delete_addon()can returnFalse; the UI always shows success.Proposed fix
def delete(e): aid = self.selected_addon['id'] def _bg(): try: - self.addon_service.delete_addon(aid) - self._show_snack("Addon deleted.", ft.Colors.GREEN) + ok = self.addon_service.delete_addon(aid) + if not ok: + self._show_snack("Addon not found (nothing deleted).", ft.Colors.RED) + return + self._show_snack("Addon deleted.", ft.Colors.GREEN) self.selected_addon = None self.app_page.close_dialog() self._load_data()src/switchcraft/gui_modern/views/stack_manager_view.py-124-139 (1)
124-139: Guard_select_stack()against stale/invalid names.
If the stack name is missing (e.g., race between delete/select or corrupted JSON),self.stacks[name]will throw.Proposed fix
def _select_stack(self, name): self.current_stack = name - items = self.stacks[name] + items = self.stacks.get(name) + if items is None: + self.current_stack = None + self.stack_content_list.controls.clear() + self._show_snack("Stack not found", ft.Colors.RED) + self.update() + returnsrc/switchcraft/services/intune_service.py-689-700 (1)
689-700: Changeraise eto bareraisefor cleaner exception re-raising. In Python 3, using bareraisein an except block re-raises the exception while preserving the original traceback. This is the preferred style overraise e.Proposed fix
except Exception as e: logger.error(f"Failed to delete group: {e}") - raise e + raise
🧹 Nitpick comments (17)
README.md (1)
66-66: Consider using an absolute GitHub URL.The relative link
../../issuesmay not render correctly in all contexts where the README is displayed (e.g., documentation sites, package registries, or mirrors).🔗 Proposed fix
-1. Go to the [Issues](../../issues) tab. +1. Go to the [Issues](https://github.com/FaserF/SwitchCraft/issues) tab.src/switchcraft/gui_modern/controls/skeleton.py (1)
46-51: Consider using a single persistent thread instead of recursive spawning.The current pattern spawns a new daemon thread every 0.8 seconds. While functional, this creates many short-lived threads over the component's lifetime. A single persistent loop thread would be more efficient.
♻️ Proposed refactor using a single thread
def did_mount(self): - self._animate() + threading.Thread(target=self._animation_loop, daemon=True).start() def will_unmount(self): self.aborted = True - def _animate(self): + def _animation_loop(self): + while not self.aborted: + self._toggle_opacity() + time.sleep(0.8) + + def _toggle_opacity(self): if self.aborted: return - - # Simple pulsing opacity animation self.opacity = 0.5 if self.opacity == 1.0 else 1.0 self.update() - - # Schedule next frame - if not self.aborted: - # ... comments ... - - def _loop(): - time.sleep(0.8) - if not self.aborted: - self._animate() - - threading.Thread(target=_loop, daemon=True).start()tests/test_modern_gui.py (1)
42-61: The dynamic import approach is cleaner; consider improving error handling.The refactoring to use
importlibis a good improvement for maintainability. However,importlib.import_module()raisesImportErroron failure rather than returningNone, so the assertionassert module is not Noneprovides no additional value.♻️ Suggested improvement with better error reporting
def test_view_imports(): """Ensure all view modules can be imported.""" view_names = [ "home_view", "analyzer_view", "helper_view", "winget_view", "intune_view", "intune_store_view", "history_view", "settings_view", "packaging_wizard_view", "detection_tester_view", "stack_manager_view", "dashboard_view", "library_view", ] for view_name in view_names: - module = importlib.import_module(f"switchcraft.gui_modern.views.{view_name}") - assert module is not None + try: + module = importlib.import_module(f"switchcraft.gui_modern.views.{view_name}") + except ImportError as e: + pytest.fail(f"Failed to import {view_name}: {e}")src/switchcraft/controllers/analysis_controller.py (2)
142-151: Unfinished merge logic leaves potential enhancements undocumented.The else branch contains comments about potential merge strategies but does nothing. Consider either:
- Implementing a merge/comparison strategy
- Removing the else block entirely
- Adding a TODO comment or logging that alternatives were found
♻️ Option: Log when community alternatives exist
if db_switches: if not info.install_switches: info.install_switches = db_switches community_match = True else: - # Merge? Or just flag that we found alternatives? - # For now, let's append if completely different? - # Simpler: If analyzer found nothing, use DB. - # If analyzer found something, maybe trust analyzer? - pass + # Analyzer found switches; log community alternatives for reference + logger.debug(f"Community DB has alternative switches: {db_switches}")
131-135: Consider caching CommunityDBService instance.
CommunityDBServiceis instantiated on every analysis call. If this involves file I/O (loading JSON), it could be cached as an instance variable in__init__or as a module-level singleton.♻️ Proposed refactor
class AnalysisController: """ Shared controller for handling the analysis workflow. Used by both Classic (Tkinter) and Modern (Flet) UIs. """ def __init__(self, ai_service=None): self.ai_service = ai_service + self._community_db = CommunityDBService()Then in
analyze_file:- db = CommunityDBService() + db = self._community_dbscripts/process_issue_ops.py (1)
63-64: Use actual timestamp and add contributor field.The
added_atfield uses a placeholder string. Additionally, the example entry inswitches.jsonincludes acontributorfield that this script doesn't populate.♻️ Proposed fix
+from datetime import datetime, timezone + def update_db(new_entry): # ... existing code ... # Add metadata - new_entry["added_at"] = "pending-merge" # In real action, could use current time + new_entry["added_at"] = datetime.now(timezone.utc).isoformat() + new_entry["contributor"] = os.environ.get("GITHUB_ACTOR", "unknown")In the workflow, ensure
GITHUB_ACTORis available (it's set by default in GitHub Actions).src/switchcraft/gui_modern/views/dashboard_view.py (1)
1-29: LGTM - Clean dashboard view initialization.The class structure and mock data setup are appropriate for a prototype dashboard. The layout composition with stats row and a two-column chart/activity section is well-organized.
Note:
loggeris imported (line 4) but not used anywhere in this file..github/ISSUE_TEMPLATE/add_switch.yml (1)
1-76: Issue template is well-structured.The template covers all necessary fields for switch submissions and aligns well with the automation workflow that processes these issues. The dropdown for installer types provides good coverage of common frameworks.
Minor observation: The
notestextarea (lines 72-76) is missing an explicitvalidationsblock. While it defaults torequired: false, adding it explicitly would maintain consistency with other fields.Optional: Add explicit validation for notes field
- type: textarea id: notes attributes: label: Additional Notes description: Any other details or caveats? + validations: + required: falsesrc/switchcraft/gui_modern/views/library_view.py (2)
103-106: Avoid bareexcept:clause.Using a bare
except:catches all exceptions includingKeyboardInterruptandSystemExit, which can hide bugs and make debugging difficult. Catch specific exceptions instead.Suggested fix
try: dt = datetime.fromisoformat(ts).strftime("%Y-%m-%d") - except: + except (ValueError, TypeError): dt = ts
119-124: Complex inline lambda with side effects is hard to read.The hover handler uses a chained
setattr(...) or e.control.update()pattern which relies onsetattrreturningNoneto executeupdate(). While functional, this is non-idiomatic and harder to maintain.Suggested refactor
- on_hover=lambda e: setattr(e.control, "bgcolor", ft.Colors.with_opacity(0.2, ft.Colors.WHITE) if e.data == "true" else ft.Colors.with_opacity(0.1, ft.Colors.WHITE)) or e.control.update() + on_hover=self._on_tile_hover ) + + def _on_tile_hover(self, e): + e.control.bgcolor = ( + ft.Colors.with_opacity(0.2, ft.Colors.WHITE) + if e.data == "true" + else ft.Colors.with_opacity(0.1, ft.Colors.WHITE) + ) + e.control.update()src/switchcraft/gui_modern/app.py (2)
92-93: Duplicate import oftimemodule.The
timemodule is already imported at line 10. This redundant import should be removed.Suggested fix
# Perform deferred UI building. # User requested visible loading screen. Since Flet execution here is linear during init, # we add a small sleep to ensure the "Loading..." ring is perceived. - import time time.sleep(1.5)
511-514: Lambda capturesdrawervariable correctly butclear_all()andclose()in list may have unintended behavior.The expression
lambda _: [self.notification_service.clear_all(), self.page.close(drawer)]uses a list to execute multiple statements. While this works, it's unconventional and the return value (a list) is discarded. Consider using a proper handler method.Suggested refactor
+ def clear_and_close(_): + self.notification_service.clear_all() + self.page.close(drawer) + drawer = ft.NavigationDrawer( controls=[ ft.Container(height=12), ft.Row([ ft.Text("Notifications", size=20, weight=ft.FontWeight.BOLD), - ft.TextButton("Clear All", on_click=lambda _: [self.notification_service.clear_all(), self.page.close(drawer)]) + ft.TextButton("Clear All", on_click=clear_and_close) ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, run_spacing=10),src/switchcraft/services/addon_service.py (1)
57-57: Bareexcept: passsilently swallows all errors.Lines 57 and 134 use bare
except: passwhich hides all exceptions including critical ones. This makes debugging difficult and can mask serious issues.Suggested fix for line 57
try: with open(d / "manifest.json") as f: data = json.load(f) if data.get("id") == addon_id: addon_path = d manifest_data = data break - except: pass + except (OSError, json.JSONDecodeError) as e: + logger.warning(f"Skipping invalid addon at {d}: {e}")Also applies to: 134-134
src/switchcraft/gui_modern/views/packaging_wizard_view.py (1)
270-271: Add timeout to prevent indefinite hangs.
requests.get()has no timeout, which can block the thread indefinitely if the server doesn't respond.♻️ Proposed fix
- with requests.get(url, stream=True) as r: + with requests.get(url, stream=True, timeout=60) as r:src/switchcraft/services/intune_service.py (3)
404-433: Make script base64 encoding robust tobytesinput + validaterun_as_accountvalues.
If UI passes bytes (or already-base64),.encode('utf-8')will throw / double-encode.Proposed hardening
- # Script content must be base64 encoded - encoded_script = base64.b64encode(script_content.encode('utf-8')).decode('utf-8') + # Script content must be base64 encoded (Graph expects base64 string) + if isinstance(script_content, bytes): + raw = script_content + else: + raw = str(script_content).encode("utf-8") + encoded_script = base64.b64encode(raw).decode("ascii") + + if run_as_account not in {"system", "user"}: + raise ValueError("run_as_account must be 'system' or 'user'")
434-466: Same base64 robustness needed for remediation scripts; avoidraise e.
Preferraiseto preserve original traceback context.Proposed tweak
- enc_detection = base64.b64encode(detection_content.encode('utf-8')).decode('utf-8') - enc_remediation = base64.b64encode(remediation_content.encode('utf-8')).decode('utf-8') + det_raw = detection_content if isinstance(detection_content, bytes) else str(detection_content).encode("utf-8") + rem_raw = remediation_content if isinstance(remediation_content, bytes) else str(remediation_content).encode("utf-8") + enc_detection = base64.b64encode(det_raw).decode("ascii") + enc_remediation = base64.b64encode(rem_raw).decode("ascii") @@ - except Exception as e: + except Exception as e: # Check for license errors common with Remediations logger.error(f"Failed to upload Remediation: {e}") - raise e + raise
649-662:list_groups: build filters viaparamsand handle pagination.
String-concatenating$filteris brittle (escaping) and Graph often paginates with@odata.nextLink.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
.github/ISSUE_TEMPLATE/add_switch.yml.github/workflows/process_switch_issue.ymlREADME.mddocs/FEATURES.mdscripts/process_issue_ops.pysrc/switchcraft/analyzers/msi.pysrc/switchcraft/controllers/analysis_controller.pysrc/switchcraft/data/community/switches.jsonsrc/switchcraft/gui_modern/app.pysrc/switchcraft/gui_modern/controls/skeleton.pysrc/switchcraft/gui_modern/views/__init__.pysrc/switchcraft/gui_modern/views/addon_manager_view.pysrc/switchcraft/gui_modern/views/dashboard_view.pysrc/switchcraft/gui_modern/views/detection_tester_view.pysrc/switchcraft/gui_modern/views/group_manager_view.pysrc/switchcraft/gui_modern/views/home_view.pysrc/switchcraft/gui_modern/views/library_view.pysrc/switchcraft/gui_modern/views/macos_wizard_view.pysrc/switchcraft/gui_modern/views/packaging_wizard_view.pysrc/switchcraft/gui_modern/views/script_upload_view.pysrc/switchcraft/gui_modern/views/stack_manager_view.pysrc/switchcraft/models.pysrc/switchcraft/services/addon_service.pysrc/switchcraft/services/community_db_service.pysrc/switchcraft/services/intune_service.pysrc/switchcraft/services/notification_service.pytests/test_modern_gui.py
🧰 Additional context used
🧬 Code graph analysis (9)
src/switchcraft/gui_modern/views/detection_tester_view.py (3)
tests/test_modern_gui.py (1)
update(32-33)src/switchcraft/gui_modern/views/addon_manager_view.py (1)
delete(126-137)src/switchcraft/gui_modern/views/group_manager_view.py (1)
delete(191-202)
src/switchcraft/gui_modern/views/home_view.py (2)
src/switchcraft/gui_modern/views/macos_wizard_view.py (1)
_build_content(28-82)src/switchcraft/utils/i18n.py (1)
get(143-170)
src/switchcraft/gui_modern/views/dashboard_view.py (1)
src/switchcraft/debug_views.py (2)
height(30-30)width(28-28)
src/switchcraft/gui_modern/views/addon_manager_view.py (2)
src/switchcraft/services/addon_service.py (3)
list_addons(18-37)install_addon(83-122)delete_addon(124-135)src/switchcraft/gui_modern/utils/file_picker_helper.py (2)
FilePickerHelper(5-85)pick_file(12-38)
src/switchcraft/gui_modern/views/library_view.py (1)
src/switchcraft/services/history_service.py (3)
HistoryService(9-63)get_history(22-35)clear(55-56)
src/switchcraft/controllers/analysis_controller.py (2)
src/switchcraft/services/community_db_service.py (3)
CommunityDBService(9-61)get_switches_by_hash(39-45)get_switches_by_name(47-54)src/switchcraft/models.py (1)
InstallerInfo(5-35)
src/switchcraft/gui_modern/views/macos_wizard_view.py (2)
src/switchcraft/services/intune_service.py (3)
IntuneService(16-700)authenticate(147-164)upload_macos_shell_script(467-498)src/switchcraft/utils/config.py (2)
get_value(31-88)get_secure_value(219-273)
src/switchcraft/services/intune_service.py (1)
src/switchcraft/gui_modern/views/group_manager_view.py (1)
delete(191-202)
src/switchcraft/gui_modern/views/__init__.py (12)
src/switchcraft/gui_modern/views/home_view.py (1)
ModernHomeView(4-84)src/switchcraft/gui_modern/views/analyzer_view.py (1)
ModernAnalyzerView(21-693)src/switchcraft/gui_modern/views/helper_view.py (1)
ModernHelperView(9-112)src/switchcraft/gui_modern/views/winget_view.py (1)
ModernWingetView(12-616)src/switchcraft/gui_modern/views/intune_view.py (1)
ModernIntuneView(10-180)src/switchcraft/gui_modern/views/intune_store_view.py (1)
ModernIntuneStoreView(9-188)src/switchcraft/gui_modern/views/history_view.py (1)
ModernHistoryView(7-95)src/switchcraft/gui_modern/views/settings_view.py (1)
ModernSettingsView(14-931)src/switchcraft/gui_modern/views/packaging_wizard_view.py (1)
PackagingWizardView(16-860)src/switchcraft/gui_modern/views/detection_tester_view.py (1)
DetectionTesterView(12-313)src/switchcraft/gui_modern/views/dashboard_view.py (1)
DashboardView(7-115)src/switchcraft/gui_modern/views/library_view.py (1)
LibraryView(8-124)
🪛 actionlint (1.7.10)
.github/workflows/process_switch_issue.yml
26-26: "github.event.issue.body" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details
(expression)
🔇 Additional comments (15)
README.md (3)
37-42: Excellent feature documentation!The new feature descriptions are clear, concise, and well-integrated into the existing structure. Each feature has an actionable description that helps users understand its value.
75-101: Well-organized release documentation!The reorganized "Release Artifacts & Variants" section with the Editions Overview and File Guide tables significantly improves clarity. The tables are well-formatted, the information is comprehensive, and the pros/cons format helps users make informed decisions about which variant to download.
71-71: The automation system described in the README is already implemented and functional. Theprocess_switch_issue.ymlworkflow automatically:
- Triggers when issues are labeled with "automation"
- Validates required fields (application name, version, silent switch)
- Checks for duplicate entries
- Creates a pull request via the peter-evans/create-pull-request action
No action needed—the claim in the README is accurate.
Likely an incorrect or invalid review comment.
docs/FEATURES.md (1)
11-14: LGTM!The new feature entries are well-documented and consistent with the existing format. They accurately reflect the new views and services introduced in this PR.
src/switchcraft/analyzers/msi.py (1)
43-43: LGTM!The assignment properly captures the MSI ProductCode into the new
product_codefield onInstallerInfo, enabling downstream usage for uninstall string construction.src/switchcraft/gui_modern/controls/skeleton.py (1)
20-24: LGTM on lifecycle management.The
did_mountandwill_unmounthooks properly manage the animation lifecycle using theabortedflag to signal thread termination.src/switchcraft/gui_modern/views/detection_tester_view.py (1)
12-85: Well-structured view initialization.The UI layout is clean with appropriate field visibility management and consistent patterns for the detection type selector. The component follows the established view patterns in the codebase.
.github/workflows/process_switch_issue.yml (1)
28-48: Workflow structure looks good.The workflow correctly:
- Filters on the 'automation' label
- Sets appropriate permissions for contents, PRs, and issues
- Uses current action versions (v4, v5, v6)
- Configures PR to close the source issue on merge
Note: The workflow relies on the Python script exiting with non-zero on failure, which it does correctly via
sys.exit(1).src/switchcraft/controllers/analysis_controller.py (1)
186-193: LGTM!The
community_matchflag is correctly integrated into theAnalysisResultreturn, enabling downstream code to identify when switches came from the community database.scripts/process_issue_ops.py (1)
81-97: LGTM on the main entry point.The main block correctly handles argument parsing, file reading, and error cases with appropriate exit codes.
src/switchcraft/gui_modern/views/dashboard_view.py (2)
31-52: Stat cards look good.The stats row and card helper methods are clean and reusable. The use of parameterized
_stat_cardavoids code duplication.
54-115: Chart and activity sections are well-structured.The mock BarChart configuration and recent activity ListView are appropriate for a dashboard prototype. The layout uses proper Flet patterns.
src/switchcraft/gui_modern/views/__init__.py (1)
1-13: LGTM!The re-exports correctly expose all new view classes. Minor observation: newer views (
PackagingWizardView,DetectionTesterView, etc.) lack theModernprefix used by older views—consider aligning naming for consistency if this is a concern.src/switchcraft/services/intune_service.py (2)
6-6:base64import is fine; ensure encoding is ASCII-safe and input-type tolerant in callers.
Right now the new methods assumescript_contentisstr; consider acceptingbytestoo (see later comments).
467-499: Minor code style improvement: use bareraiseinstead ofraise e.The hardcoded
executionFrequencyandretryCountfields are required by the Microsoft Graph API for deviceShellScript creation and cannot be safely omitted. Removing them would cause API rejection, not prevent it. If parametrization is desired for future flexibility, add these as optional function parameters with sensible defaults rather than removing them.The comment about
"PT15M"should clarify it's a valid ISO 8601 duration (15 minutes).Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.