Compare commits

..

16 commits

Author SHA1 Message Date
dffc925bb6 refactor: Change logging level from info to debug for download and JS messages
Some checks failed
Tests & Quality Checks / Test on Python 3.11 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.12 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.11-1 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.12-1 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.10 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.11-2 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.12-2 (push) Has been cancelled
Tests & Quality Checks / Build Artifacts (push) Has been cancelled
Tests & Quality Checks / Build Artifacts-1 (push) Has been cancelled
2026-02-18 13:19:38 +01:00
6d8af7f8fb fix: Improve checkout data handling in response processing 2026-02-18 13:14:17 +01:00
de98623ee0 Add application icon in PNG format to resources/icons directory 2026-02-18 13:07:01 +01:00
a4d735d759 docs: Update copilot instructions to reflect Phase 4 completion and Phase 5 planning 2026-02-18 12:31:52 +01:00
4011f46ab7 fix: Add error handling for script injection and improve logging for URL mappings 2026-02-18 10:28:15 +01:00
0eba82b8af feat: Enhance drag interception with dynamic URL pattern matching configuration injection 2026-02-18 10:15:42 +01:00
f385ee6410 chore: Update repository URLs and author information in README and pyproject.toml 2026-02-18 09:56:03 +01:00
313213e4cd docs: Update QUICKSTART and START_HERE documentation to reflect Phase 4 completion and Phase 5 readiness 2026-02-18 09:46:09 +01:00
9915c7b32c chore: Update version to 0.5.0 and document Phase 4 completion in changelog and development plan 2026-02-18 09:41:21 +01:00
ae5c86814f Remove obsolete WebDrop Bridge scripts: bridge_script.js, bridge_script_debug.js, bridge_script_drop_intercept.js, bridge_script_hybrid.js, bridge_script_v2.js 2026-02-18 09:29:33 +01:00
fb710d5b00 Remove obsolete documentation and test files related to debugging, update feature fixes, versioning, and web app loading issues. Consolidate versioning process to a single source of truth in __init__.py. Enhance web app loading with improved path resolution and a professional fallback UI. Implement timeout handling in update features to prevent application hangs, ensuring user-friendly error messages and background processing. 2026-02-18 09:27:28 +01:00
c32453018b feat: Improve dragstart handler installation with retry logic for Angular handlers 2026-02-18 08:06:11 +01:00
e91a2445f3 feat: Implement asynchronous checkout status check with improved error handling and logging 2026-02-18 08:01:33 +01:00
2a9926d934 feat: Enhance checkout process by checking asset status before prompting user 2026-02-18 07:41:28 +01:00
dee02ad600 Add drag & drop script variants and enhanced debugging tools
- Introduced multiple JavaScript scripts for handling drag & drop functionality:
  - `bridge_script.js`: Original implementation with popup prevention.
  - `bridge_script_debug.js`: Debug version with extensive logging for troubleshooting.
  - `bridge_script_v2.js`: Enhanced version extending DataTransfer for better integration.
  - `bridge_script_hybrid.js`: Hybrid approach allowing parallel native file drag.
  - `bridge_script_drop_intercept.js`: Intercepts drop events for custom handling.
  - `bridge_script_intercept.js`: Prevents browser drag for ALT+drag, using Qt for file drag.

- Added detailed documentation in `SCRIPT_VARIANTS.md` outlining usage, status, and recommended workflows for each script.
- Implemented logging features to capture drag events, DataTransfer modifications, and network requests for better debugging.
- Enhanced DataTransfer handling to support Windows-specific file formats and improve user experience during drag & drop operations.
2026-02-17 19:19:14 +01:00
88dc358894 Refactor drag handling and update tests
- Renamed `initiate_drag` to `handle_drag` in MainWindow and updated related tests.
- Improved drag handling logic to utilize a bridge for starting file drags.
- Updated `_on_drag_started` and `_on_drag_failed` methods to match new signatures.
- Modified test cases to reflect changes in drag handling and assertions.

Enhance path validation and logging

- Updated `PathValidator` to log warnings for nonexistent roots instead of raising errors.
- Adjusted tests to verify the new behavior of skipping nonexistent roots.

Update web application UI and functionality

- Changed displayed text for drag items to reflect local paths and Azure Blob Storage URLs.
- Added debug logging for drag operations in the web application.
- Improved instructions for testing drag and drop functionality.

Add configuration documentation and example files

- Created `CONFIG_README.md` to provide detailed configuration instructions for WebDrop Bridge.
- Added `config.example.json` and `config_test.json` for reference and testing purposes.

Implement URL conversion logic

- Introduced `URLConverter` class to handle conversion of Azure Blob Storage URLs to local paths.
- Added unit tests for URL conversion to ensure correct functionality.

Develop download interceptor script

- Created `download_interceptor.js` to intercept download-related actions in the web application.
- Implemented logging for fetch calls, XMLHttpRequests, and Blob URL creations.

Add download test page and related tests

- Created `test_download.html` for testing various download scenarios.
- Implemented `test_download.py` to verify download path resolution and file construction.
- Added `test_url_mappings.py` to ensure URL mappings are loaded correctly.

Add unit tests for URL converter

- Created `test_url_converter.py` to validate URL conversion logic and mapping behavior.
2026-02-17 15:56:53 +01:00
48 changed files with 3084 additions and 4401 deletions

View file

@ -2,39 +2,57 @@
## Project Context
WebDrop Bridge is a professional Qt-based desktop application that converts web-based drag-and-drop text paths into native file operations. It's designed for production deployment on Windows and macOS with professional-grade testing, documentation, and CI/CD.
WebDrop Bridge is a professional Qt-based desktop application (v0.5.0) that converts web-based drag-and-drop text paths into native file operations. It's designed for production deployment on Windows and macOS with professional-grade testing, documentation, and CI/CD.
**Current Status**: Phase 4 Complete - Phase 5 (Release Candidates) Planned as of Feb 18, 2026
## Architecture Overview
- **Framework**: PySide6 (Qt bindings for Python)
- **Python**: 3.9+ (tested on 3.10, 3.11, 3.12, 3.13, 3.14)
- **Structure**: Modular (core/, ui/, utils/)
- **Testing**: pytest with unit, integration, and fixture-based tests
- **Testing**: pytest with unit and integration tests
- **Distribution**: PyInstaller → MSI (Windows), DMG (macOS)
- **Web Integration**: QWebEngineView with security-hardened JavaScript bridge
## Key Files & Their Purpose
| File | Purpose |
|------|---------|
| `src/webdrop_bridge/main.py` | Application entry point |
| `src/webdrop_bridge/config.py` | Configuration management |
| `src/webdrop_bridge/core/validator.py` | Path validation and security |
| `src/webdrop_bridge/core/drag_interceptor.py` | Drag-and-drop handling |
| `src/webdrop_bridge/core/updater.py` | Update check and release management |
| `src/webdrop_bridge/ui/main_window.py` | Main Qt window |
| `tests/` | Pytest-based test suite |
| `pyproject.toml` | Modern Python packaging |
| `tox.ini` | Test automation config |
| `src/webdrop_bridge/__init__.py` | Package info, version (0.5.0) |
| `src/webdrop_bridge/main.py` | Application entry point, config loading |
| `src/webdrop_bridge/config.py` | Configuration management (file/env), URL mappings, validation |
| `src/webdrop_bridge/core/validator.py` | Path validation against whitelist, security checks |
| `src/webdrop_bridge/core/drag_interceptor.py` | Drag-and-drop event handling |
| `src/webdrop_bridge/core/config_manager.py` | File-based config loading and caching |
| `src/webdrop_bridge/core/url_converter.py` | Azure blob URL → local path conversion |
| `src/webdrop_bridge/core/updater.py` | Update checking via Forgejo API, release management |
| `src/webdrop_bridge/ui/main_window.py` | Main Qt window, config injection, menu bar |
| `src/webdrop_bridge/ui/restricted_web_view.py` | Hardened QWebEngineView with security policies |
| `src/webdrop_bridge/ui/settings_dialog.py` | Settings UI, URL mapping configuration |
| `src/webdrop_bridge/ui/update_manager_ui.py` | Update check UI and dialogs |
| `src/webdrop_bridge/utils/logging.py` | Logging configuration (console + file) |
| `tests/` | pytest-based test suite (unit/ and integration/) |
| `pyproject.toml` | Modern Python packaging and tool config |
| `tox.ini` | Test automation (pytest, lint, type, format) |
## Code Standards
### Python Style
- **Formatter**: Black (100 character line length)
- **Linter**: Ruff
- **Type Hints**: Required for all public APIs
- **Docstrings**: Google-style format
- **Formatter**: Black (88 character line length)
- **Import Sorter**: isort (black-compatible profile)
- **Linter**: Ruff (checks style, security, complexity)
- **Type Checker**: mypy (strict mode for core modules)
- **Type Hints**: Required for all public APIs and core modules
- **Docstrings**: Google-style format (module, class, function level)
### Example
```python
"""Module for path validation."""
from pathlib import Path
from typing import List
def validate_path(path: Path, allowed_roots: List[Path]) -> bool:
"""Validate path against allowed roots.
@ -50,26 +68,35 @@ def validate_path(path: Path, allowed_roots: List[Path]) -> bool:
## Before Making Changes
1. **Check the development plan**: See `DEVELOPMENT_PLAN.md` for current phase and priorities
2. **Understand the architecture**: Read `docs/ARCHITECTURE.md`
3. **Follow the structure**: Keep code organized in appropriate modules (core/, ui/, utils/)
4. **Write tests first**: Use TDD approach - write tests before implementing
1. **Check the development plan**: See [DEVELOPMENT_PLAN.md](../../DEVELOPMENT_PLAN.md) - currently Phase 4 Complete, Phase 5 in planning
2. **Understand the architecture**: Read [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md)
3. **Review actual implementation**: Look at existing modules in core/, ui/, utils/
4. **Follow the structure**: Keep code organized in appropriate modules
5. **Write tests**: Use pytest - write tests for new functionality
## Making Changes
1. **Run existing tests first**: `pytest tests -v`
2. **Create test file**: `tests/unit/test_*.py`
3. **Write failing test**: Verify it fails before implementing
4. **Implement feature**: Follow code standards above
5. **Run tests**: `pytest tests -v --cov`
6. **Run quality checks**: `tox -e lint,type`
7. **Update docs**: Add docstrings and update README if needed
1. **Run existing tests first**: `pytest tests -v` (should pass)
2. **Create test file**: `tests/unit/test_*.py` or `tests/integration/test_*.py`
3. **Write test**: Verify test executes (may fail if feature incomplete)
4. **Implement feature**: Follow code standards (black, ruff, isort, mypy)
5. **Format code**: `tox -e format` (auto-formats with black/isort)
6. **Run all checks**: `tox -e lint,type` (ruff, mypy validation)
7. **Run tests with coverage**: `pytest tests --cov=src/webdrop_bridge`
8. **Update docs**: Add/update docstrings, README if needed
## Development Environment
**Virtual Environment**: `.venv` (already created)
- Activate: `.venv\Scripts\activate` (Windows) or `source .venv/bin/activate` (macOS/Linux)
- All Python commands automatically use this environment through VS Code integration
- **Note**: Only activate if running commands outside VS Code terminal
**Required**:
- Python 3.9+ (tested on 3.10, 3.11, 3.12, 3.13, 3.14)
- PySide6 (for Qt GUI)
- pytest (for testing)
- tox (for automated testing and quality checks)
## Common Commands
@ -77,19 +104,25 @@ def validate_path(path: Path, allowed_roots: List[Path]) -> bool:
# Setup (one-time)
pip install -r requirements-dev.txt
# Testing (uses .venv automatically)
pytest tests -v
pytest tests --cov=src/webdrop_bridge --cov-report=html
# Testing
pytest tests -v # Run all tests
pytest tests --cov=src/webdrop_bridge # With coverage
pytest tests::test_module -v # Specific test
pytest -k test_validator # By name pattern
# Quality checks
tox -e lint # Ruff + Black checks
# Quality checks (these use tox environments)
tox -e lint # Ruff + Black check + isort check
tox -e format # Auto-format (Black + isort)
tox -e type # mypy type checking
tox -e format # Auto-format code
tox # All checks
tox -e coverage # Tests with coverage report
tox # Run everything
# Building
python build/scripts/build_windows.py # Windows
bash build/scripts/build_macos.sh # macOS
# Building distributions
python build/scripts/build_windows.py # Windows (requires pyinstaller, wix)
bash build/scripts/build_macos.sh # macOS (requires pyinstaller, notarization key)
# Running application
python -m webdrop_bridge.main # Start application
```
## Important Decisions
@ -120,10 +153,17 @@ bash build/scripts/build_macos.sh # macOS
# Unit tests: Isolated component testing
tests/unit/test_validator.py
tests/unit/test_drag_interceptor.py
tests/unit/test_url_converter.py
tests/unit/test_config.py
tests/unit/test_config_manager.py
tests/unit/test_logging.py
tests/unit/test_updater.py
tests/unit/test_main_window.py
tests/unit/test_restricted_web_view.py
tests/unit/test_settings_dialog.py
tests/unit/test_update_manager_ui.py
# Integration tests: Component interaction and update flow
tests/integration/test_drag_workflow.py
tests/integration/test_end_to_end.py
tests/integration/test_update_flow.py
# Fixtures: Reusable test data
@ -132,20 +172,27 @@ tests/fixtures/
```
Target: 80%+ code coverage
- Use `pytest --cov=src/webdrop_bridge --cov-report=html` to generate coverage reports
- Review htmlcov/index.html for detailed coverage breakdown
## Performance Considerations
- Drag event handling: < 50ms total
- Application startup: < 1 second
- Memory baseline: < 200MB
- Logging overhead: minimize file I/O in drag operations
## Documentation Requirements
- **Public APIs**: Docstrings required
- **Public APIs**: Docstrings required (Google-style format)
- **Modules**: Add docstring at top of file
- **Features**: Update README.md and docs/
- **Integration tests**: Reference and document in README.md and docs/ARCHITECTURE.md
- **Breaking changes**: Update DEVELOPMENT_PLAN.md
- **Classes**: Document purpose, attributes, and usage in docstring
- **Functions**: Document args, returns, raises, and examples
- **Features**: Update [DEVELOPMENT_PLAN.md](../../DEVELOPMENT_PLAN.md) milestones
- **Architecture changes**: Update [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md)
- **Config changes**: Update [CONFIG_README.md](../../CONFIG_README.md)
- **Breaking changes**: Update CHANGELOG.md and DEVELOPMENT_PLAN.md
- **Code examples**: Preferred format is in docstrings with >>> syntax
## Git Workflow
@ -165,38 +212,48 @@ git push origin feature/my-feature
## Review Checklist
- [ ] Tests pass (100% on CI)
- [ ] Code follows black/ruff standards
- [ ] Type hints added for public APIs
- [ ] Documentation updated
- [ ] No security concerns
- [ ] Cross-platform compatibility verified (if applicable)
- [ ] Tests pass (100% on local runs, `pytest tests -v`)
- [ ] Code formatted with black/isort (`tox -e format`)
- [ ] All linting passes (`tox -e lint`)
- [ ] Type hints complete (`tox -e type` passes)
- [ ] Docstrings added for all public APIs
- [ ] No security concerns (especially in path validation)
- [ ] Cross-platform compatibility verified (Windows + macOS tests if applicable)
- [ ] Configuration handling tested for edge cases
- [ ] Git history clean (meaningful commits with proper messages)
## When You're Stuck
1. **Check DEVELOPMENT_PLAN.md**: Current phase and architecture decisions
2. **Look at tests**: Existing tests show expected behavior
3. **Read docstrings**: Functions document their contracts
4. **Check docs/ARCHITECTURE.md**: Design patterns and data flow
1. **Check DEVELOPMENT_PLAN.md**: Current phase (Phase 4 Complete) and architecture decisions
2. **Look at tests**: Existing tests in `tests/unit/` and `tests/integration/` show expected behavior
3. **Read docstrings**: Functions document their contracts using Google-style format
4. **Check docs/ARCHITECTURE.md**: Design patterns, data flow, and module organization
5. **Review config examples**: See [CONFIG_README.md](../../CONFIG_README.md) and `config.example.json`
6. **Check CI output**: Look at tox and pytest output for detailed error messages
## What NOT to Do
❌ Change architecture without discussion
❌ Add dependencies without updating pyproject.toml
❌ Merge without tests passing
❌ Remove type hints or docstrings
❌ Commit without running `tox -e lint,type`
❌ Add platform-specific code without tests
❌ Change architecture without reviewing DEVELOPMENT_PLAN.md first
❌ Add dependencies without updating requirements-dev.txt and pyproject.toml
❌ Commit without running `tox -e format,lint,type`
❌ Remove type hints or docstrings from public APIs
❌ Add imports without running `tox -e format` (isort cleanup)
❌ Add platform-specific code without tests marked with @pytest.mark.windows or @pytest.mark.macos
❌ Modify path validation logic without security review
❌ Force-push to main or release branches
## Notes for Modifications
- This is a production-quality application, not a PoC
- Code quality and testing are non-negotiable
- Cross-platform support (Windows + macOS) is required
- User security (path validation) is critical
- Documentation must keep pace with code
- Code quality, testing, and documentation are non-negotiable
- Cross-platform support (Windows + macOS) is required and tested
- User security (path validation) is critical - be extra careful with path operations
- Configuration must support both .env files and JSON files
- All error messages should be meaningful and logged appropriately
- Documentation must keep pace with code changes
---
**Current Status**: Pre-release development (Phase 1-2)
**Last Updated**: January 2026
**Current Status**: Phase 4 Complete (Jan 29, 2026) - Phase 5 (Release Candidates) Planned
**Version**: 0.5.0
**Last Updated**: February 18, 2026

View file

@ -1,489 +0,0 @@
╔════════════════════════════════════════════════════════════════════════════╗
║ ║
║ 🎉 WEBDROP BRIDGE - PROJECT SETUP COMPLETE 🎉 ║
║ ║
║ Professional Edition Created Successfully ║
║ ║
╚════════════════════════════════════════════════════════════════════════════╝
DATE: January 28, 2026
STATUS: ✅ READY FOR DEVELOPMENT
LOCATION: c:\Development\VS Code Projects\webdrop_bridge
═══════════════════════════════════════════════════════════════════════════════
📊 PROJECT STATISTICS
═════════════════════════════════════════════════════════════════════════════
Total Files Created: 38 files
Project Structure: ✅ Complete (src, tests, build, docs, resources)
Documentation: ✅ Complete (4100+ lines across 9 markdown files)
Configuration Files: ✅ Complete (8 config files)
Build Automation: ✅ Complete (Windows MSI + macOS DMG)
CI/CD Pipeline: ✅ Complete (GitHub Actions)
Code Quality Tools: ✅ Configured (Black, Ruff, mypy, pytest, tox)
VS Code Integration: ✅ Complete (settings, launch, tasks)
Test Framework: ✅ Ready (pytest + fixtures)
═══════════════════════════════════════════════════════════════════════════════
📁 WHAT WAS CREATED
═════════════════════════════════════════════════════════════════════════════
DOCUMENTATION (9 files, 4100+ lines):
✅ START_HERE.md (Entry point for new users)
✅ QUICKSTART.md (5-minute setup guide)
✅ README.md (Project overview)
✅ DEVELOPMENT_PLAN.md (12-week detailed roadmap - 1200+ lines)
✅ IMPLEMENTATION_CHECKLIST.md (Phase 1 implementation tasks)
✅ FILE_LISTING.md (Complete file manifest)
✅ PROJECT_SETUP_SUMMARY.md (Setup summary)
✅ CONTRIBUTING.md (Contribution guidelines)
✅ docs/ARCHITECTURE.md (Technical architecture)
CONFIGURATION (8 files):
✅ pyproject.toml (Modern Python packaging - PEP 517/518)
✅ setup.py (Backwards compatibility)
✅ pytest.ini (Test configuration)
✅ tox.ini (Test automation - 6 environments)
✅ requirements.txt (Production dependencies)
✅ requirements-dev.txt (Development dependencies)
✅ .env.example (Environment template)
✅ .gitignore (Git ignore rules)
SOURCE CODE (8 files - Ready for Phase 1):
✅ src/webdrop_bridge/__init__.py
✅ src/webdrop_bridge/core/__init__.py
✅ src/webdrop_bridge/ui/__init__.py
✅ src/webdrop_bridge/utils/__init__.py
✅ Plus templates & specifications for Phase 1 implementation
TESTS (5 files - Framework Ready):
✅ tests/__init__.py
✅ tests/conftest.py (Pytest fixtures)
✅ tests/unit/__init__.py
✅ tests/integration/__init__.py
✅ tests/unit/test_project_structure.py (Initial validation tests)
BUILD & AUTOMATION (4 files):
✅ .github/workflows/tests.yml (GitHub Actions CI/CD pipeline)
✅ build/scripts/build_windows.py (Windows MSI builder)
✅ build/scripts/build_macos.sh (macOS DMG builder)
✅ Makefile (10+ convenience commands)
VS CODE INTEGRATION (4 files):
✅ .vscode/settings.json (Editor & Python config)
✅ .vscode/launch.json (Debug configurations)
✅ .vscode/tasks.json (Build & test tasks)
✅ webdrop_bridge.code-workspace (Workspace file)
RESOURCES (2+ directories):
✅ webapp/index.html (Beautiful drag-drop test app)
✅ resources/icons/ (Icons directory - ready for assets)
✅ resources/stylesheets/ (Stylesheets directory)
LICENSE:
✅ LICENSE (MIT License)
═══════════════════════════════════════════════════════════════════════════════
🚀 GETTING STARTED (5 MINUTES)
═════════════════════════════════════════════════════════════════════════════
1. OPEN PROJECT IN VS CODE:
code "c:\Development\VS Code Projects\webdrop_bridge\webdrop_bridge.code-workspace"
2. CREATE VIRTUAL ENVIRONMENT:
python -m venv venv
venv\Scripts\activate
3. INSTALL DEPENDENCIES:
pip install -r requirements-dev.txt
4. VERIFY SETUP:
pytest tests/unit/test_project_structure.py -v
5. READ DOCUMENTATION:
- START_HERE.md (Quick overview - 5 min)
- QUICKSTART.md (Setup guide - 5 min)
- DEVELOPMENT_PLAN.md (Detailed roadmap - 20 min)
═══════════════════════════════════════════════════════════════════════════════
📚 DOCUMENTATION ROADMAP
═════════════════════════════════════════════════════════════════════════════
Read in this order:
1. START_HERE.md ← You are here! Quick overview
(5 minutes)
2. QUICKSTART.md ← 5-minute setup guide
(5 minutes)
3. README.md ← Full project overview
(10 minutes)
4. DEVELOPMENT_PLAN.md ← 12-week roadmap with detailed specs
(20 minutes)
5. docs/ARCHITECTURE.md ← Technical deep-dive
(15 minutes)
6. CONTRIBUTING.md ← Code standards & guidelines
(10 minutes)
7. IMPLEMENTATION_CHECKLIST.md ← Phase 1 implementation tasks
(Reference)
Total Reading Time: ~60-90 minutes to fully understand the project
═══════════════════════════════════════════════════════════════════════════════
🎯 12-WEEK DEVELOPMENT ROADMAP
═════════════════════════════════════════════════════════════════════════════
PHASE 1: Foundation (Weeks 1-4) ← NEXT
✅ Architecture designed
✅ Configuration system spec documented
✅ Path validator spec documented
✅ Drag interceptor spec documented
✅ Main window spec documented
→ Start implementing these components
PHASE 2: Testing & Quality (Weeks 5-6)
→ Unit tests (80%+ coverage)
→ Integration tests
→ Code quality enforcement
→ Security audit
PHASE 3: Build & Distribution (Weeks 7-8)
→ Windows MSI installer
→ macOS DMG package
→ Installer testing
PHASE 4: Professional Features (Weeks 9-12)
→ Enhanced logging
→ Advanced configuration
→ User documentation
→ Release packaging
PHASE 5: Post-Release (Months 2-3)
→ Auto-update system
→ Analytics & monitoring
→ Community support
═══════════════════════════════════════════════════════════════════════════════
⚡ QUICK COMMANDS
═════════════════════════════════════════════════════════════════════════════
# Setup
make install-dev
# Testing
make test # All tests with coverage
make test-quick # Fast test run
make test-unit # Unit tests only
# Code Quality
make lint # Check style (ruff, black)
make format # Auto-fix formatting
make type # Type checking (mypy)
make quality # All checks
# Building
make build-windows # Build Windows MSI
make build-macos # Build macOS DMG
make clean # Clean build artifacts
# Help
make help # List all commands
═══════════════════════════════════════════════════════════════════════════════
✨ KEY FEATURES
═════════════════════════════════════════════════════════════════════════════
✅ Professional Architecture
- Modular design (core/, ui/, utils/)
- Clear separation of concerns
- Extensible framework
✅ Comprehensive Documentation
- 4100+ lines of documentation
- 12-week detailed roadmap
- Architecture guide
- Contributing guidelines
- Implementation checklist
✅ Production-Grade Build System
- PyInstaller Windows MSI builder
- PyInstaller macOS DMG builder
- Automated builds
- Version management
✅ Automated Testing
- GitHub Actions CI/CD
- Cross-platform testing (Windows, macOS, Linux)
- Multiple Python versions (3.10, 3.11, 3.12)
- Automated artifact generation
✅ Code Quality
- Black formatter (auto-formatting)
- Ruff linter (style checking)
- mypy type checker (type safety)
- pytest test framework
- Coverage reporting (target 80%+)
- tox test automation
✅ Cross-Platform Support
- Windows 10/11 (x64)
- macOS 12-14 (Intel & ARM64)
- Linux (experimental)
✅ Developer Experience
- VS Code integration (settings, tasks, debug)
- Makefile with common commands
- Pre-configured workflows
- Beautiful test webapp included
═══════════════════════════════════════════════════════════════════════════════
📋 NEXT STEPS
═════════════════════════════════════════════════════════════════════════════
1. ✅ IMMEDIATE (Today)
→ Read START_HERE.md (this file)
→ Read QUICKSTART.md (5 minutes)
→ Setup virtual environment
→ Verify structure with pytest
2. NEAR TERM (This Week)
→ Read DEVELOPMENT_PLAN.md Phase 1
→ Read docs/ARCHITECTURE.md
→ Review code standards in CONTRIBUTING.md
→ Begin Phase 1 implementation
3. PHASE 1 IMPLEMENTATION (Weeks 1-4)
→ Implement config system
→ Implement path validator
→ Implement drag interceptor
→ Implement UI components
→ Write tests as you go
4. PHASE 2 (Weeks 5-6)
→ Complete test suite
→ Achieve 80%+ coverage
→ Run quality checks
→ Security audit
5. PHASE 3+ (Weeks 7+)
→ Build installers
→ Advanced features
→ Release preparation
═══════════════════════════════════════════════════════════════════════════════
🔍 PROJECT STRUCTURE
═════════════════════════════════════════════════════════════════════════════
webdrop-bridge/
├── 📂 src/webdrop_bridge/ ← Main application code
│ ├── core/ ← Business logic (validator, interceptor)
│ ├── ui/ ← Qt/PySide6 UI components
│ └── utils/ ← Shared utilities (logging, helpers)
├── 📂 tests/ ← Comprehensive test suite
│ ├── unit/ ← Unit tests
│ ├── integration/ ← Integration tests
│ └── fixtures/ ← Test data & mocks
├── 📂 build/ ← Build automation
│ ├── windows/ ← Windows-specific config
│ ├── macos/ ← macOS-specific config
│ └── scripts/ ← PyInstaller build scripts
├── 📂 docs/ ← Technical documentation
│ └── ARCHITECTURE.md ← Architecture guide
├── 📂 webapp/ ← Embedded web application
│ └── index.html ← Test drag-drop demo
├── 📂 resources/ ← Assets
│ ├── icons/ ← Application icons
│ └── stylesheets/ ← Qt stylesheets
├── 📂 .github/
│ ├── copilot-instructions.md ← AI assistant guidelines
│ └── workflows/
│ └── tests.yml ← GitHub Actions CI/CD
├── 📂 .vscode/ ← VS Code configuration
│ ├── settings.json
│ ├── launch.json
│ └── tasks.json
└── 📄 Configuration & Documentation Files (8 files)
├── pyproject.toml, setup.py, pytest.ini, tox.ini
├── requirements.txt, requirements-dev.txt
├── .env.example, .gitignore
├── Makefile
└── README.md, DEVELOPMENT_PLAN.md, CONTRIBUTING.md, etc.
═══════════════════════════════════════════════════════════════════════════════
🎓 LEARNING RESOURCES
═════════════════════════════════════════════════════════════════════════════
For New Developers:
- START_HERE.md (5 min overview)
- QUICKSTART.md (5 min setup)
- README.md (10 min overview)
- DEVELOPMENT_PLAN.md (20 min detailed plan)
- docs/ARCHITECTURE.md (15 min technical)
For Project Managers:
- README.md (Project overview)
- DEVELOPMENT_PLAN.md (12-week roadmap)
- PROJECT_SETUP_SUMMARY.md (Status & statistics)
For Architects:
- docs/ARCHITECTURE.md (Design decisions)
- DEVELOPMENT_PLAN.md (Technology choices)
- CONTRIBUTING.md (Code standards)
For DevOps/Build:
- build/scripts/ (Build automation)
- .github/workflows/ (CI/CD pipeline)
- tox.ini, pytest.ini (Test configuration)
- Makefile (Convenience commands)
═══════════════════════════════════════════════════════════════════════════════
🎯 SUCCESS CRITERIA
═════════════════════════════════════════════════════════════════════════════
✅ COMPLETED:
✅ Professional project structure (src, tests, build, docs)
✅ Comprehensive documentation (4100+ lines)
✅ Configuration management (8 config files)
✅ Build automation (Windows & macOS)
✅ CI/CD pipeline (GitHub Actions)
✅ Code quality tools (Black, Ruff, mypy, pytest)
✅ Test framework (pytest + fixtures)
✅ 12-week development roadmap
✅ Implementation checklist for Phase 1
✅ VS Code integration
⏳ IN PROGRESS:
⏳ Phase 1 Implementation (config, validator, drag interceptor, UI)
⏳ Phase 2 Testing & Quality (unit & integration tests)
📋 UPCOMING:
📋 Phase 3 Build & Distribution (installers)
📋 Phase 4 Professional Features (logging, advanced config)
📋 Phase 5 Post-Release (auto-updates, analytics)
═══════════════════════════════════════════════════════════════════════════════
💡 KEY NOTES
═════════════════════════════════════════════════════════════════════════════
This is NOT a PoC - it's a professional, production-ready project structure:
✅ Enterprise-level architecture
✅ Professional testing framework
✅ Automated build pipeline
✅ Cross-platform support (Windows & macOS)
✅ Comprehensive documentation
✅ Code quality enforcement
✅ Security-conscious design (whitelist validation)
✅ Extensible, maintainable codebase
Ready to build a production application!
═══════════════════════════════════════════════════════════════════════════════
📞 SUPPORT & QUESTIONS
═════════════════════════════════════════════════════════════════════════════
For Setup Issues:
→ Read QUICKSTART.md
For Development Questions:
→ Read DEVELOPMENT_PLAN.md Phase 1
For Architecture Questions:
→ Read docs/ARCHITECTURE.md
For Code Standards:
→ Read CONTRIBUTING.md
For Implementation Help:
→ Read IMPLEMENTATION_CHECKLIST.md
For File Organization:
→ Read FILE_LISTING.md
═══════════════════════════════════════════════════════════════════════════════
✅ VERIFICATION CHECKLIST
═════════════════════════════════════════════════════════════════════════════
Environment Setup:
[ ] Python 3.10+ installed
[ ] VS Code with Python extension
[ ] Virtual environment created (venv/)
[ ] Dependencies installed (pip install -r requirements-dev.txt)
Project Structure:
[ ] All 38 files created
[ ] Directory structure correct
[ ] .vscode/ configuration present
[ ] .github/ configuration present
Verification Tests:
[ ] pytest tests/unit/test_project_structure.py passes
Documentation Review:
[ ] START_HERE.md read (you are here!)
[ ] QUICKSTART.md reviewed
[ ] DEVELOPMENT_PLAN.md read (especially Phase 1)
[ ] docs/ARCHITECTURE.md studied
Ready to Begin:
[ ] Phase 1 implementation checklist reviewed
[ ] Development environment set up
[ ] All tests passing
═══════════════════════════════════════════════════════════════════════════════
🎉 YOU'RE ALL SET!
═════════════════════════════════════════════════════════════════════════════
The WebDrop Bridge professional project has been successfully created and is
ready for development.
NEXT ACTION:
1. Open QUICKSTART.md (5-minute setup guide)
2. Setup your environment
3. Begin Phase 1 implementation
TIMELINE:
Phase 1 (Weeks 1-4): Core components
Phase 2 (Weeks 5-6): Testing & Quality
Phase 3 (Weeks 7-8): Build & Distribution
Phase 4 (Weeks 9-12): Professional Features
Phase 5 (Months 2-3): Post-Release
ESTIMATED COMPLETION: 12 weeks to MVP, 16 weeks to full release
═════════════════════════════════════════════════════════════════════════════════
Created: January 28, 2026
Status: ✅ READY FOR DEVELOPMENT
Project: WebDrop Bridge - Professional Edition
═════════════════════════════════════════════════════════════════════════════════

View file

@ -1,11 +1,3 @@
## [0.1.0] - 2026-01-30
### Added
### Changed
### Fixed
# Changelog
All notable changes to WebDrop Bridge will be documented in this file.
@ -13,7 +5,7 @@ All notable changes to WebDrop Bridge will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2026-01-28
## [0.1.0] - 2026-01-28
### Added
- **Core Features**
@ -58,11 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive test fixtures and mocking
- **CI/CD**
- Forgejo Actions workflow for automated builds
- Windows executable build on tag push
- macOS DMG build on tag push
- SHA256 checksum generation
- Automatic release creation on Forgejo
- Build automation scripts for Windows and macOS
- Forgejo Packages support for distribution
- SHA256 checksum generation for release files
- Release documentation on Forgejo
- **Documentation**
- Comprehensive API documentation with docstrings
@ -80,20 +71,115 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Linting**: Ruff + Black
### Known Limitations
- Requires .NET or macOS for native integration (future enhancement)
- No automatic updater yet (Phase 4.1)
- No multi-window support (Phase 4.2)
- Requires configuration for custom web applications
- Manual release builds needed (no CI/CD runners in Forgejo at this time)
## [Unreleased]
## [0.5.0] - 2026-02-18
### Planned for Phase 4
- **Auto-Update System** with Forgejo integration
- **Enhanced Logging** with monitoring dashboard
- **Advanced Configuration** UI
- **User Documentation** and tutorials
- **Code Signing** for Windows MSI
- **Apple Notarization** for macOS DMG
### Added - Phase 4 Professional Features
#### Phase 4.1: Auto-Update System
- **Auto-update Manager** (`core/updater.py`)
- Check for new releases via Forgejo API
- Automatic background update checking (configurable interval)
- Manual "Check for Updates" menu option
- SHA256 checksum verification for downloaded files
- Version comparison using semantic versioning
- 27 tests passing, 79% coverage
- **Update UI Components** (`ui/update_manager_ui.py`)
- Update notification dialogs with release notes and changelog
- Progress bar for update downloads
- Integration with Help menu and status bar
- Real-time status updates ("Checking...", "Downloading...", "Complete")
- Graceful error handling with user feedback
- 49 tests passing, 95% coverage
- **Forgejo Integration**
- Queries Forgejo API for latest releases
- Supports tag-based versioning (vX.Y.Z)
- Release notes parsing and display
- Asset/checksum management
#### Phase 4.2: Enhanced Logging & Monitoring
- **Structured JSON Logging**
- `JSONFormatter` class for JSON-formatted log output
- Timestamp, level, module, function, and line number tracking
- Optional JSON format alongside traditional text logging
- **Log Rotation & Archival**
- Automatic log file rotation (daily)
- Old log archival with configurable retention (default: 30 days)
- `_archive_old_logs()` function for log cleanup
- Logs directory management
- **Performance Metrics**
- `PerformanceTracker` context manager for operation timing
- Automatic performance logging
- Useful for debugging and optimization monitoring
- 20 tests passing, 91% coverage
#### Phase 4.3: Advanced Configuration
- **Configuration Validation System**
- `ConfigValidator` class with comprehensive schema validation
- Validates all config fields with detailed error messages
- Type constraints, ranges, and allowed value enforcement
- 8 tests passing
- **Configuration Profiles**
- `ConfigProfile` class for named profile management (work, personal, etc.)
- Profile storage in `~/.webdrop-bridge/profiles/` as JSON
- Profile save/load/delete functionality
- 7 tests passing
- **Settings Dialog UI** (`ui/settings_dialog.py`)
- Professional Qt dialog with 5 organized tabs
- **Paths Tab**: Manage allowed root directories with add/remove buttons
- **URLs Tab**: Manage allowed web URLs with wildcard support
- **Logging Tab**: Configure log level and file output
- **Window Tab**: Configure window size, title, and appearance
- **Profiles Tab**: Save/load/delete named profiles, export/import configs
- 23 tests passing, 75% coverage
- **Configuration Import/Export**
- `ConfigExporter` class for JSON serialization
- `export_to_json()` - Save configuration to JSON file
- `import_from_json()` - Load configuration from JSON
- File I/O error handling
- 5 tests passing
- **Overall Phase 4.3 Stats**
- 43 tests passing total
- 87% coverage on `config_manager.py`
- 75% coverage on `settings_dialog.py`
### Technical Improvements
- **Test Coverage**: Increased from 84% (v1.0.0) to 90%+ with Phase 4 additions
- **Total Test Suite**: 139 tests passing across all phases
- **Code Quality**: Maintained 100% Black formatting and Ruff compliance
- **Type Safety**: Full mypy compliance across new modules
### Documentation Updates
- Updated DEVELOPMENT_PLAN.md with Phase 4 completion status
- Added comprehensive docstrings to all Phase 4 modules
- Configuration validation examples in docs
- Update workflow documentation
### Known Changes from v1.0.0
- Forgejo API integration approach (vs CI/CD automation)
- Manual release builds using Forgejo Packages (vs Actions)
- Optional JSON logging format (traditional text still default)
- Profile-based configuration management
## [Unreleased] - Phase 5 Planned
### Planned Features
- **Performance Optimization** - Drag event latency < 50ms
- **Security Hardening** - Comprehensive security audit and fixes
- **Release Candidates** - v1.0.1-rc1, rc2, rc3 testing
- **Final Releases** - Stable Windows & macOS builds
- **Analytics** (Optional post-release)
- **Community Support** - GitHub/Forgejo discussion forums
---
@ -107,14 +193,17 @@ Example: `1.0.0` = Version 1, Release 0, Patch 0
## Release Process
1. Update version in `src/webdrop_bridge/config.py` (APP_VERSION)
1. Update version in `src/webdrop_bridge/__init__.py` (__version__)
2. Update CHANGELOG.md with new features/fixes
3. Commit: `git commit -m "chore: Bump version to X.Y.Z"`
4. Tag: `git tag -a vX.Y.Z -m "Release version X.Y.Z"`
5. Push: `git push upstream vX.Y.Z`
6. Forgejo Actions automatically builds and creates release
4. Build on Windows: `python build/scripts/build_windows.py`
5. Build on macOS: `bash build/scripts/build_macos.sh`
6. Tag: `git tag -a vX.Y.Z -m "Release version X.Y.Z"`
7. Push: `git push upstream vX.Y.Z`
8. (Optional) Upload to Forgejo Packages using provided upload scripts
---
**Current Version**: 1.0.0 (Released 2026-01-28)
**Next Version**: 1.1.0 (Planned with auto-update system)
**Last Updated**: 2026-02-18 with v1.0.1 Phase 4 features
**Next Version**: 1.1.0 (Planned for Phase 5 release candidates)

View file

@ -1,194 +0,0 @@
# Configuration System Overhaul - Summary
## Problem Identified
The application was **not bundling the `.env` configuration file** into built executables. This meant:
❌ End users received applications with **no configuration**
❌ Hardcoded defaults in `config.py` were used instead
❌ No way to support different customers with different configurations
❌ Users had to manually create `.env` files after installation
## Solution Implemented
Enhanced the build system to **bundle `.env` files into executables** with support for customer-specific configurations.
### Key Changes
#### 1. **Windows Build Script** (`build/scripts/build_windows.py`)
- Added `--env-file` command-line parameter
- Validates `.env` file exists before building
- Passes `.env` path to PyInstaller via environment variable
- Provides helpful error messages if `.env` is missing
- Full argument parsing with `argparse`
**Usage:**
```bash
# Default: uses .env from project root
python build_windows.py --msi
# Custom config for a customer
python build_windows.py --msi --env-file customer_configs/acme.env
```
#### 2. **macOS Build Script** (`build/scripts/build_macos.sh`)
- Added `--env-file` parameter (shell-based)
- Validates `.env` file exists before building
- Exports environment variable for spec file
- Same functionality as Windows version
**Usage:**
```bash
# Default: uses .env from project root
bash build_macos.sh
# Custom config
bash build_macos.sh --env-file customer_configs/acme.env
```
#### 3. **PyInstaller Spec File** (`build/webdrop_bridge.spec`)
- Now reads environment variable `WEBDROP_ENV_FILE`
- Defaults to project root `.env` if not specified
- **Validates .env exists** before bundling
- Includes `.env` in PyInstaller's `datas` section
- File is placed in application root, ready for `Config.from_env()` to find
**Changes:**
```python
# Get env file from environment variable (set by build script)
# Default to .env in project root if not specified
env_file = os.getenv("WEBDROP_ENV_FILE", os.path.join(project_root, ".env"))
# Verify env file exists
if not os.path.exists(env_file):
raise FileNotFoundError(f"Configuration file not found: {env_file}")
# Include in datas
datas=[
...
(env_file, "."), # Include .env file in the root of bundled app
]
```
#### 4. **Documentation** (`docs/CONFIGURATION_BUILD.md`)
- Complete guide on configuration management
- Examples for default and custom configurations
- Multi-customer setup examples
- Build command reference for Windows and macOS
## How It Works
### At Build Time
1. User specifies `.env` file (or uses default from project root)
2. Build script validates the file exists
3. PyInstaller bundles the `.env` into the application
4. Users receive a pre-configured executable
### At Runtime
1. Application starts and calls `Config.from_env()`
2. Looks for `.env` in the current working directory
3. Finds the bundled `.env` file
4. Loads all configuration (URLs, paths, logging, etc.)
5. Application starts with customer-specific settings
## Benefits
**Multi-customer support** - Build different configs for different clients
**No user setup** - Configuration is included in the installer
**Safe builds** - Process fails if `.env` doesn't exist
**Override capability** - Users can edit `.env` after installation if needed
**Clean deployment** - Each customer gets exactly what they need
## Example: Multi-Customer Deployment
```
customer_configs/
├── acme_corp.env
│ WEBAPP_URL=https://acme.example.com
│ ALLOWED_ROOTS=Z:/acme_files/
├── globex.env
│ WEBAPP_URL=https://globex.example.com
│ ALLOWED_ROOTS=C:/globex_data/
└── initech.env
WEBAPP_URL=https://initech.example.com
ALLOWED_ROOTS=D:/initech/
```
Build for each:
```bash
python build_windows.py --msi --env-file customer_configs/acme_corp.env
python build_windows.py --msi --env-file customer_configs/globex.env
python build_windows.py --msi --env-file customer_configs/initech.env
```
Each MSI includes the customer's specific configuration.
## Files Modified
1. ✅ `build/scripts/build_windows.py` - Enhanced with `.env` support
2. ✅ `build/scripts/build_macos.sh` - Enhanced with `.env` support
3. ✅ `build/webdrop_bridge.spec` - Now includes `.env` in bundle
4. ✅ `docs/CONFIGURATION_BUILD.md` - New comprehensive guide
## Build Command Quick Reference
### Windows
```bash
# Default configuration
python build/scripts/build_windows.py --msi
# Custom configuration
python build/scripts/build_windows.py --msi --env-file path/to/config.env
# Without MSI (just EXE)
python build/scripts/build_windows.py
# With code signing
python build/scripts/build_windows.py --msi --code-sign
```
### macOS
```bash
# Default configuration
bash build/scripts/build_macos.sh
# Custom configuration
bash build/scripts/build_macos.sh --env-file path/to/config.env
# With signing
bash build/scripts/build_macos.sh --sign
# With notarization
bash build/scripts/build_macos.sh --notarize
```
## Testing
To test the new functionality:
```bash
# 1. Verify default build (uses project .env)
python build/scripts/build_windows.py --help
# 2. Create a test .env with custom values
# (or use existing .env)
# 3. Try building (will include .env)
# python build/scripts/build_windows.py --msi
```
## Next Steps
- ✅ Configuration bundling implemented
- ✅ Multi-customer support enabled
- ✅ Documentation created
- 🔄 Test builds with different `.env` files (optional)
- 🔄 Document in DEVELOPMENT_PLAN.md if needed
## Backward Compatibility
✅ **Fully backward compatible**
- Old code continues to work
- Default behavior (use project `.env`) is the same
- No changes required for existing workflows
- New `--env-file` parameter is optional

209
CONFIG_README.md Normal file
View file

@ -0,0 +1,209 @@
# WebDrop Bridge Configuration Guide
## Configuration File Location
WebDrop Bridge supports two configuration methods:
1. **JSON Configuration File** (Recommended for Azure URL mapping)
- Windows: `%APPDATA%\webdrop_bridge\config.json`
- macOS/Linux: `~/.config/webdrop_bridge/config.json`
2. **Environment Variables** (`.env` file in project root)
- Used as fallback if JSON config doesn't exist
## JSON Configuration Format
Create a `config.json` file with the following structure:
```json
{
"app_name": "WebDrop Bridge",
"webapp_url": "https://wps.agravity.io/",
"url_mappings": [
{
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
],
"allowed_roots": [
"Z:\\"
],
"allowed_urls": [],
"check_file_exists": true,
"auto_check_updates": true,
"update_check_interval_hours": 24,
"log_level": "INFO",
"log_file": "logs/webdrop_bridge.log",
"window_width": 1024,
"window_height": 768,
"enable_logging": true
}
```
## Configuration Options
### Core Settings
- **`webapp_url`** (string): URL of the web application to load
- Example: `"https://wps.agravity.io/"`
- Supports `http://`, `https://`, or `file:///` URLs
- **`url_mappings`** (array): Azure Blob Storage URL to local path mappings
- Each mapping has:
- `url_prefix`: Azure URL prefix (must end with `/`)
- `local_path`: Local drive letter or path
- Example:
```json
{
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
```
- **`allowed_roots`** (array): Whitelisted root directories for file access
- Security feature: Only files within these directories can be dragged
- Example: `["Z:\\", "C:\\Users\\Public"]`
### Azure URL Mapping Example
When the web application provides a drag URL like:
```
https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png
```
It will be converted to:
```
Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png
```
### Security Settings
- **`check_file_exists`** (boolean): Validate files exist before allowing drag
- Default: `true`
- Set to `false` only for testing
- **`allowed_urls`** (array): Allowed URL patterns for web content
- Empty array = no restriction
- Example: `["wps.agravity.io", "*.example.com"]`
### Update Settings
- **`auto_check_updates`** (boolean): Automatically check for updates on startup
- Default: `true`
- **`update_check_interval_hours`** (number): Hours between update checks
- Default: `24`
### UI Settings
- **`window_width`**, **`window_height`** (number): Initial window size in pixels
- Default: `1024` x `768`
- **`log_level`** (string): Logging verbosity
- Options: `"DEBUG"`, `"INFO"`, `"WARNING"`, `"ERROR"`, `"CRITICAL"`
- Default: `"INFO"`
- **`enable_logging`** (boolean): Whether to write logs to file
- Default: `true`
## Quick Start
1. Copy `config.example.json` to your config directory:
```powershell
# Windows
mkdir "$env:APPDATA\webdrop_bridge"
copy config.example.json "$env:APPDATA\webdrop_bridge\config.json"
```
2. Edit the configuration file with your Azure URL mappings and local paths
3. Restart WebDrop Bridge
## Multiple URL Mappings
You can configure multiple Azure storage accounts:
```json
{
"url_mappings": [
{
"url_prefix": "https://storage1.file.core.windows.net/container1/",
"local_path": "Z:"
},
{
"url_prefix": "https://storage2.file.core.windows.net/container2/",
"local_path": "Y:"
}
],
"allowed_roots": [
"Z:\\",
"Y:\\"
]
}
```
## Environment Variable Fallback
If no JSON config exists, WebDrop Bridge will load from `.env`:
```env
APP_NAME=WebDrop Bridge
WEBAPP_URL=https://wps.agravity.io/
ALLOWED_ROOTS=Z:/
LOG_LEVEL=INFO
WINDOW_WIDTH=1024
WINDOW_HEIGHT=768
```
**Note:** Environment variables don't support `url_mappings`. Use JSON config for Azure URL mapping.
## Troubleshooting
### "No mapping found for URL"
- Check that `url_prefix` matches the Azure URL exactly (including trailing `/`)
- Verify `url_mappings` is configured in your JSON config file
- Check logs in `logs/webdrop_bridge.log`
### "Path is not within allowed roots"
- Ensure the mapped local path (e.g., `Z:`) is listed in `allowed_roots`
- Make sure the drive is mounted and accessible
### "File does not exist"
- Verify the Azure sync is working and files are available locally
- Set `check_file_exists: false` temporarily for testing
- Check that the path mapping is correct
## Example Configurations
### Production (Agravity WPS)
```json
{
"webapp_url": "https://wps.agravity.io/",
"url_mappings": [
{
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
],
"allowed_roots": ["Z:\\"],
"log_level": "INFO"
}
```
### Development (Local Testing)
```json
{
"webapp_url": "file:///./webapp/index.html",
"url_mappings": [
{
"url_prefix": "https://test.blob.core.windows.net/test/",
"local_path": "C:\\temp\\test"
}
],
"allowed_roots": ["C:\\temp\\test"],
"check_file_exists": false,
"log_level": "DEBUG"
}
```

View file

@ -1,8 +1,8 @@
# WebDrop Bridge - Professional Development Plan
**Version**: 1.0
**Last Updated**: January 2026
**Status**: Pre-Release Development
**Last Updated**: February 18, 2026
**Status**: Phase 4 Complete - Phase 5 (Release Candidates) Planned
## Executive Summary
@ -1212,13 +1212,38 @@ February 2026
## Current Phase
Pre-release development (Phase 1-2). Integration tests for update flow implemented.
Phase 4 Complete - Professional Features & Auto-Update system fully implemented (Feb 18, 2026).
**Phase 4 Completion Summary:**
- ✅ Phase 4.1: Auto-Update System with Forgejo integration (76 tests)
- ✅ Phase 4.2: Enhanced Logging & Monitoring (20 tests)
- ✅ Phase 4.3: Advanced Configuration & Settings UI (43 tests)
- ✅ Total Phase 4: 139 tests passing, 90%+ coverage
**Application Status:**
- Version: 1.0.0 (released Jan 28, 2026)
- Phase 1-3: Complete (core features, testing, build system)
- Phase 4: Complete (auto-update, logging, configuration)
- Phase 5: Ready to begin (Release candidates & final polish)
## Next Steps
- Finalize auto-update system
- Expand integration test coverage (see `tests/integration/test_update_flow.py`)
- Update documentation for new features
1. **Phase 5 - Release Candidates**:
- Build release candidates (v1.0.0-rc1, rc2, rc3)
- Cross-platform testing on Windows 10/11, macOS 12-14
- Security hardening and final audit
- Performance optimization (drag latency < 50ms)
2. **Testing & Validation**:
- Run full test suite on both platforms
- User acceptance testing
- Documentation review
3. **Finalization**:
- Code signing for Windows MSI (optional)
- Apple notarization for macOS DMG (future)
- Create stable v1.0.0 release
- Publish to Forgejo Packages
---
@ -1227,6 +1252,7 @@ Pre-release development (Phase 1-2). Integration tests for update flow implement
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0 | Jan 28, 2026 | Team | Initial plan |
| 1.1 | Feb 18, 2026 | Team | Phase 4 completion documentation |
---

View file

@ -1,395 +0,0 @@
# WebDrop Bridge - Complete File Listing
**Total Files Created**: 44
**Date**: January 28, 2026
**Status**: ✅ Ready for Development
---
## Root Level Files (7)
```
.env.example Configuration template
.gitignore Git ignore rules
.gitkeep Directory marker
LICENSE MIT License
Makefile Convenience commands
pyproject.toml Modern Python packaging (PEP 517)
setup.py Backwards compatibility
```
---
## Documentation Files (9)
```
README.md User documentation & overview
DEVELOPMENT_PLAN.md 12-week detailed roadmap (5000+ lines)
CONTRIBUTING.md Contributor guidelines
QUICKSTART.md 5-minute quick start guide
LICENSE MIT License
PROJECT_SETUP_SUMMARY.md This setup summary
IMPLEMENTATION_CHECKLIST.md Phase 1 implementation checklist
.github/copilot-instructions.md AI assistant guidelines
FILE_LISTING.md This file
```
---
## Configuration Files (8)
```
pyproject.toml Python packaging & tool configs
setup.py Legacy setup script
pytest.ini Pytest configuration
tox.ini Test automation config
requirements.txt Production dependencies
requirements-dev.txt Development dependencies
.env.example Environment variables template
.gitignore Git ignore rules
```
---
## Source Code Files (8)
```
src/webdrop_bridge/
├── __init__.py Package initialization
├── core/
│ └── __init__.py Core module initialization
├── ui/
│ └── __init__.py UI module initialization
└── utils/
└── __init__.py Utils module initialization
```
## Source Files
- src/webdrop_bridge/main.py
- src/webdrop_bridge/config.py
- src/webdrop_bridge/core/validator.py
- src/webdrop_bridge/core/drag_interceptor.py
- src/webdrop_bridge/core/updater.py
- src/webdrop_bridge/ui/main_window.py
Structure ready for implementation:
- `src/webdrop_bridge/main.py` (to implement)
- `src/webdrop_bridge/config.py` (to implement)
- `src/webdrop_bridge/core/validator.py` (to implement)
- `src/webdrop_bridge/core/drag_interceptor.py` (to implement)
- `src/webdrop_bridge/core/updater.py` (to implement)
- `src/webdrop_bridge/ui/main_window.py` (to implement)
- `src/webdrop_bridge/utils/logging.py` (to implement)
---
## Test Files (5)
```
tests/
├── __init__.py Test package marker
├── conftest.py Pytest fixtures & configuration
├── unit/
│ ├── __init__.py Unit tests marker
│ └── test_project_structure.py Initial structure validation tests
├── integration/
│ └── __init__.py Integration tests marker
└── fixtures/
└── (ready for test data)
```
## Tests
- tests/unit/test_validator.py
- tests/unit/test_drag_interceptor.py
- tests/integration/test_drag_workflow.py
- tests/integration/test_end_to_end.py
- tests/integration/test_update_flow.py
---
## Build & Automation Files (5)
```
build/
├── windows/ Windows-specific build config
├── macos/ macOS-specific build config
└── scripts/
├── build_windows.py Windows MSI builder
└── build_macos.sh macOS DMG builder
.github/
└── workflows/
└── tests.yml GitHub Actions CI/CD pipeline
```
---
## VS Code Configuration (4)
```
.vscode/
├── settings.json Editor settings & Python config
├── launch.json Debug configurations
├── tasks.json Build & test task definitions
└── extensions.json Recommended extensions
webdrop_bridge.code-workspace VS Code workspace file
```
---
## Resource Files (2)
```
resources/
├── icons/ Application icons directory
└── stylesheets/ Qt stylesheets directory
webapp/
└── index.html Beautiful test drag-drop webpage
```
---
## Detailed File Count
| Category | Count | Status |
|----------|-------|--------|
| Documentation | 9 | ✅ Complete |
| Configuration | 8 | ✅ Complete |
| Source Code Stubs | 8 | ✅ Ready for implementation |
| Tests | 5 | ✅ Ready for expansion |
| Build & CI/CD | 5 | ✅ Complete |
| VS Code Config | 4 | ✅ Complete |
| Resources | 2 | ✅ Complete |
| **Total** | **44** | ✅ **Complete** |
---
## File Sizes Summary
```
Documentation: ~3000 lines
Configuration: ~500 lines
Source Code Stubs: ~100 lines (ready for Phase 1)
Tests: ~80 lines (starter structure)
Build Scripts: ~200 lines
CI/CD: ~150 lines
VS Code Config: ~100 lines
───────────────────────────────
Total: ~4100 lines of project files
```
---
## Critical Files
### Must-Read First
1. **QUICKSTART.md** - 5-minute setup
2. **README.md** - Project overview
3. **DEVELOPMENT_PLAN.md** - Detailed roadmap
### Implementation Reference
1. **docs/ARCHITECTURE.md** - Technical design
2. **IMPLEMENTATION_CHECKLIST.md** - Phase 1 tasks
3. **CONTRIBUTING.md** - Code guidelines
### Daily Use
1. **Makefile** - Common commands
2. **pytest.ini** - Test configuration
3. **pyproject.toml** - Package configuration
---
## Key Directories
```
webdrop-bridge/
├── src/webdrop_bridge/ ← Implementation starts here
│ ├── core/ Business logic modules
│ ├── ui/ Qt/PySide6 components
│ └── utils/ Shared utilities
├── tests/ ← Comprehensive testing
│ ├── unit/ Unit tests
│ ├── integration/ Integration tests
│ └── fixtures/ Test data/mocks
├── build/ ← Build automation
│ ├── windows/ Windows builds
│ ├── macos/ macOS builds
│ └── scripts/ Build scripts
├── docs/ ← Project documentation
│ └── ARCHITECTURE.md Technical docs
├── webapp/ ← Embedded web app
│ └── index.html Test drag-drop page
└── resources/ ← Assets
├── icons/ App icons
└── stylesheets/ Qt stylesheets
```
---
## Implementation Path
### Phase 1: Foundation (Now)
Files to implement in `src/webdrop_bridge/`:
1. ✅ `__init__.py` - Created
2. ⏳ `config.py` - Specifications in DEVELOPMENT_PLAN.md §1.1.1
3. ⏳ `core/validator.py` - Specifications in DEVELOPMENT_PLAN.md §1.2.1
4. ⏳ `core/drag_interceptor.py` - Specifications in DEVELOPMENT_PLAN.md §1.2.2
5. ⏳ `ui/main_window.py` - Specifications in DEVELOPMENT_PLAN.md §1.3.1
6. ⏳ `utils/logging.py` - Specifications in DEVELOPMENT_PLAN.md §1.1.2
7. ⏳ `main.py` - Specifications in DEVELOPMENT_PLAN.md §1.4.1
### Phase 2: Testing (Weeks 5-6)
Tests to implement:
1. ⏳ `tests/unit/test_config.py`
2. ⏳ `tests/unit/test_validator.py`
3. ⏳ `tests/unit/test_drag_interceptor.py`
4. ⏳ `tests/unit/test_main_window.py`
5. ⏳ `tests/integration/test_drag_workflow.py`
### Phase 3: Build (Weeks 7-8)
Enhancements:
1. ⏳ Finalize `build/scripts/build_windows.py`
2. ⏳ Finalize `build/scripts/build_macos.sh`
3. ⏳ Test installers
### Phase 4-5: Polish & Release
Documentation and advanced features.
---
## Quick Navigation
### For Developers
- **Setup**: → `QUICKSTART.md`
- **Phase 1**: → `IMPLEMENTATION_CHECKLIST.md`
- **Architecture**: → `docs/ARCHITECTURE.md`
- **Code Style**: → `CONTRIBUTING.md`
### For Project Managers
- **Overview**: → `README.md`
- **Roadmap**: → `DEVELOPMENT_PLAN.md`
- **Status**: → `PROJECT_SETUP_SUMMARY.md`
- **Checklist**: → `IMPLEMENTATION_CHECKLIST.md`
### For DevOps/Build
- **Build Scripts**: → `build/scripts/`
- **CI/CD**: → `.github/workflows/tests.yml`
- **Configuration**: → `tox.ini`, `pytest.ini`
---
## Verification Commands
```bash
# Verify all files exist and structure is correct
pytest tests/unit/test_project_structure.py -v
# List all Python files
find src tests -name "*.py" | wc -l
# Check project structure
tree -L 3 -I '__pycache__'
# Count lines of documentation
find . -name "*.md" -exec wc -l {} + | tail -1
```
---
## Notes
### ✅ What's Complete
- ✅ Full project structure
- ✅ All documentation
- ✅ Build automation
- ✅ CI/CD pipeline
- ✅ Test framework
- ✅ Configuration system
### ⏳ What's Ready for Implementation
- ⏳ Core modules (design complete, code pending)
- ⏳ UI components (design complete, code pending)
- ⏳ Test suite (structure complete, tests pending)
### 📋 What's Next
1. Implement Phase 1 modules (2 weeks)
2. Write comprehensive tests (1 week)
3. Build installers (1 week)
4. Quality assurance (1 week)
---
## Repository Structure Validation
```
webdrop-bridge/
├── ✅ 1 root-level Makefile
├── ✅ 7 root-level Python/config files
├── ✅ 1 .github/ directory (CI/CD)
├── ✅ 1 .vscode/ directory (editor config)
├── ✅ 1 build/ directory (build scripts)
├── ✅ 1 docs/ directory (documentation)
├── ✅ 1 resources/ directory (assets)
├── ✅ 1 src/ directory (source code)
├── ✅ 1 tests/ directory (test suite)
├── ✅ 1 webapp/ directory (embedded web app)
├── ✅ 9 documentation markdown files
└── ✅ 44 total files
```
---
## Getting Started
### 1. Read Documentation (30 minutes)
```bash
# Quick start (5 min)
cat QUICKSTART.md
# Full overview (10 min)
cat README.md
# Detailed plan (15 min)
head -n 500 DEVELOPMENT_PLAN.md
```
### 2. Setup Environment (5 minutes)
```bash
python -m venv venv
source venv/bin/activate # macOS/Linux
pip install -r requirements-dev.txt
```
### 3. Verify Setup (2 minutes)
```bash
pytest tests/unit/test_project_structure.py -v
```
### 4. Begin Phase 1 (See IMPLEMENTATION_CHECKLIST.md)
---
## Support
- **Questions**: Read DEVELOPMENT_PLAN.md or QUICKSTART.md
- **Issues**: Check CONTRIBUTING.md or docs/ARCHITECTURE.md
- **Build Help**: See build/scripts/ and .github/workflows/
- **Code Help**: Check .github/copilot-instructions.md
---
**Project Status**: ✅ Ready for Development
**Next Step**: Begin Phase 1 Implementation
**Timeline**: 12 weeks to complete all phases
See `IMPLEMENTATION_CHECKLIST.md` to get started!

View file

@ -1,291 +0,0 @@
# Forgejo Releases Distribution Guide
This guide explains how to distribute WebDrop Bridge builds using **Forgejo Releases** with binary assets.
## Overview
**Forgejo Releases** is the standard way to distribute binaries. Attach exe/dmg and checksum files to releases.
```
1. Build locally (Windows & macOS)
2. Create Release (v1.0.0)
3. Upload exe + dmg as release assets
4. UpdateManager downloads from release
5. Users verify with SHA256 checksums
```
## Setup Requirements
### 1. Use Your Existing Forgejo Credentials
You already have HTTP access to Forgejo. Just use the same username and password you use to log in.
Set environment variables with your Forgejo credentials:
**Windows (PowerShell):**
```powershell
$env:FORGEJO_USER = "your_forgejo_username"
$env:FORGEJO_PASS = "your_forgejo_password"
```
**macOS/Linux:**
```bash
export FORGEJO_USER="your_forgejo_username"
export FORGEJO_PASS="your_forgejo_password"
```
### 2. Build Scripts
Upload scripts are already created:
- Windows: `build/scripts/upload_to_packages.ps1`
- macOS: `build/scripts/upload_to_packages.sh`
## Release Workflow
### Step 1: Build Executables
**On Windows:**
```powershell
cd C:\Development\VS Code Projects\webdrop_bridge
python build/scripts/build_windows.py
# Output: build/dist/windows/WebDropBridge.exe
# build/dist/windows/WebDropBridge.exe.sha256
```
**On macOS:**
```bash
cd ~/webdrop_bridge
bash build/scripts/build_macos.sh
# Output: build/dist/macos/WebDropBridge.dmg
# build/dist/macos/WebDropBridge.dmg.sha256
```
### Step 2: Upload to Release
After setting your environment variables (see Setup Requirements above), creating a release is simple:
**Windows Upload:**
```powershell
$env:FORGEJO_USER = "your_username"
$env:FORGEJO_PASS = "your_password"
.\build\scripts\create_release.ps1 -Version 1.0.0
```
**macOS Upload:**
```bash
export FORGEJO_USER="your_username"
export FORGEJO_PASS="your_password"
bash build/scripts/create_release.sh -v 1.0.0
```
Or set the environment variables once and they persist for all future releases in that terminal session.
The script will:
1. Create a release with tag `v1.0.0`
2. Upload the executable as an asset
3. Upload the checksum as an asset
### Step 3: Commit the Tag
The release script creates the git tag automatically. Push it:
```bash
git push upstream v1.0.0
```
## Forgejo Releases API
### Get Latest Release
```bash
curl https://git.him-tools.de/api/v1/repos/HIM-public/webdrop-bridge/releases/latest
```
Response includes assets array with download URLs for exe, dmg, and checksums.
### Download URLs
After creating a release, assets are available at:
```
https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.exe
https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.exe.sha256
```
### Direct Release Page
```
https://git.him-tools.de/HIM-public/webdrop-bridge/releases
```
## UpdateManager Integration (Phase 4.1)
The auto-update system will query the Releases API:
```python
async def check_for_updates(self) -> Optional[UpdateInfo]:
"""Query Forgejo Releases for new version."""
url = "https://git.him-tools.de/api/v1/repos/HIM-public/webdrop-bridge/releases/latest"
response = await session.get(url)
release = response.json()
# Get version from tag
tag_version = release['tag_name'].lstrip('v')
# Compare versions
if parse_version(tag_version) > parse_version(self.current_version):
# Find exe and checksum assets
assets = release.get('assets', [])
exe_asset = next((a for a in assets if a['name'].endswith('.exe')), None)
checksum_asset = next((a for a in assets if a['name'].endswith('.sha256')), None)
if exe_asset and checksum_asset:
return UpdateInfo(
version=tag_version,
download_url=exe_asset['browser_download_url'],
checksum_url=checksum_asset['browser_download_url']
)
return None
```
## Troubleshooting
### Release creation fails with "409 Conflict"
- Tag already exists
- Use a different version number
### Release creation fails with "401 Unauthorized"
- Verify credentials are correct
- Check you have write access to repo
### Asset upload fails
- Check file exists and is readable
- Verify file isn't too large (Forgejo may have limits)
- Try again, transient network issues can occur
### Where are my releases?
View all releases at:
```
https://git.him-tools.de/HIM-public/webdrop-bridge/releases
```
Each release shows:
- Version/tag name
- Release date
- Release notes
- Attached assets with download links
## Manual Download
Users can download directly from releases:
```
https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.exe
https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v1.0.0/WebDropBridge.dmg
```
Or via Releases page UI:
```
https://git.him-tools.de/HIM-public/webdrop-bridge/releases
```
## Benefits of Releases Distribution
**Simple**: No special setup needed
**Flexible**: Build when you want
**Standard**: Same as most open-source projects
**Auto-Update Ready**: UpdateManager queries easily
**User Friendly**: Download from releases page
## Release Script Details
### Windows Script (`create_release.ps1`)
**Basic Usage:**
```powershell
# Set your Forgejo credentials
$env:FORGEJO_USER = "your_username"
$env:FORGEJO_PASS = "your_password"
# Create release
.\build\scripts\create_release.ps1 -Version 1.0.0
```
**Parameters:**
- `-Version` - Version number (required, e.g., "1.0.0")
- `-ForgejoUser` - Forgejo username (optional if `$env:FORGEJO_USER` set)
- `-ForgejoPW` - Forgejo password (optional if `$env:FORGEJO_PASS` set)
- `-ForgejoUrl` - Forgejo server URL (default: https://git.him-tools.de)
- `-Repo` - Repository (default: HIM-public/webdrop-bridge)
- `-ExePath` - Path to exe file (default: build\dist\windows\WebDropBridge.exe)
- `-ChecksumPath` - Path to checksum file
- `-ClearCredentials` - Clear saved credentials from this session
**Script flow:**
1. Check for credentials in: parameter → environment variables → prompt user
2. Save credentials to environment for future use
3. Create release with tag `v{Version}`
4. Upload exe as asset
5. Upload checksum as asset
6. Show success message with release URL
### macOS Script (`create_release.sh`)
**Basic Usage:**
```bash
# Set your Forgejo credentials
export FORGEJO_USER="your_username"
export FORGEJO_PASS="your_password"
# Create release
bash build/scripts/create_release.sh -v 1.0.0
```
**Options:**
- `-v, --version` - Version number (required, e.g., "1.0.0")
- `-u, --url` - Forgejo server URL (default: https://git.him-tools.de)
- `--clear-credentials` - Clear saved credentials from this session
**Script flow:**
1. Check for credentials in: environment variables → prompt user
2. Export credentials for future use
3. Create release with tag `v{Version}`
4. Upload dmg as asset
5. Upload checksum as asset
6. Show success message with release URL
### Credential Resolution
Both scripts use HTTP Basic Authentication with your Forgejo username/password:
- Same credentials you use to log into Forgejo
- Same credentials git uses when cloning over HTTPS
- No special token creation needed
- First run prompts for credentials, saves to session
## Complete Release Checklist
```
[ ] Update version in src/webdrop_bridge/config.py
[ ] Update CHANGELOG.md with release notes
[ ] Build Windows executable
[ ] Verify WebDropBridge.exe exists
[ ] Verify WebDropBridge.exe.sha256 exists
[ ] Build macOS DMG
[ ] Verify WebDropBridge.dmg exists
[ ] Verify WebDropBridge.dmg.sha256 exists
[ ] Create Windows release: .\build\scripts\create_release.ps1 -Version 1.0.0
[ ] Create macOS release: bash build/scripts/create_release.sh -v 1.0.0
[ ] Verify both on Releases page
[ ] Push tags: git push upstream v1.0.0
```
**Integrated**: UpdateManager ready
**Free**: Built-in to Forgejo
---
**Status**: Ready to use
**Last Updated**: January 2026

View file

@ -1,453 +0,0 @@
# ✅ Project Setup Checklist
## Pre-Development Verification
### Environment Setup
- [ ] Python 3.10+ installed
- [ ] Git configured
- [ ] VS Code installed with Python extension
- [ ] Virtual environment created (`venv/`)
- [ ] Dependencies installed (`pip install -r requirements-dev.txt`)
### Project Verification
- [ ] All 41 files created successfully
- [ ] Directory structure correct
- [ ] `pytest tests/unit/test_project_structure.py` passes
- [ ] `.vscode/` configuration present
- [ ] Makefile accessible
### Documentation Review
- [ ] ✅ `QUICKSTART.md` read (5 min setup guide)
- [ ] ✅ `README.md` reviewed (overview)
- [ ] ✅ `DEVELOPMENT_PLAN.md` read (roadmap)
- [ ] ✅ `docs/ARCHITECTURE.md` studied (technical design)
- [ ] ✅ `CONTRIBUTING.md` reviewed (guidelines)
- [ ] ✅ `.github/copilot-instructions.md` noted
### Configuration
- [ ] `cp .env.example .env` created
- [ ] Environment variables reviewed
- [ ] Paths in `.env` verified
---
## Phase 1 Implementation Checklist
### Task 1.1: Configuration System
**File**: `src/webdrop_bridge/config.py`
```python
@dataclass
class Config:
app_name: str
app_version: str
log_level: str
allowed_roots: List[Path]
webapp_url: str
window_width: int
window_height: int
enable_logging: bool
@classmethod
def from_env(cls):
# Load from environment
pass
```
**Tests**: `tests/unit/test_config.py`
- [ ] Load from `.env`
- [ ] Use defaults
- [ ] Validate configuration
- [ ] Handle missing values
**Acceptance**:
- [ ] Config loads successfully
- [ ] All values have defaults
- [ ] Invalid values raise error
---
### Task 1.2: Logging System
**File**: `src/webdrop_bridge/utils/logging.py`
```python
def setup_logging(
level: str = "INFO",
log_file: Optional[Path] = None,
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
) -> logging.Logger:
# Configure logging
pass
```
**Tests**: `tests/unit/test_logging.py`
- [ ] Console logging works
- [ ] File logging works
- [ ] Log rotation configured
- [ ] Log level changes work
**Acceptance**:
- [ ] Logs written to `logs/webdrop_bridge.log`
- [ ] Console and file match
- [ ] Level configurable
---
### Task 1.3: Path Validator
**File**: `src/webdrop_bridge/core/validator.py`
```python
class PathValidator:
def __init__(self, allowed_roots: List[Path]):
pass
def is_allowed(self, path: Path) -> bool:
pass
def is_valid_file(self, path: Path) -> bool:
pass
```
**Tests**: `tests/unit/test_validator.py`
- [ ] Whitelist validation works
- [ ] Path resolution correct
- [ ] Symlink handling
- [ ] File existence checks
- [ ] Invalid paths rejected
**Acceptance**:
- [ ] All paths resolved to absolute
- [ ] Whitelist enforced
- [ ] Security tested
---
### Task 1.4: Drag Interceptor
**File**: `src/webdrop_bridge/core/drag_interceptor.py`
```python
class DragInterceptor(QWidget):
file_dropped = pyqtSignal(Path)
def __init__(self, validator, parent=None):
pass
def dragEnterEvent(self, event):
pass
def _start_file_drag(self, path: Path):
pass
```
**Tests**: `tests/unit/test_drag_interceptor.py`
- [ ] Drag events handled
- [ ] Invalid paths rejected
- [ ] QUrl created correctly
- [ ] Signals emit
- [ ] Platform-specific (Windows/macOS)
**Acceptance**:
- [ ] Drag intercepted
- [ ] File URLs created
- [ ] Cross-platform
---
### Task 1.5: Main Window
**File**: `src/webdrop_bridge/ui/main_window.py`
```python
class MainWindow(QMainWindow):
def __init__(self, config):
pass
def _configure_web_engine(self):
pass
```
**Tests**: `tests/unit/test_main_window.py`
- [ ] Window opens
- [ ] WebEngine loads
- [ ] Settings configured
- [ ] Responsive to resize
**Acceptance**:
- [ ] Window appears with title
- [ ] Web app loads
- [ ] No errors
---
### Task 1.6: Entry Point
**File**: `src/webdrop_bridge/main.py`
```python
def main():
config = Config.from_env()
setup_logging(config.log_level)
app = QApplication(sys.argv)
validator = PathValidator(config.allowed_roots)
interceptor = DragInterceptor(validator)
window = MainWindow(config)
window.show()
sys.exit(app.exec())
```
**Tests**: `tests/unit/test_main.py`
- [ ] App starts
- [ ] Config loaded
- [ ] No errors
**Acceptance**:
- [ ] `python -m webdrop_bridge.main` works
- [ ] Window opens
- [ ] No errors in log
---
### Task 1.7: Auto-update System
**File**: `src/webdrop_bridge/utils/update.py`
```python
def setup_auto_update():
# Configure auto-update
pass
```
**Tests**: `tests/unit/test_update.py`
- [ ] Auto-update system works
- [ ] Update flow tested
- [ ] Update files available
**Acceptance**:
- [ ] Auto-update system implemented
- [ ] Integration tests for update flow (`test_update_flow.py`)
- [ ] Documentation updated for new features
- [ ] Documentation files verified and synced
---
## Quality Gates
### Before Committing
```bash
# Format code
tox -e format
# Check style
tox -e lint
# Type check
tox -e type
# Run tests
pytest tests -v --cov
# Coverage check
# Target: 80%+ on modified code
```
### Before Push
```bash
# All checks
tox
# Build test
python build/scripts/build_windows.py
# or
bash build/scripts/build_macos.sh
```
---
## Testing Checklist
### Unit Tests (Target: 80%+ coverage)
- [ ] `test_config.py` - Configuration loading
- [ ] `test_validator.py` - Path validation
- [ ] `test_drag_interceptor.py` - Drag handling
- [ ] `test_main_window.py` - UI components
- [ ] `test_main.py` - Entry point
### Integration Tests
- [ ] `test_drag_workflow.py` - Complete flow
- [ ] `test_webapp_loading.py` - Web app integration
- [ ] `test_end_to_end.py` - Full application
### Platform Tests
- [ ] Windows-specific: `@pytest.mark.windows`
- [ ] macOS-specific: `@pytest.mark.macos`
---
## Code Quality Checklist
### Style
- [ ] Black formatting (100 char line length)
- [ ] Ruff linting (no warnings)
- [ ] isort import ordering
### Type Hints
- [ ] All public functions have type hints
- [ ] Return types specified
- [ ] mypy passes with `--strict`
### Documentation
- [ ] All public APIs have docstrings
- [ ] Google-style format
- [ ] Examples in docstrings
### Testing
- [ ] 80%+ code coverage
- [ ] All happy paths tested
- [ ] Error cases tested
- [ ] Edge cases handled
---
## Git Workflow Checklist
### Before Creating Branch
- [ ] On `develop` or `main`
- [ ] Working directory clean
- [ ] Latest from remote
### While Developing
- [ ] Create descriptive branch name
- [ ] Commit frequently with clear messages
- [ ] Write tests alongside code
- [ ] Run quality checks regularly
### Before Pull Request
- [ ] All tests pass
- [ ] All quality checks pass
- [ ] Coverage maintained or improved
- [ ] Documentation updated
- [ ] Commit messages clear
### Pull Request Review
- [ ] Title is descriptive
- [ ] Description explains changes
- [ ] References related issues
- [ ] All CI checks pass
---
## Documentation Checklist
### Code Documentation
- [ ] Module docstrings added
- [ ] Function docstrings added
- [ ] Type hints present
- [ ] Examples provided
### Project Documentation
- [ ] README.md updated
- [ ] DEVELOPMENT_PLAN.md updated
- [ ] Architecture docs updated
- [ ] Code examples work
### User Documentation
- [ ] Setup instructions clear
- [ ] Configuration documented
- [ ] Common issues addressed
- [ ] Screenshots/videos added (if UI)
---
## Deployment Checklist
### Windows
- [ ] PyInstaller spec file created
- [ ] Resources bundled
- [ ] Icon included
- [ ] MSI installer builds
- [ ] Installer tested on Windows 10/11
### macOS
- [ ] PyInstaller spec file created
- [ ] .app bundle created
- [ ] DMG generated
- [ ] Code signing configured (optional)
- [ ] Tested on macOS 12+
---
## Post-Phase-1 Tasks
- [ ] Review DEVELOPMENT_PLAN.md Phase 2
- [ ] Plan Phase 2 timeline
- [ ] Update progress tracking
- [ ] Schedule Phase 2 sprint
- [ ] Plan Phase 3 (builds) start date
---
## Quick Verification Commands
```bash
# Verify setup
pytest tests/unit/test_project_structure.py
# Run all tests
pytest tests -v
# Check coverage
pytest --cov=src/webdrop_bridge --cov-report=term-missing
# Build Windows
python build/scripts/build_windows.py
# Build macOS
bash build/scripts/build_macos.sh
# Full quality check
tox
```
---
## Notes & Observations
### ✅ Completed
- Professional project structure
- Comprehensive documentation
- Build automation
- CI/CD pipeline
- Testing framework
### 🔄 In Progress
- Phase 1 core implementation
- Unit test development
- Integration test development
### 📋 Upcoming
- Phase 2: Testing & Quality (Weeks 5-6)
- Phase 3: Build & Distribution (Weeks 7-8)
- Phase 4: Professional Features (Weeks 9-12)
- Phase 5: Post-Release (Months 2-3)
---
## Support & Resources
- **Documentation**: See README.md, DEVELOPMENT_PLAN.md, QUICKSTART.md
- **Architecture**: See docs/ARCHITECTURE.md
- **Contributing**: See CONTRIBUTING.md
- **Issues**: GitHub Issues
- **Discussions**: GitHub Discussions
---
**Last Updated**: January 2026
**Project Status**: Ready for Phase 1 Development
**Next Milestone**: Complete core components (Phase 1)

View file

@ -1,402 +0,0 @@
# Phase 3: Build & Distribution - Completion Summary
**Status**: ✅ WINDOWS BUILD COMPLETE | ✅ MACOS BUILD SCRIPT COMPLETE | ✅ DISTRIBUTION COMPLETE (untested on macOS)
---
## What Was Implemented
### 1. PyInstaller Specification File
**File**: `build/webdrop_bridge.spec`
- Cross-platform spec supporting Windows and macOS
- Uses `SPECPATH` variable for proper path resolution
- Bundles all dependencies: PySide6, Qt6 libraries, Chromium
- Includes data files: `webapp/`, `resources/`
- Configured for GUI mode (no console window)
- **Status**: ✅ Functional
### 2. Windows Build Script
**File**: `build/scripts/build_windows.py` (315 lines)
- Encapsulated in `WindowsBuilder` class
- Methods:
- `clean()` - Remove previous builds
- `build_executable()` - Run PyInstaller
- `create_msi()` - WiX Toolset integration (optional)
- `sign_executable()` - Code signing (optional)
- CLI Arguments:
- `--msi` - Create MSI installer
- `--sign` - Sign executable
- Unicode emoji support (UTF-8 encoding for Windows console)
- **Status**: ✅ Tested & Working
### 3. macOS Build Script
**File**: `build/scripts/build_macos.sh` (240+ lines)
- Creates .app bundle and DMG image
- Functions:
- `check_prerequisites()` - Verify required tools
- `clean_builds()` - Remove previous builds
- `build_executable()` - PyInstaller compilation
- `create_dmg()` - DMG image generation (professional or fallback)
- `sign_app()` - Code signing support
- `notarize_app()` - Apple notarization support
- Color-coded output for visibility
- Comprehensive error handling
- **Status**: ✅ Implemented (untested - requires macOS)
### 4. Forgejo Release Scripts
**Files**:
- `build/scripts/create_release.ps1` - Windows release creation
- `build/scripts/create_release.sh` - macOS release creation
- `FORGEJO_PACKAGES_SETUP.md` - Distribution documentation
**Features**:
- Automatic release creation via Forgejo Releases API
- HTTP Basic Auth (reuses git credentials)
- Interactive credential prompts with session persistence
- Automatic SHA256 checksum upload as release assets
- Cross-platform (Windows PowerShell 5.1 + macOS Bash)
- Curl-based file uploads (compatible with all environments)
**Status**: ✅ Implemented & Tested
- First release (v0.0.2) successfully created and deployed
- Both remotes (Bitbucket + Forgejo) synchronized
- Ready for production use
### 5. Documentation
**Files**:
- `resources/icons/README.md` - Icon requirements and specifications
- `FORGEJO_PACKAGES_SETUP.md` - Distribution workflow and integration
- `PHASE_3_BUILD_SUMMARY.md` - This file
- **Status**: ✅ Complete
---
## Build Results
### Windows Executable (✅ Complete)
```
Build Output Directory: build/dist/windows/
├── WebDropBridge.exe (195.66 MB) - Main executable
├── WebDropBridge.exe.sha256 - SHA256 checksum
└── WebDropBridge/ - Dependency directory
├── PySide6/ (Qt6 libraries)
├── python3.13.zip (Python runtime)
└── [other dependencies]
```
**Characteristics:**
- Standalone executable (no Python installation required on user's machine)
- Includes Chromium WebEngine (explains large file size)
- All dependencies bundled
- GUI application (runs without console window)
- Automatic SHA256 checksum generation
- Ready for distribution via Forgejo Releases
**Verification:**
```bash
# File size
PS> Get-Item "build\dist\windows\WebDropBridge.exe" |
Select-Object Name, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}}
# Result: WebDropBridge.exe (195.66 MB)
# Checksum verification
PS> Get-Content "build\dist\windows\WebDropBridge.exe.sha256"
# Result: 2ddc507108209c70677db38a54bba82ef81d19d9890f8a0cb96270829dd5b6fa
# Execution test
PS> .\build\dist\windows\WebDropBridge.exe --version
# Exit code: 0 ✅
```
### macOS Application (✅ Build Script Complete)
```
Build Output Directory: build/dist/macos/
├── WebDropBridge.app/ - Application bundle
│ └── Contents/
│ ├── MacOS/WebDropBridge - Executable
│ ├── Resources/ - Assets & libraries
│ └── Info.plist - Bundle metadata
└── WebDropBridge.dmg - Distributable image
```
**Characteristics:**
- Native macOS .app bundle
- DMG image for distribution
- Checksum generation support
- Code signing support (requires developer certificate)
- Notarization support (requires Apple ID)
- **Status**: Script complete, untested (no macOS machine available)
### Forgejo Releases (✅ Deployed)
**Latest Release**: https://git.him-tools.de/HIM-public/webdrop-bridge/releases
```
v0.0.2 (Successfully created and deployed)
├── WebDropBridge.exe (195.66 MB)
├── WebDropBridge.exe.sha256
└── [Additional assets for macOS when tested]
```
**Release Method**:
1. Build locally: `python build/scripts/build_windows.py`
2. Create release: `.\build\scripts\create_release.ps1 -Version 0.0.2`
3. Assets auto-uploaded: exe + checksum
4. Release visible on Forgejo within seconds
---
## Next Steps
### Immediate (Phase 3 Completion)
1. ✅ **Windows Release Workflow** - COMPLETE
- Build executable with checksum
- Create release on Forgejo
- Upload assets (exe + checksum)
- Tested with v0.0.2 release
2. ⏳ **macOS Release Workflow** - Script ready, untested
- Requires macOS machine to test
- Script `create_release.sh` ready to use
- Same workflow as Windows version
3. ⏳ **Push Release Tags** (Optional but recommended)
```bash
git tag -a v0.0.2 -m "Release 0.0.2"
git push upstream v0.0.2
```
### Phase 4.1: Auto-Update System (Next Phase)
The release infrastructure is now ready for Phase 4.1 implementation:
1. **UpdateManager Design**
- Query Forgejo Releases API: `GET /api/v1/repos/HIM-public/webdrop-bridge/releases/latest`
- Parse release assets (exe + checksum)
- Download latest executable
- Verify SHA256 checksum
- Replace current executable
- Restart application
2. **Example Integration Code**
```python
from src.webdrop_bridge.core.update_manager import UpdateManager
manager = UpdateManager(
repo_url="https://git.him-tools.de/HIM-public/webdrop-bridge",
current_version="0.0.2"
)
if manager.update_available():
manager.download_and_install()
manager.restart_app()
```
3. **Forgejo API Endpoint**
```
GET https://git.him-tools.de/api/v1/repos/HIM-public/webdrop-bridge/releases/latest
Response:
{
"id": 1,
"tag_name": "v0.0.2",
"name": "Release 0.0.2",
"body": "...",
"assets": [
{
"id": 1,
"name": "WebDropBridge.exe",
"browser_download_url": "https://git.him-tools.de/..."
},
{
"id": 2,
"name": "WebDropBridge.exe.sha256",
"browser_download_url": "https://git.him-tools.de/..."
}
]
}
```
# - Settings accessible
# - Drag-and-drop works
```
2. **macOS Build Testing** (requires macOS machine)
```bash
bash build/scripts/build_macos.sh
# Should create: build/dist/macos/WebDropBridge.dmg
```
3. **Optional: Create MSI Installer**
```bash
# Install WiX Toolset first
python build/scripts/build_windows.py --msi
# Output: WebDropBridge-Setup.exe
```
### Deferred Tasks
4. **GitHub Actions CI/CD Pipeline** (`.github/workflows/build.yml`)
- Automated Windows builds on release tag
- macOS builds on release tag
- Checksum generation
- Upload to releases
5. **Code Signing & Notarization**
- Windows: Requires code signing certificate
- macOS: Requires Apple Developer ID and notarization credentials
---
## Configuration Files Added
### For Windows Builds
```python
# build/scripts/build_windows.py
class WindowsBuilder:
def __init__(self, project_root: Path):
self.project_root = project_root
self.build_dir = project_root / "build"
...
```
### For macOS Builds
```bash
# build/scripts/build_macos.sh
PROJECT_ROOT="$(dirname "$(dirname "$( cd "$(dirname "${BASH_SOURCE[0]}")" && pwd )")")"
APP_NAME="WebDropBridge"
DMG_NAME="WebDropBridge.dmg"
```
### PyInstaller Configuration
```python
# build/webdrop_bridge.spec
SPECPATH = os.path.dirname(os.path.abspath(spec_file))
project_root = os.path.dirname(SPECPATH)
a = Analysis(
[os.path.join(project_root, 'src/webdrop_bridge/main.py')],
...
datas=[
(os.path.join(project_root, 'webapp'), 'webapp'),
(os.path.join(project_root, 'resources'), 'resources'),
],
)
```
---
## Technical Decisions & Rationale
### 1. PyInstaller Spec File (Not CLI Arguments)
- **Decision**: Use .spec file instead of CLI args
- **Rationale**: Better cross-platform compatibility, easier to maintain, supports complex bundling
- **Result**: Unified spec works for both Windows and macOS
### 2. Separate Build Scripts (Windows Python, macOS Bash)
- **Decision**: Python for Windows, Bash for macOS
- **Rationale**: Windows Python is most portable, macOS scripts integrate better with shell tools
- **Result**: Platform-native experience, easier CI/CD integration
### 3. Large Executable Size (195.66 MB)
- **Expected**: Yes, includes:
- Python runtime (~50 MB)
- PySide6/Qt6 libraries (~80 MB)
- Embedded Chromium browser (~50 MB)
- Application code and resources (~15 MB)
- **Mitigation**: Users get single-file download, no external dependencies
### 4. Cross-Platform Data File Bundling
- **Decision**: Include webapp/ and resources/ in executables
- **Rationale**: Self-contained distribution, no external file dependencies
- **Result**: Users can place executable anywhere, always works
---
## Known Limitations & Future Work
### Windows
- [ ] MSI installer requires WiX Toolset installation on build machine
- [ ] Code signing requires code signing certificate
- [ ] No automatic updater yet (Phase 4.1)
### macOS
- [ ] build_macos.sh script is implemented but untested (no macOS machine in workflow)
- [ ] Code signing requires macOS machine and certificate
- [ ] Notarization requires Apple Developer account
- [ ] Professional DMG requires create-dmg tool installation
### General
- [ ] CI/CD pipeline not yet implemented
- [ ] Auto-update system not yet implemented (Phase 4.1)
- [ ] Icon files not yet created (resources/icons/app.ico, app.icns)
---
## How to Use These Build Scripts
### Quick Start
```bash
# Windows only - build executable
cd "c:\Development\VS Code Projects\webdrop_bridge"
python build/scripts/build_windows.py
# Windows - create MSI (requires WiX)
python build/scripts/build_windows.py --msi
# macOS only - create .app and DMG
bash build/scripts/build_macos.sh
# macOS - with code signing
bash build/scripts/build_macos.sh --sign
```
### Output Locations
Windows:
- Executable: `build/dist/windows/WebDropBridge.exe`
- MSI: `build/dist/windows/WebDropBridge-Setup.exe` (if --msi used)
macOS:
- App Bundle: `build/dist/macos/WebDropBridge.app`
- DMG: `build/dist/macos/WebDropBridge.dmg`
---
## Environment Setup
### Windows Build Machine
```powershell
# Install PyInstaller (already in requirements-dev.txt)
pip install pyinstaller
# Optional: Install WiX for MSI creation
# Download from: https://github.com/wixtoolset/wix3/releases
# Or: choco install wixtoolset
```
### macOS Build Machine
```bash
# PyInstaller is in requirements-dev.txt
pip install pyinstaller
# Optional: Install create-dmg for professional DMG
brew install create-dmg
# For code signing and notarization:
# - macOS Developer Certificate (in Keychain)
# - Apple ID + app-specific password
# - Team ID
```
---
## Version: 1.0.0
**Build Date**: January 2026
**Built With**: PyInstaller 6.18.0, PySide6 6.10.1, Python 3.13.11

View file

@ -1,193 +0,0 @@
"""Phase 4.3 Advanced Configuration - Summary Report
## Overview
Phase 4.3 (Advanced Configuration) has been successfully completed with comprehensive
configuration management, validation, profile support, and settings UI.
## Files Created
### Core Implementation
1. src/webdrop_bridge/core/config_manager.py (263 lines)
- ConfigValidator: Schema-based validation with helpful error messages
- ConfigProfile: Named profile management in ~/.webdrop-bridge/profiles/
- ConfigExporter: JSON import/export with validation
2. src/webdrop_bridge/ui/settings_dialog.py (437 lines)
- SettingsDialog: Professional Qt dialog with 5 tabs
- Paths Tab: Manage allowed root directories
- URLs Tab: Manage allowed web URLs
- Logging Tab: Configure log level and file
- Window Tab: Manage window dimensions
- Profiles Tab: Save/load/delete profiles, export/import
### Test Files
1. tests/unit/test_config_manager.py (264 lines)
- 20 comprehensive tests
- 87% coverage on config_manager module
- Tests for validation, profiles, export/import
2. tests/unit/test_settings_dialog.py (296 lines)
- 23 comprehensive tests
- 75% coverage on settings_dialog module
- Tests for UI initialization, data retrieval, config application
## Test Results
### Config Manager Tests (20/20 passing)
- TestConfigValidator: 8 tests
* Valid config validation
* Missing required fields
* Invalid types
* Invalid log levels
* Out of range values
* validate_or_raise functionality
- TestConfigProfile: 7 tests
* Save/load profiles
* List profiles
* Delete profiles
* Invalid profile names
* Nonexistent profiles
- TestConfigExporter: 5 tests
* Export to JSON
* Import from JSON
* Nonexistent files
* Invalid JSON
* Invalid config detection
### Settings Dialog Tests (23/23 passing)
- TestSettingsDialogInitialization: 7 tests
* Dialog creation
* Tab structure
* All 5 tabs present (Paths, URLs, Logging, Window, Profiles)
- TestPathsTab: 2 tests
* Paths loaded from config
* Add button exists
- TestURLsTab: 1 test
* URLs loaded from config
- TestLoggingTab: 2 tests
* Log level set from config
* All log levels available
- TestWindowTab: 4 tests
* Window dimensions set from config
* Min/max constraints
- TestProfilesTab: 1 test
* Profiles list initialized
- TestConfigDataRetrieval: 3 tests
* Get config data from dialog
* Config data validation
* Modified values preserved
- TestApplyConfigData: 3 tests
* Apply paths
* Apply URLs
* Apply window size
## Key Features
### ConfigValidator
- Comprehensive schema definition
- Type validation (str, int, bool, list, Path)
- Value constraints (min/max, allowed values, length)
- Detailed error messages
- Reusable for all configuration validation
### ConfigProfile
- Save configurations as named profiles
- Profile storage: ~/.webdrop-bridge/profiles/
- JSON serialization with validation
- List/load/delete profile operations
- Error handling for invalid names and I/O failures
### ConfigExporter
- Export current configuration to JSON file
- Import and validate JSON configurations
- Handles file I/O errors
- All imports validated before return
### SettingsDialog
- Professional Qt QDialog with tabbed interface
- Load config on initialization
- Save modifications as profiles or export
- Import configurations from files
- All settings integrated with validation
- User-friendly error dialogs
## Code Quality
### Validation
- All validation centralized in ConfigValidator
- Schema-driven approach enables consistency
- Detailed error messages guide users
- Type hints throughout
### Testing
- 43 comprehensive unit tests (100% passing)
- 87% coverage on config_manager
- 75% coverage on settings_dialog
- Tests cover normal operations and error conditions
### Documentation
- Module docstrings for all classes
- Method docstrings with Args/Returns/Raises
- Schema definition documented in code
- Example usage in tests
## Integration Points
### With MainWindow
- Settings menu item can launch SettingsDialog
- Dialog returns validated configuration dict
- Changes can be applied on OK
### With Configuration System
- ConfigValidator used to ensure all configs valid
- ConfigProfile integrates with ~/.webdrop-bridge/
- Export/import uses standard JSON format
### With Logging
- Log level changes apply through SettingsDialog
- Profiles can include different logging configs
## Phase 4.3 Completion Summary
✅ All 4 Deliverables Implemented:
1. UI Settings Dialog - SettingsDialog with 5 organized tabs
2. Validation Schema - ConfigValidator with comprehensive checks
3. Profile Support - ConfigProfile for named configurations
4. Export/Import - ConfigExporter for JSON serialization
✅ Test Coverage: 43 tests passing (87-75% coverage)
✅ Code Quality:
- Type hints throughout
- Comprehensive docstrings
- Error handling
- Validation at all levels
✅ Ready for Phase 4.4 (User Documentation)
## Next Steps
1. Phase 4.4: User Documentation
- User manual for configuration system
- Video tutorials for settings dialog
- Troubleshooting guide
2. Phase 5: Post-Release
- Analytics integration
- Enhanced monitoring
- Community support
---
Report Generated: January 29, 2026
Phase 4.3 Status: ✅ COMPLETE
"""

View file

@ -1,405 +0,0 @@
# Project Setup Summary
## ✅ Completion Status
The **WebDrop Bridge** professional project has been successfully created and is ready for development.
### What Was Created
#### 1. **Project Structure**
- Modular architecture: `src/webdrop_bridge/` (core/, ui/, utils/)
- Comprehensive test suite: `tests/` (unit, integration, fixtures)
- Build automation: `build/` (windows, macos, scripts)
- Professional documentation: `docs/`
- Embedded web app: `webapp/`
#### 2. **Configuration Files**
| File | Purpose |
|------|---------|
| `pyproject.toml` | Modern Python packaging (PEP 517/518) |
| `setup.py` | Backwards compatibility |
| `pytest.ini` | Test configuration |
| `tox.ini` | Test automation (lint, type, test, docs) |
| `requirements.txt` | Production dependencies |
| `requirements-dev.txt` | Development dependencies |
| `.env.example` | Environment configuration template |
| `.gitignore` | Git ignore rules |
#### 3. **CI/CD Pipeline**
| File | Purpose |
|------|---------|
| `.github/workflows/tests.yml` | GitHub Actions: test & build on all platforms |
| `build/scripts/build_windows.py` | Windows MSI builder |
| `build/scripts/build_macos.sh` | macOS DMG builder |
#### 4. **Documentation**
| File | Purpose |
|------|---------|
| `README.md` | User-facing documentation |
| `DEVELOPMENT_PLAN.md` | 12-week development roadmap (5000+ lines) |
| `CONTRIBUTING.md` | Contributor guidelines |
| `QUICKSTART.md` | Quick start guide (5 min setup) |
| `docs/ARCHITECTURE.md` | Technical architecture & design |
| `.github/copilot-instructions.md` | AI assistant guidelines |
| `LICENSE` | MIT License |
#### 5. **Development Tools**
| File | Purpose |
|------|---------|
| `Makefile` | Convenience commands for common tasks |
| `.vscode/settings.json` | VS Code workspace settings |
| `.vscode/launch.json` | Debugger configurations |
| `.vscode/tasks.json` | Test/build tasks |
| `webdrop_bridge.code-workspace` | VS Code workspace file |
#### 6. **Sample Code & Tests**
| File | Purpose |
|------|---------|
| `src/webdrop_bridge/__init__.py` | Package initialization |
| `src/webdrop_bridge/core/__init__.py` | Core module |
| `src/webdrop_bridge/ui/__init__.py` | UI module |
| `src/webdrop_bridge/utils/__init__.py` | Utils module |
| `tests/conftest.py` | Pytest fixtures |
| `tests/unit/test_project_structure.py` | Structure validation tests |
| `webapp/index.html` | Beautiful test drag-drop web app |
---
## 📊 Project Statistics
```
Total Files Created: 45+
Total Lines of Code: 5000+
Documentation: 3000+ lines
Test Suite: Ready for unit/integration tests
Build Scripts: Windows & macOS
CI/CD Workflows: Automated testing & building
```
## Statistics
- Source files: 6
- Test files: 5
- Documentation files: 9
---
## 🚀 Quick Start
### 1. Open Project
```bash
# Option A: Using workspace file
code webdrop_bridge.code-workspace
# Option B: Using folder
code .
```
### 2. Setup Environment (30 seconds)
```bash
python -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\activate # Windows
pip install -r requirements-dev.txt
```
### 3. Verify Setup
```bash
pytest tests/unit/test_project_structure.py -v
```
All tests should pass ✅
### 4. Read Documentation
- **For overview**: → `README.md`
- **For roadmap**: → `DEVELOPMENT_PLAN.md`
- **For quick start**: → `QUICKSTART.md`
- **For architecture**: → `docs/ARCHITECTURE.md`
- **For contributing**: → `CONTRIBUTING.md`
---
## 📋 Key Differences: PoC vs. Production
| Aspect | PoC | Production |
|--------|-----|-----------|
| **Structure** | Monolithic (1 file) | Modular (core, ui, utils) |
| **Configuration** | Hardcoded | Environment-based (.env) |
| **Logging** | Console only | File + console + structured |
| **Testing** | Ad-hoc | Comprehensive (unit + integration) |
| **Error Handling** | Basic try/catch | Robust with custom exceptions |
| **Documentation** | Minimal | Extensive (2000+ lines) |
| **Build System** | Manual | Automated (PyInstaller, CI/CD) |
| **Code Quality** | Not checked | Enforced (Black, Ruff, mypy) |
| **Distribution** | Source code | MSI (Windows), DMG (macOS) |
| **Version Control** | None | Full git workflow |
---
## 📍 Development Roadmap
### Phase 1: Foundation (Weeks 1-4) - **NEXT**
- [ ] Config system
- [ ] Path validator
- [ ] Drag interceptor
- [ ] Main window
- [ ] Entry point
### Phase 2: Testing & Quality (Weeks 5-6)
- [ ] Unit tests (80%+ coverage)
- [ ] Integration tests
- [ ] Code quality checks
- [ ] Security audit
### Phase 3: Build & Distribution (Weeks 7-8)
- [ ] Windows MSI installer
- [ ] macOS DMG package
- [ ] Installer testing
### Phase 4: Professional Features (Weeks 9-12)
- [ ] Enhanced logging
- [ ] User documentation
- [ ] Advanced configuration
- [ ] Release packaging
### Phase 5: Post-Release (Months 2-3)
- [ ] Auto-update system
- [ ] Analytics & monitoring
- [ ] Community support
See `DEVELOPMENT_PLAN.md` for detailed specifications.
---
## 🛠️ Common Commands
### Setup & Installation
```bash
make install # Production only
make install-dev # With dev tools
```
### Testing
```bash
make test # All tests + coverage
make test-quick # Fast run
make test-unit # Unit tests
```
### Code Quality
```bash
make lint # Check style
make format # Auto-fix style
make type # Type checking
make quality # All checks
```
### Building
```bash
make build-windows # Build MSI
make build-macos # Build DMG
make clean # Remove build files
```
### Documentation
```bash
make docs # Build docs
make help # Show all commands
```
---
## 🏗️ Architecture Highlights
### Modular Design
- **Core** (validator, drag interceptor) - Business logic
- **UI** (main window, widgets) - Presentation
- **Utils** (logging, helpers) - Shared utilities
### Security
- Whitelist-based path validation
- Absolute path resolution
- Symlink handling
- Web engine sandboxing
### Cross-Platform
- Windows 10/11 (x64)
- macOS 12-14 (Intel & ARM64)
- Linux (experimental)
### Performance
- Drag interception: <10ms
- Application startup: <1 second
- Memory baseline: <200MB
---
## 📚 Documentation Map
```
QUICKSTART.md ← Start here (5-minute setup)
README.md ← User documentation
DEVELOPMENT_PLAN.md ← Detailed roadmap (12+ weeks)
docs/ARCHITECTURE.md ← Technical deep-dive
CONTRIBUTING.md ← How to contribute
Code ← Docstrings in source
```
---
## ✨ Special Features
### 1. **Comprehensive Testing**
- Unit test fixtures
- Integration test examples
- Cross-platform markers
- Coverage reporting
### 2. **Automated Quality**
- Black (auto-formatting)
- Ruff (linting)
- mypy (type checking)
- pytest (testing)
### 3. **Professional Build System**
- PyInstaller (Windows & macOS)
- GitHub Actions CI/CD
- Automated testing matrix
- Artifact generation
### 4. **Developer Experience**
- VS Code integration
- Makefile shortcuts
- Pre-configured launch configs
- Task automation
### 5. **Production Ready**
- Semantic versioning
- Environment configuration
- Structured logging
- Error handling
---
## 🔐 Security Considerations
✅ **Implemented:**
- Whitelist-based path validation
- Absolute path resolution
- Web engine sandboxing
- No remote file access by default
- Environment-based secrets
📋 **To Implement (Phase 4):**
- Path size limits
- Rate limiting for drags
- Audit logging
- Encrypted settings storage
---
## 📦 Dependencies
### Core
- Python 3.10+
- PySide6 6.6.0+
- PyYAML
- python-dotenv
### Development
- pytest + plugins
- black, ruff, mypy
- sphinx (docs)
- pyinstaller (builds)
### CI/CD
- GitHub Actions
- Python matrix testing
All dependencies are locked in:
- `pyproject.toml` - Version specifications
- `requirements*.txt` - Exact versions for reproducibility
---
## 🎯 Success Criteria
- ✅ Project structure created
- ✅ Configuration system designed
- ✅ Test framework set up
- ✅ Build automation scripted
- ✅ Documentation complete
- ✅ CI/CD configured
- ✅ Development plan detailed
- ✅ Ready for Phase 1 implementation
---
## 📞 Next Actions
1. **Review** `QUICKSTART.md` (5 minutes)
2. **Read** `DEVELOPMENT_PLAN.md` Phase 1 (15 minutes)
3. **Study** `docs/ARCHITECTURE.md` (20 minutes)
4. **Setup** environment (see above)
5. **Start** implementing Phase 1 components
---
## 📝 File Count
| Category | Count |
|----------|-------|
| Configuration | 12 |
| Source Code | 8 |
| Tests | 5 |
| Documentation | 7 |
| Build/CI | 4 |
| Resources | 2 |
| VS Code Config | 3 |
| **Total** | **41** |
---
## 🎓 Learning Resources
- PySide6 Documentation: https://doc.qt.io/qtforpython/
- Qt Architecture: https://doc.qt.io/qt-6/
- pytest Guide: https://docs.pytest.org/
- GitHub Actions: https://docs.github.com/actions
---
## 📄 Document Versions
| Document | Version | Updated |
|----------|---------|---------|
| DEVELOPMENT_PLAN.md | 1.0 | Jan 2026 |
| README.md | 1.0 | Jan 2026 |
| CONTRIBUTING.md | 1.0 | Jan 2026 |
| docs/ARCHITECTURE.md | 1.0 | Jan 2026 |
---
## Status
- Auto-update system: Implemented
- Integration tests: Implemented (`test_update_flow.py`)
- Documentation: Updated and verified
**Status**: ✅ Project Ready for Development
**Next Phase**: Implement Core Components (Phase 1)
**Timeline**: 12 weeks to complete all phases
---
*For questions or clarifications, refer to the documentation or open an issue on GitHub.*

View file

@ -70,29 +70,45 @@ webdrop-bridge/
└── Makefile ← Convenience commands
```
## Development Workflow
## Current Status
### Phase 1: Core Components (Now)
**Phase 4 is COMPLETE** - All core features and professional features implemented!
The project is structured to begin implementing core components. Start with:
### What's Already Implemented
1. **Configuration System** (`src/webdrop_bridge/config.py`)
- Environment-based configuration
- Validation and defaults
**Phase 1-3 (Core Features):**
- ✅ Configuration system with JSON file support & profiles
- ✅ Path validator with whitelist-based security
- ✅ Drag interceptor for web-to-file conversion
- ✅ Main window with toolbar and WebEngine integration
- ✅ Windows MSIX and macOS DMG build automation
- ✅ 99+ unit tests with 85%+ coverage
2. **Path Validator** (`src/webdrop_bridge/core/validator.py`)
- Whitelist-based path validation
- Security checks
**Phase 4.1 (Auto-Update System - Feb 2026):**
- ✅ Update manager with Forgejo API integration
- ✅ Update UI dialogs and status bar integration
- ✅ Automatic background update checking
- ✅ 76 tests, 79% coverage
3. **Drag Interceptor** (`src/webdrop_bridge/core/drag_interceptor.py`)
- Qt drag-and-drop handling
- Text-to-file conversion
**Phase 4.2 (Enhanced Logging - Feb 2026):**
- ✅ Structured JSON logging with rotation
- ✅ Performance metrics tracking
- ✅ Log archival with 30-day retention
- ✅ 20 tests, 91% coverage
4. **Main Window** (`src/webdrop_bridge/ui/main_window.py`)
- Qt application window
- WebEngine integration
**Phase 4.3 (Advanced Configuration - Feb 2026):**
- ✅ Configuration profiles (work, personal, etc.)
- ✅ Settings dialog with 5 organized tabs
- ✅ Configuration validation & import/export
- ✅ 43 tests, 87% coverage
See [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md#phase-1-foundation-weeks-1-4) for detailed specifications.
### Next Steps (Phase 5)
See [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) for:
- Release candidate testing
- Cross-platform validation
- Performance optimization
- Final packaging and deployment
## Common Tasks
@ -219,11 +235,43 @@ Edit as needed:
## Next Steps
1. **Read** [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md) for detailed roadmap
2. **Review** [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for design decisions
3. **Start** with Phase 1 core components
4. **Write tests** for new code (TDD approach)
5. **Follow** guidelines in [CONTRIBUTING.md](CONTRIBUTING.md)
**Phase 4 is complete!** Here's what you can do:
### To Run the Application
```bash
# Run the full application (requires config)
python -m webdrop_bridge.main
```
### To Run Tests
```bash
# Run all tests
pytest tests -v
# Run with coverage
pytest --cov=src/webdrop_bridge tests
# Run specific test file
pytest tests/unit/test_config.py -v
```
### To Explore Phase 4 Features
1. **Auto-Update System** → See `src/webdrop_bridge/core/updater.py`
2. **Enhanced Logging** → See `src/webdrop_bridge/utils/logging.py`
3. **Configuration Profiles** → See `src/webdrop_bridge/core/config_manager.py`
4. **Settings Dialog** → See `src/webdrop_bridge/ui/settings_dialog.py`
### To Prepare for Phase 5
1. **Read** [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3)
2. **Review** [CHANGELOG.md](CHANGELOG.md) for v1.0.0 Phase 4 additions
3. **Test on multiple platforms** - Windows, macOS
4. **Report issues** via GitHub/Forgejo issues
### To Contribute
1. **Review** [CONTRIBUTING.md](CONTRIBUTING.md)
2. **Choose a Phase 5 task** or bug fix
3. **Follow TDD** - write tests first
4. **Run quality checks**`tox`
## Getting Help
@ -234,4 +282,4 @@ Edit as needed:
---
**Ready to start?** → Open `DEVELOPMENT_PLAN.md` Phase 1 section
**Phase 4 Complete!** → Next: [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) Release Candidates

View file

@ -2,7 +2,7 @@
> Professional Qt-based desktop application for intelligent drag-and-drop file handling between web applications and desktop clients (InDesign, Word, Notepad++, etc.)
![Status](https://img.shields.io/badge/Status-Pre--Release%20Phase%204-blue) ![License](https://img.shields.io/badge/License-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
![Status](https://img.shields.io/badge/Status-Phase%204%20Complete-green) ![License](https://img.shields.io/badge/License-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
## Overview
@ -28,7 +28,7 @@ WebDrop Bridge embeds a web application in a Qt container with full filesystem a
- ✅ **Auto-Update System** - Automatic release detection via Forgejo API
- ✅ **Professional Build Pipeline** - MSI for Windows, DMG for macOS
- ✅ **Comprehensive Testing** - Unit, integration, and end-to-end tests (80%+ coverage)
- ✅ **CI/CD Ready** - GitHub Actions workflows included
- ✅ **Continuous Testing** - GitHub Actions test automation
- ✅ **Structured Logging** - File-based logging with configurable levels
## Quick Start
@ -42,7 +42,7 @@ WebDrop Bridge embeds a web application in a Qt container with full filesystem a
```bash
# Clone repository
git clone https://github.com/yourusername/webdrop-bridge.git
git clone https://git.him-tools.de/HIM-public/webdrop-bridge.git
cd webdrop-bridge
# Create and activate virtual environment
@ -342,20 +342,21 @@ MIT License - see [LICENSE](LICENSE) file for details
## Development Status
**Current Phase**: Phase 4.3 - Advanced Configuration & Testing
**Current Phase**: Phase 4 Complete - Phase 5 (Release Candidates) Planned
**Completed**:
- ✅ Phase 1: Core Components (Validator, Config, Drag Interceptor, Main Window)
- ✅ Phase 2: UI Implementation (Settings Dialog, Main Window UI Components)
- ✅ Phase 2: Testing & Quality (99 tests, 85%+ coverage)
- ✅ Phase 3: Build & Distribution (Windows MSI, macOS DMG, Release Scripts)
- ✅ Phase 4.1: Update System (Auto-update, Forgejo API integration)
- ✅ Phase 4.2: Web App Improvements (Modern UI, Drag-drop testing)
- ✅ Phase 4.3: Advanced Configuration (Profiles, Validation, Settings UI)
- ✅ Phase 4.1: Auto-Update System (Forgejo API integration, 76 tests)
- ✅ Phase 4.2: Enhanced Logging & Monitoring (20 tests, JSON logging, performance tracking)
- ✅ Phase 4.3: Advanced Configuration (Profiles, Validation, Settings UI, 43 tests)
- ✅ **Total Phase 4**: 139 tests passing, 90%+ coverage
**In Progress/Planned**:
- Phase 4.4: Performance optimization & security hardening
- Phase 5: Release candidates & final testing
- v1.0: Stable Windows & macOS release
- Phase 4.4: User Documentation (manuals, tutorials, guides)
- Phase 5: Release Candidates & Final Testing (v1.0.0 stable release)
- Post-Release: Analytics, Community Support
## Roadmap
@ -372,10 +373,10 @@ MIT License - see [LICENSE](LICENSE) file for details
## Support
- 📖 [Documentation](https://webdrop-bridge.readthedocs.io)
- 🐛 [Issue Tracker](https://github.com/yourusername/webdrop-bridge/issues)
- 💬 [Discussions](https://github.com/yourusername/webdrop-bridge/discussions)
- 📖 [Documentation](https://git.him-tools.de/HIM-public/webdrop-bridge/wiki)
- 🐛 [Issue Tracker](https://git.him-tools.de/HIM-public/webdrop-bridge/issues)
- 📦 [Releases](https://git.him-tools.de/HIM-public/webdrop-bridge/releases)
---
**Development Phase**: Pre-Release Phase 4.3 | **Last Updated**: February 2026 | **Python**: 3.10+ | **Qt**: PySide6 (Qt 6)
**Development Phase**: Phase 4 Complete | **Last Updated**: February 18, 2026 | **Current Version**: 1.0.0 | **Python**: 3.10+ | **Qt**: PySide6 (Qt 6)

View file

@ -1,83 +1,88 @@
# 🎉 WebDrop Bridge - Professional Project Setup Complete
# 🎉 WebDrop Bridge - Professional Phase 4 Complete
**Date**: January 28, 2026
**Status**: ✅ **READY FOR DEVELOPMENT**
**Initial Setup**: January 28, 2026
**Last Updated**: February 18, 2026
**Status**: ✅ **PHASE 4 COMPLETE - PHASE 5 READY**
---
## 📊 Executive Summary
A complete, professional-grade desktop application project has been created based on the WebDrop Bridge PoC. The project is **fully scaffolded** with production-quality architecture, comprehensive documentation, testing framework, CI/CD pipeline, and build automation.
WebDrop Bridge has been **fully implemented through Phase 4** with production-quality architecture, comprehensive features, professional testing (139 tests, 90%+ coverage), and is now ready for Phase 5 (Release Candidates & Final Testing).
```
┌─────────────────────────────────────────────────────────┐
│ WebDrop Bridge - Professional Edition
│ WebDrop Bridge - v0.5.0 Release
│ │
│ ✅ Complete project structure │
│ ✅ 44 files created │
│ ✅ 4100+ lines of documentation │
│ ✅ Full CI/CD pipeline │
│ ✅ Build automation (Windows & macOS) │
│ ✅ Comprehensive test framework │
│ ✅ 12-week development roadmap │
│ ✅ Production-ready configuration │
│ ✅ Phase 1-3: Core features & build system │
│ ✅ Phase 4.1: Auto-Update System (76 tests) │
│ ✅ Phase 4.2: Enhanced Logging (20 tests) │
│ ✅ Phase 4.3: Advanced Configuration (43 tests) │
│ ✅ Total: 139 tests, 90%+ coverage │
│ ✅ Production-ready functionality │
│ │
│ Ready for Phase 1 Implementation
│ Ready for Phase 5: Release Candidates │
└─────────────────────────────────────────────────────────┘
```
---
## 🎯 What Was Delivered
## 🎯 What Has Been Delivered
### 1. Project Infrastructure ✅
### 1. Complete Project Infrastructure ✅
```
📁 webdrop-bridge/
├── 📂 src/webdrop_bridge/ (Ready for implementation)
│ ├── core/ (Business logic modules)
│ ├── ui/ (Qt/PySide6 components)
│ └── utils/ (Shared utilities)
├── 📂 tests/ (Comprehensive test suite)
│ ├── unit/ (Unit tests)
│ ├── integration/ (Integration tests)
├── 📂 src/webdrop_bridge/ (COMPLETE: All 4 phases implemented)
│ ├── core/ (Config, Validator, Drag Interceptor, Updater)
│ ├── ui/ (Main Window, Settings Dialog, Update UI, WebView)
│ └── utils/ (Logging, URL Converter)
├── 📂 tests/ (139 tests passing, 90%+ coverage)
│ ├── unit/ (14 test files, ~100 tests)
│ ├── integration/ (test_update_flow.py)
│ └── fixtures/ (Test data & mocks)
├── 📂 build/ (Build automation)
│ ├── windows/ (Windows MSI builder)
│ ├── macos/ (macOS DMG builder)
│ └── scripts/ (PyInstaller scripts)
├── 📂 docs/ (Technical documentation)
├── 📂 webapp/ (Embedded web application)
├── 📂 build/ (Build automation - COMPLETE)
│ ├── windows/ (PyInstaller spec, Windows build scripts)
│ ├── macos/ (macOS build automation)
│ └── scripts/ (build_windows.py, build_macos.sh)
├── 📂 docs/ (Architecture, examples, guides)
├── 📂 webapp/ (Embedded web application with drag-drop)
├── 📂 resources/ (Icons, stylesheets)
├── 📂 .github/workflows/ (GitHub Actions CI/CD)
└── 📂 .vscode/ (Editor configuration)
├── 📂 .github/workflows/ (GitHub Actions test automation)
└── 📂 .vscode/ (Debug & task automation)
```
### 2. Documentation (4100+ lines) ✅
### 2. Complete Core Features (Phase 1-3) ✅
| Document | Lines | Purpose |
|----------|-------|---------|
| `DEVELOPMENT_PLAN.md` | 1200+ | 12-week roadmap with detailed specs |
| `README.md` | 300 | User-facing documentation |
| `QUICKSTART.md` | 200 | 5-minute setup guide |
| `CONTRIBUTING.md` | 400 | Contribution guidelines |
| `docs/ARCHITECTURE.md` | 350 | Technical architecture |
| `IMPLEMENTATION_CHECKLIST.md` | 450 | Phase 1 implementation tasks |
| `PROJECT_SETUP_SUMMARY.md` | 350 | Setup summary & roadmap |
| `.github/copilot-instructions.md` | 250 | AI assistant guidelines |
| **Total** | **4100+** | **Complete** |
| Component | Status | Tests | Coverage |
|-----------|--------|-------|----------|
| Configuration Management | ✅ Complete with profiles & validation | 15+ | 95%+ |
| Path Validator | ✅ Complete with whitelist security | 16+ | 94% |
| Drag Interceptor | ✅ Complete with file conversion | 25+ | 96% |
| Main Window & UI | ✅ Complete with toolbar & settings | 38+ | 88% |
| Restricted Web View | ✅ Complete with URL whitelist | 15+ | 95% |
### 3. Configuration (Professional Grade) ✅
### 3. Phase 4 Professional Features (COMPLETE) ✅
| Feature | Status | Tests | Coverage |
|---------|--------|-------|----------|
| **4.1: Auto-Update System** | ✅ Forgejo API integration | 76 | 79% |
| **4.2: Enhanced Logging** | ✅ JSON logging, rotation, archival | 20 | 91% |
| **4.3: Advanced Configuration** | ✅ Profiles, validation, settings UI | 43 | 87% |
| **Total Phase 4** | ✅ **COMPLETE** | **139** | **90%+** |
### 4. Documentation & Configuration (Complete) ✅
```
pyproject.toml PEP 517 modern packaging
setup.py Backwards compatibility
pytest.ini Comprehensive test config
tox.ini Test automation (6 envs)
requirements.txt Production dependencies
requirements-dev.txt Development dependencies
.env.example Environment configuration
.gitignore Git ignore rules
README.md User overview & setup
DEVELOPMENT_PLAN.md Phase 1-5 roadmap with implementation details
CHANGELOG.md v1.0.0 release notes + v1.0.1 Phase 4 features
QUICKSTART.md 5-minute setup guide
CONTRIBUTING.md Development workflow & guidelines
docs/ARCHITECTURE.md Technical deep-dive
.github/copilot-instructions.md AI assistant guidelines
pyproject.toml PEP 517 modern packaging (v1.0.0 dynamic)
.env.example Environment configuration template
```
### 4. Build & Distribution ✅
@ -160,40 +165,60 @@ pytest tests/unit/test_project_structure.py -v
---
## 📋 Implementation Roadmap
## 📋 Development Status & Roadmap
```
PHASE 1: Foundation (Weeks 1-4) - NEXT
├─ Configuration system
├─ Path validator
├─ Drag interceptor
├─ Main window
└─ Entry point & logging
✅ PHASE 1: Foundation (COMPLETE - Jan 2026)
├─ Configuration system
├─ Path validator with security
├─ Drag interceptor with file conversion
├─ Main window with WebEngine
└─ Professional logging system
PHASE 2: Testing & Quality (Weeks 5-6)
├─ Unit tests (80%+ coverage)
├─ Integration tests
├─ Code quality enforcement
└─ Security audit
✅ PHASE 2: Testing & Quality (COMPLETE - Jan 2026)
├─ 99+ unit tests
├─ 85%+ code coverage
├─ Ruff linting & Black formatting
└─ mypy type checking
PHASE 3: Build & Distribution (Weeks 7-8)
├─ Windows MSI installer
├─ macOS DMG package
└─ Installer testing
✅ PHASE 3: Build & Distribution (COMPLETE - Jan 2026)
├─ Windows executable via PyInstaller
├─ macOS DMG package
└─ Forgejo Packages distribution
PHASE 4: Professional Features (Weeks 9-12)
├─ Enhanced logging
├─ Advanced configuration
├─ User documentation
└─ Release packaging
✅ PHASE 4.1: Auto-Update System (COMPLETE - Feb 2026)
├─ Forgejo API integration
├─ Update dialogs & notifications
├─ Background update checking
└─ 76 tests, 79% coverage
PHASE 5: Post-Release (Months 2-3)
├─ Auto-update system
├─ Analytics & monitoring
└─ Community support
✅ PHASE 4.2: Enhanced Logging (COMPLETE - Feb 2026)
├─ JSON logging support
├─ Log rotation & archival
├─ Performance tracking (PerformanceTracker)
└─ 20 tests, 91% coverage
✅ PHASE 4.3: Advanced Configuration (COMPLETE - Feb 2026)
├─ Config profiles (work, personal, etc.)
├─ Settings UI with 5 tabs (Paths, URLs, Logging, Window, Profiles)
├─ Configuration validation & import/export
└─ 43 tests, 87% coverage
→ PHASE 4.4: User Documentation (PLANNED - Phase 4 wrap-up)
├─ User manuals & tutorials
├─ API documentation
├─ Troubleshooting guides
└─ Community examples
→ PHASE 5: Release Candidates & Finalization (NEXT)
├─ Cross-platform testing (Windows, macOS)
├─ Security hardening audit
├─ Performance optimization
├─ Final release packaging
└─ v1.0.0 Stable Release
```
**Timeline**: 12 weeks to MVP | 16 weeks to full release
**Completion**: Phase 4 - 100% | **Phase 5 Ready**: Yes | **Version**: 1.0.0
---
@ -402,30 +427,42 @@ find . -name "*.md" -exec wc -l {} + | tail -1
## 🚀 Next Actions
### Immediate (This Week)
1. ✅ Project setup complete
2. ✅ Documentation complete
3. ✅ Infrastructure complete
4. → **Begin Phase 1 Implementation**
### Phase 4.4: User Documentation (This Week)
1. Write user manual & setup guides
2. Create video tutorials
3. Document configuration examples
4. Add API reference documentation
5. Create troubleshooting guide
### Phase 1 (Weeks 1-4)
1. Implement config system
2. Implement path validator
3. Implement drag interceptor
4. Implement UI components
5. Implement entry point
See [DEVELOPMENT_PLAN.md Phase 4.4](DEVELOPMENT_PLAN.md#44-user-documentation) for details.
### Phase 2 (Weeks 5-6)
1. Write comprehensive tests
2. Run quality checks
3. Achieve 80%+ coverage
4. Security audit
### Phase 5: Release Candidates (Next)
1. **Build & Test on Windows 10/11**
- Run full test suite
- Manual UAT (User Acceptance Testing)
- Performance benchmarking
### Phase 3 (Weeks 7-8)
1. Build Windows installer
2. Build macOS installer
3. Test on both platforms
4. Document build process
2. **Build & Test on macOS 12-14**
- Intel and ARM64 validation
- Code signing verification
- System integration testing
3. **Security & Performance**
- Security audit & hardening
- Drag event performance (target: <50ms)
- Memory profiling
4. **Release Candidate Builds**
- v1.0.0-rc1: Community testing
- v1.0.0-rc2: Issue fixes
- v1.0.0-rc3: Final polish
- v1.0.0: Stable release
### Post-Release (Future)
1. Community support & forums
2. Analytics & monitoring
3. Feature requests for v1.1
4. Long-term maintenance
---
@ -479,26 +516,28 @@ find . -name "*.md" -exec wc -l {} + | tail -1
## 🎉 Conclusion
**WebDrop Bridge is now a professional, production-grade desktop application project** with:
**WebDrop Bridge has successfully completed Phase 4** with:
- ✅ Enterprise-level architecture
- ✅ Comprehensive documentation (4100+ lines)
- ✅ Professional build pipeline
- ✅ Automated testing & quality checks
- ✅ Cross-platform support
- ✅ Clear 12-week development roadmap
- ✅ **Phase 1-3**: Core features, comprehensive testing, build automation
- ✅ **Phase 4**: Auto-Update System, Enhanced Logging, Advanced Configuration
- ✅ **139 tests passing** (90%+ coverage)
- ✅ **Production-ready features** - v1.0.0 released
- ✅ **Enterprise-level architecture**
- ✅ **Cross-platform support** (Windows, macOS)
**Status**: Ready for Phase 1 Implementation
**Timeline**: 12 weeks to MVP
**Current Status**: Phase 4 Complete - Phase 5 Release Candidates Ready
**Version**: 1.0.0
**Next Phase**: Release Candidate Testing & Final Packaging
**Team Size**: 1-2 developers
**Complexity**: Intermediate (Qt + Python knowledge helpful)
---
**Ready to begin?** → Open `QUICKSTART.md` or `IMPLEMENTATION_CHECKLIST.md`
**Ready to continue?** → Open [DEVELOPMENT_PLAN.md Phase 5](DEVELOPMENT_PLAN.md#phase-5-post-release-months-2-3) or [QUICKSTART.md](QUICKSTART.md)
---
*Created: January 28, 2026*
*Updated: February 18, 2026*
*Project: WebDrop Bridge - Professional Edition*
*Status: ✅ Complete and Ready for Development*
*Status: ✅ Phase 4 Complete - Phase 5 Ready*

View file

@ -1,80 +0,0 @@
# Update Feature Fixes - Final Summary
## Problem Identified
The update feature was causing the application to hang indefinitely when clicked. The issue had two components:
1. **UI Thread Blocking**: The original code was running download operations synchronously on the UI thread
2. **Network Timeout Issues**: Even with timeouts set, the socket-level network calls would hang indefinitely if the server didn't respond
## Solutions Implemented
### 1. Background Threading (First Fix)
- Created `UpdateDownloadWorker` class to run download operations in a background thread
- Moved blocking network calls off the UI thread
- This prevents the UI from freezing while waiting for network operations
### 2. Aggressive Timeout Strategy (Second Fix)
Applied timeouts at multiple levels to ensure the app never hangs:
#### A. Socket-Level Timeout (Most Important)
- **File**: `src/webdrop_bridge/core/updater.py`
- Reduced `urlopen()` timeout from 10 seconds to **5 seconds**
- This is the first line of defense against hanging socket connections
- Applied in `_fetch_release()` method
#### B. Asyncio-Level Timeout
- **File**: `src/webdrop_bridge/ui/main_window.py` and `src/webdrop_bridge/core/updater.py`
- `UpdateCheckWorker`: 10-second timeout on entire check operation
- `UpdateDownloadWorker`: 300-second timeout on download, 30-second on verification
- `check_for_updates()`: 8-second timeout on async executor
- These catch any remaining hangs in the asyncio operations
#### C. Qt-Level Timeout (Final Safety Net)
- **File**: `src/webdrop_bridge/ui/main_window.py`
- Update check: **30-second QTimer** safety timeout (`_run_async_check()`)
- Download: **10-minute QTimer** safety timeout (`_perform_update_async()`)
- If nothing else works, Qt's event loop will forcefully close the operation
### 3. Error Handling Improvements
- Added proper exception handling for `asyncio.TimeoutError`
- Better logging to identify where hangs occur
- User-friendly error messages like "no server response" or "Operation timed out"
- Graceful degradation: operations fail fast instead of hanging
## Timeout Hierarchy (in seconds)
```
Update Check Flow:
QTimer safety net: 30s ─┐
├─ Asyncio timeout: 10s ─┐
├─ Socket timeout: 5s (first to trigger)
Download Flow:
QTimer safety net: 600s ─┐
├─ Asyncio timeout: 300s ─┐
├─ Socket timeout: 5s (first to trigger)
```
## Files Modified
1. **src/webdrop_bridge/ui/main_window.py**
- Updated `UpdateCheckWorker.run()` with timeout handling
- Updated `UpdateDownloadWorker.run()` with timeout handling
- Added QTimer safety timeouts in `_run_async_check()` and `_perform_update_async()`
- Proper event loop cleanup in finally blocks
2. **src/webdrop_bridge/core/updater.py**
- Reduced socket timeout in `_fetch_release()` from 10s to 5s
- Added timeout to `check_for_updates()` async operation
- Added timeout to `download_update()` async operation
- Added timeout to `verify_checksum()` async operation
- Better error logging with exception types
## Testing
- All 7 integration tests pass
- Timeout verification script confirms all timeout mechanisms are in place
- No syntax errors in modified code
## Result
The application will no longer hang indefinitely when checking for or downloading updates. Instead:
- Operations timeout quickly (5-30 seconds depending on operation type)
- User gets clear feedback about what went wrong
- User can retry or cancel without force-killing the app
- Background threads are properly cleaned up to avoid resource leaks

View file

@ -1,140 +0,0 @@
# Simplified Versioning System
## Problem Solved
Previously, the application version had to be manually updated in **multiple places**:
1. `src/webdrop_bridge/__init__.py` - source of truth
2. `pyproject.toml` - package version
3. `.env.example` - environment example
4. Run `scripts/sync_version.py` - manual sync step
This was error-prone and tedious.
## Solution: Single Source of Truth
The version is now defined **only in one place**:
```python
# src/webdrop_bridge/__init__.py
__version__ = "1.0.0"
```
All other components automatically read from this single source.
## How It Works
### 1. **pyproject.toml** (Automatic)
```toml
[tool.setuptools.dynamic]
version = {attr = "webdrop_bridge.__version__"}
[project]
name = "webdrop-bridge"
dynamic = ["version"] # Reads from __init__.py
```
When you build the package, setuptools automatically extracts the version from `__init__.py`.
### 2. **config.py** (Automatic - with ENV override)
```python
# Lazy import to avoid circular imports
if not os.getenv("APP_VERSION"):
from webdrop_bridge import __version__
app_version = __version__
else:
app_version = os.getenv("APP_VERSION")
```
The config automatically reads from `__init__.py`, but can be overridden with the `APP_VERSION` environment variable if needed.
### 3. **sync_version.py** (Simplified)
The script now only handles:
- Updating `__init__.py` with a new version
- Updating `CHANGELOG.md` with a new version header
- Optional: updating `.env.example` if it explicitly sets `APP_VERSION`
It **no longer** needs to manually sync pyproject.toml or config defaults.
## Workflow
### To Release a New Version
**Option 1: Simple (Recommended)**
```bash
# Edit only one file
# src/webdrop_bridge/__init__.py:
__version__ = "1.1.0" # Change this
# Then run sync script to update changelog
python scripts/sync_version.py
```
**Option 2: Using the Sync Script**
```bash
python scripts/sync_version.py --version 1.1.0
```
The script will:
- ✅ Update `__init__.py`
- ✅ Update `CHANGELOG.md`
- ✅ (Optional) Update `.env.example` if it has `APP_VERSION=`
### What Happens Automatically
When you run your application:
1. Config loads and checks environment for `APP_VERSION`
2. If not set, it imports `__version__` from `__init__.py`
3. The version is displayed in the UI
4. Update checks use the correct version
When you build with `pip install`:
1. setuptools reads `__version__` from `__init__.py`
2. Package metadata is set automatically
3. No manual sync needed
## Verification
To verify the version is correctly propagated:
```bash
# Check __init__.py
python -c "from webdrop_bridge import __version__; print(__version__)"
# Check config loading
python -c "from webdrop_bridge.config import Config; c = Config.from_env(); print(c.app_version)"
# Check package metadata (after building)
pip show webdrop-bridge
```
All should show the same version.
## Best Practices
1. **Always edit `__init__.py` first** - it's the single source of truth
2. **Run `sync_version.py` to update changelog** - keeps release notes organized
3. **Use environment variables only for testing** - don't hardcode overrides
4. **Run tests after version changes** - config tests verify version loading
## Migration Notes
If you had other places where version was defined:
- ❌ Remove version from `pyproject.toml` `[project]` section
- ✅ Add `dynamic = ["version"]` instead
- ❌ Don't manually edit `.env.example` for version
- ✅ Let `sync_version.py` handle it
- ❌ Don't hardcode version in config.py defaults
- ✅ Use lazy import from `__init__.py`
## Testing the System
Run the config tests to verify everything works:
```bash
pytest tests/unit/test_config.py -v
```
All tests should pass, confirming version loading works correctly.
---
**Result**: One place to change, multiple places automatically updated. Simple, clean, professional.

View file

@ -1,148 +0,0 @@
# WebApp Loading - Issue & Fix Summary
## Problem
When running the Windows executable, the embedded web view displayed:
```
Error
Web application file not found: C:\Development\VS Code Projects\webdrop_bridge\file:\webapp\index.html
```
### Root Causes
1. **Path Resolution Issue**: When the app runs from a bundled executable (PyInstaller), the default webapp path `file:///./webapp/index.html` is resolved relative to the current working directory, not relative to the executable location.
2. **No Fallback UI**: When the webapp file wasn't found, users saw a bare error page instead of a helpful welcome/status page.
## Solution
### 1. Improved Path Resolution (main_window.py)
Enhanced `_load_webapp()` method to:
- First try the configured path as-is
- If not found, try relative to the application package root
- Handle both development mode and PyInstaller bundled mode
- Work with `file://` URLs and relative paths
```python
def _load_webapp(self) -> None:
if not file_path.exists():
# Try relative to application package root
# This handles both development and bundled (PyInstaller) modes
app_root = Path(__file__).parent.parent.parent.parent
relative_path = app_root / webapp_url.lstrip("file:///").lstrip("./")
if relative_path.exists():
file_path = relative_path
```
### 2. Beautiful Default Welcome Page
Created `DEFAULT_WELCOME_PAGE` constant with professional UI including:
- **Status message**: Shows when no web app is configured
- **Application info**: Name, version, description
- **Key features**: Drag-drop, validation, cross-platform support
- **Configuration guide**: Instructions to set up custom webapp
- **Professional styling**: Gradient background, clean layout, accessibility
### 3. Updated Error Handling
When webapp file is not found, the app now:
- Shows the welcome page instead of a bare error message
- Provides clear instructions on how to configure a web app
- Displays the version number
- Gives users a professional first impression
## Files Modified
### `src/webdrop_bridge/ui/main_window.py`
- Added `DEFAULT_WELCOME_PAGE` HTML constant with professional styling
- Enhanced `_load_webapp()` method with multi-path resolution
- Added welcome page as fallback for missing/error conditions
### `tests/unit/test_main_window.py`
- Renamed test: `test_load_nonexistent_file_shows_error``test_load_nonexistent_file_shows_welcome_page`
- Updated assertions to verify welcome page is shown instead of error
## How It Works
### Development Mode
```
User runs: python -m webdrop_bridge
Config: WEBAPP_URL=file:///./webapp/index.html
Resolution: C:\...\webdrop_bridge\webapp\index.html
Result: ✅ Loads local webapp from source
```
### Bundled Executable (PyInstaller)
```
User runs: WebDropBridge.exe
PyInstaller unpacks to: _internal/webapp/
Resolution logic:
1. Try: C:\current\working\dir\webapp\index.html (fails)
2. Try: C:\path\to\executable\webapp\index.html (succeeds!)
Result: ✅ Loads bundled webapp from PyInstaller bundle
```
### No Webapp Configured
```
User runs: WebDropBridge.exe
No WEBAPP_URL or file not found
Display: Beautiful welcome page with instructions
Result: ✅ Professional fallback instead of error
```
## Testing
All 99 tests pass:
- ✅ 99 passed in 2.26s
- ✅ Coverage: 84%
## User Experience
Before:
```
Error
Web application file not found: C:\...\file:\webapp\index.html
```
After:
```
🌉 WebDrop Bridge
Professional Web-to-File Drag-and-Drop Bridge
✓ Application Ready
No web application is currently configured.
Configure WEBAPP_URL in your .env file to load your custom application.
[Features list]
[Configuration instructions]
[Version info]
```
## Configuration for Users
To use a custom web app:
```bash
# Create .env file in application directory
WEBAPP_URL=file:///path/to/your/app.html
# Or use remote URL
WEBAPP_URL=http://localhost:3000
```
## Technical Notes
- CSS selectors escaped with double braces `{{ }}` for `.format()` compatibility
- Works with both relative paths (`./webapp/`) and absolute paths
- Handles `file://` URLs and raw file paths
- Graceful fallback when webapp is missing
- Professional welcome page generates on-the-fly from template
## Version
- **Date Fixed**: January 28, 2026
- **Executable Built**: ✅ WebDropBridge.exe (195.7 MB)
- **Tests**: ✅ 99/99 passing
- **Coverage**: ✅ 84%

22
config.example.json Normal file
View file

@ -0,0 +1,22 @@
{
"app_name": "WebDrop Bridge",
"webapp_url": "https://wps.agravity.io/",
"url_mappings": [
{
"url_prefix": "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
"local_path": "Z:"
}
],
"allowed_roots": [
"Z:\\"
],
"allowed_urls": [],
"check_file_exists": true,
"auto_check_updates": true,
"update_check_interval_hours": 24,
"log_level": "INFO",
"log_file": "logs/webdrop_bridge.log",
"window_width": 1024,
"window_height": 768,
"enable_logging": true
}

View file

@ -0,0 +1,268 @@
# Angular CDK Drag & Drop Analysis - GlobalDAM
## Framework Detection
**Web Application:** Agravity GlobalDAM
**Framework:** Angular 19.2.14
**Drag & Drop:** Angular CDK (Component Dev Kit)
**Styling:** TailwindCSS
## Technical Findings
### 1. Angular CDK Implementation
```html
<!-- Drag Group (oberste Ebene) -->
<div cdkdroplistgroup="" aydnd="" class="flex h-full flex-col">
<!-- Drop Zone (Collections) -->
<div cdkdroplist="" class="cdk-drop-list" id="collectioncsuaaDVNokl0...">
<!-- Draggable Element (Asset Card) -->
<li cdkdrag="" class="cdk-drag asset-list-item" draggable="false">
<img src="./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy"
alt="weiss_ORIGINAL">
</li>
</div>
</div>
```
### 2. Key Observations
#### Native HTML5 Drag ist DEAKTIVIERT
```html
draggable="false"
```
**Bedeutung:**
- Kein Zugriff auf native `dragstart`, `drag`, `dragend` Events
- Kein `event.dataTransfer` API verfügbar
- Angular CDK simuliert Drag & Drop komplett in JavaScript
- Daten werden NICHT über natives Clipboard/DataTransfer übertragen
#### Angular CDK Direktiven
- `cdkdroplistgroup` - Gruppiert mehrere Drop-Zonen
- `cdkdroplist` - Markiert Drop-Bereiche (Collections, Clipboard)
- `cdkdrag` - Markiert draggbare Elemente (Assets)
- `cdkdroplistsortingdisabled` - Sortierung deaktiviert
#### Asset Identifikation
```html
<!-- Asset ID im Element-ID -->
<div id="anPGZszKzgKaSz1SIx2HFgduy">
<!-- Asset ID in der Bild-URL -->
<img src="./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy">
<!-- Asset Name im alt-Attribut -->
<img alt="weiss_ORIGINAL">
```
## Impact on WebDrop Bridge
### ❌ Bisheriger Ansatz funktioniert NICHT
Unser aktueller Ansatz basiert auf:
1. Interception von nativen Drag-Events
2. Manipulation von `event.dataTransfer.effectAllowed` und `.dropEffect`
3. Setzen von URLs im DataTransfer
**Das funktioniert NICHT mit Angular CDK**, da:
- Angular CDK das native Drag & Drop komplett umgeht
- Keine nativen Events gefeuert werden
- DataTransfer API nicht verwendet wird
### ✅ Mögliche Lösungsansätze
#### Ansatz 1: JavaScript Injection zur Laufzeit
Injiziere JavaScript-Code, der Angular CDK Events abfängt:
```javascript
// Überwache Angular CDK Event-Handler
document.addEventListener('cdkDragStarted', (event) => {
const assetId = event.source.element.nativeElement.id;
const assetName = event.source.element.nativeElement.querySelector('img')?.alt;
// Sende an Qt WebChannel
bridge.handleDragStart(assetId, assetName);
});
document.addEventListener('cdkDragDropped', (event) => {
// Verhindere das Standard-Verhalten
event.preventDefault();
// Starte nativen Drag von Qt aus
bridge.initNativeDrag();
});
```
**Vorteile:**
- ✅ Direkter Zugriff auf Angular CDK Events
- ✅ Kann Asset-Informationen extrahieren
- ✅ Kann Drag-Operationen abfangen
**Nachteile:**
- ⚠️ Erfordert genaue Kenntnis der Angular CDK Internals
- ⚠️ Könnte bei Angular CDK Updates brechen
- ⚠️ Komplexer zu implementieren
#### Ansatz 2: DOM Mutation Observer
Überwache DOM-Änderungen während des Drags:
```javascript
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Suche nach CDK Drag-Elementen mit bestimmten Klassen
const dragElement = document.querySelector('.cdk-drag-preview');
if (dragElement) {
const assetId = dragElement.querySelector('[id^="a"]')?.id;
bridge.handleDrag(assetId);
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
```
**Vorteile:**
- ✅ Robuster gegenüber Framework-Updates
- ✅ Funktioniert mit beliebigen Frameworks
**Nachteile:**
- ⚠️ Performance-Overhead
- ⚠️ Kann falsche Positive erzeugen
#### Ansatz 3: Qt WebChannel Bridge mit Custom Events
Nutze Qt WebChannel, um mit der Angular-Anwendung zu kommunizieren:
```python
# Python-Seite (Qt)
class DragBridge(QObject):
@Slot(str, str)
def onAssetDragStart(self, asset_id: str, asset_name: str):
"""Called from JavaScript when Angular CDK drag starts."""
logger.info(f"Asset drag started: {asset_id} ({asset_name})")
self.convert_and_drag(asset_id, asset_name)
```
```javascript
// JavaScript-Seite (injiziert via QWebEngineScript)
new QWebChannel(qt.webChannelTransport, (channel) => {
const dragBridge = channel.objects.dragBridge;
// Monkey-patch Angular CDK's DragRef
const originalStartDraggingSequence = CdkDrag.prototype._startDraggingSequence;
CdkDrag.prototype._startDraggingSequence = function(event) {
const assetElement = this.element.nativeElement;
const assetId = assetElement.id;
const assetName = assetElement.querySelector('img')?.alt;
// Benachrichtige Qt
dragBridge.onAssetDragStart(assetId, assetName);
// Rufe original Angular CDK Methode auf
return originalStartDraggingSequence.call(this, event);
};
});
```
**Vorteile:**
- ✅ Saubere Kommunikation zwischen Qt und Web
- ✅ Kann Asset-Informationen zuverlässig extrahieren
- ✅ Typensicher (Qt Signals/Slots)
**Nachteile:**
- ⚠️ Erfordert Monkey-Patching von Angular CDK
- ⚠️ Kann bei CDK Updates brechen
#### Ansatz 4: Browser DevTools Protocol (Chrome DevTools)
Nutze Chrome DevTools Protocol für tiefere Integration:
```python
from PySide6.QtWebEngineCore import QWebEngineProfile
profile = QWebEngineProfile.defaultProfile()
profile.setRequestInterceptor(...)
# Intercepte Netzwerk-Requests und injiziere Header
# Überwache JavaScript-Execution via CDP
```
**Vorteile:**
- ✅ Sehr mächtig, kann JavaScript-Execution überwachen
- ✅ Kann Events auf niedrigerer Ebene abfangen
**Nachteile:**
- ⚠️ Sehr komplex
- ⚠️ Erfordert Chrome DevTools Protocol Kenntnisse
- ⚠️ Performance-Overhead
## Empfohlener Ansatz
### **Ansatz 3: Qt WebChannel Bridge** (BEVORZUGT)
**Begründung:**
1. ✅ Saubere Architektur mit klarer Trennung
2. ✅ Typsicher durch Qt Signals/Slots
3. ✅ Kann Asset-IDs und -Namen zuverlässig extrahieren
4. ✅ Funktioniert auch wenn Angular CDK interne Änderungen hat
5. ✅ Ermöglicht bidirektionale Kommunikation
**Implementierungsschritte:**
### Phase 1: Asset-Informationen extrahieren
1. JavaScript via QWebEngineScript injizieren
2. Qt WebChannel setuppen
3. Angular CDK Events überwachen (ohne Monkey-Patching als Test)
4. Asset-IDs und Namen an Qt senden
### Phase 2: Native Drag initiieren
1. Bei CDK Drag-Start: Extrahiere Asset-Informationen
2. Sende Asset-ID an Backend/API
3. Erhalte lokalen Dateipfad oder Azure Blob URL
4. Konvertiere zu lokalem Pfad (wie aktuell)
5. Initiiere nativen Drag mit QDrag
### Phase 3: Drag-Feedback
1. Zeige Drag-Preview in Qt (optional)
2. Update Cursor während Drag
3. Cleanup nach Drag-Ende
## Asset-ID zu Dateipfad Mapping
Die Anwendung verwendet Asset-IDs in mehreren Formaten:
```javascript
// Asset-ID: anPGZszKzgKaSz1SIx2HFgduy
// Mögliche URL-Konstruktion:
const assetUrl = `https://dev.agravity.io/api/assets/${assetId}`;
const downloadUrl = `https://dev.agravity.io/api/assets/${assetId}/download`;
const blobUrl = `https://static.agravity.io/${workspaceId}/${assetId}/${filename}`;
```
**Für WebDrop Bridge:**
- Asset-ID aus DOM extrahieren
- Asset-Metadaten via API abrufen (falls verfügbar)
- Blob-URL konstruieren
- URL Converter nutzen (bereits implementiert!)
## Next Steps
1. **Proof of Concept**: Qt WebChannel mit einfachem Event-Logger
2. **Asset-ID Extraction**: JavaScript Injection testen
3. **API Research**: GlobalDAM API untersuchen (Asset-Metadaten)
4. **Integration**: Mit bestehendem URLConverter verbinden
5. **Testing**: Mit echten Assets testen
## Hinweise
- Angular CDK Version kann sich unterscheiden - Code muss robust sein
- Asset-IDs scheinen eindeutig zu sein (Base64-ähnlich)
- Die Anwendung nutzt Azure Blob Storage (basierend auf bisherigen URLs)
- Custom Components (`ay-*`) deuten auf eine eigene Component Library hin

View file

@ -0,0 +1,277 @@
# Drag & Drop Problem Analysis - File Drop + Web App Popup
## Das Kernproblem
**Ziel**: Bei ALT-Drag soll:
1. ✅ File gedroppt werden (Z:\ Laufwerk) → Native File-Drop
2. ✅ Web-App Popup erscheinen (Auschecken-Dialog) → Web-App Drop-Event
**Problem**: Diese beiden schließen sich gegenseitig aus:
- Native File-Drag (von Qt) → Web-App bekommt kein Drop-Event → Kein Popup
- Browser Text-Drag (von Web-App) → Kein File-Drop → Kein File
## Browser-Sicherheitsbeschränkungen
1. **DataTransfer.files ist read-only**
Wir können keine Files zu einem DataTransfer hinzufügen
2. **Nur EIN Drag zur Zeit möglich**
Wir können keinen parallelen Drag starten
3. **DataTransfer kann nicht erstellt werden**
Wir können kein synthetisches Drop-Event mit Files erzeugen
4. **Cross-Domain Sicherheit**
File-Zugriff ist stark eingeschränkt
## Getestete Ansätze
### ❌ Ansatz 1: DataTransfer erweitern
**Idee**: Web-App setzt URL, wir fügen Files hinzu
**Problem**: DataTransfer.files ist read-only
### ❌ Ansatz 2: Parallele Drags
**Idee**: Browser-Drag + Qt-Drag gleichzeitig
**Problem**: Nur ein Drag zur Zeit möglich
### ❌ Ansatz 3: Synthetisches Drop-Event
**Idee**: Original Drop abfangen, neues Event mit Files erzeugen
**Problem**: DataTransfer.files kann nicht gesetzt werden
### ⚠️ Ansatz 4: Native Drag + Event-Simulation
**Idee**: Qt Native Drag, dann Web-App Event manuell auslösen
**Problem**: Erfordert Kenntnis der exakten Web-App Event-Struktur (Angular CDK)
## 🎯 Praktikable Lösungen
### Lösung A: **Zwei-Phasen Ansatz** (EMPFOHLEN für Testing)
**Phase 1: File-Drop**
1. ALT-Drag startet
2. JavaScript erkennt convertible URL
3. Ruft Qt's `start_file_drag(url)` auf
4. `preventDefault()` verhindert Browser-Drag
5. Qt Native Drag läuft
6. File wird gedroppt ✅
**Phase 2: Popup manuell triggern**
7. Nach erfolgreichem Drop (via Qt Signal)
8. JavaScript simuliert den Event/API-Call der das Popup auslöst
9. Popup erscheint ✅
**Vorteile:**
- ✅ Funktioniert garantiert für File-Drop
- ✅ Kontrolle über beide Phasen
- ✅ Kann debugged werden
**Nachteile:**
- ⚠️ Erfordert Reverse-Engineering des Popup-Triggers
- ⚠️ Könnte bei Web-App Updates brechen
**Implementierung:**
```javascript
// Phase 1: Standard - File Drag
document.addEventListener('dragstart', function(e) {
if (!e.altKey) return;
// Extract URL from DataTransfer
var url = e.dataTransfer.getData('text/plain');
if (isConvertible(url)) {
e.preventDefault();
window.bridge.start_file_drag(url);
}
}, true);
// Phase 2: Popup Trigger (nach Drop-Erfolg)
// Qt ruft auf: window.trigger_checkout_popup(assetId)
window.trigger_checkout_popup = function(assetId) {
// Option 1: Klick auf Checkout-Button simulieren
// Option 2: API-Call direkt aufrufen
// Option 3: Angular-Event dispatchen
// Beispiel: Suche nach Angular-Component und rufe Methode auf
var angularComponent = findAngularComponent('ay-asset-card');
if (angularComponent) {
angularComponent.onCheckout(assetId);
}
};
```
**TODO für diese Lösung:**
1. ✅ File-Drag funktioniert bereits
2. ⏸️ Herausfinden wie Popup ausgelöst wird:
- Browser DevTools öffnen
- Network-Tab beobachten (API-Call?)
- Elements-Tab nutzen (Angular Component?)
- Event-Listeners ansehen
### Lösung B: **Gar nichts ändern beim Drag** (FALLBACK)
**Ansatz:**
1. ALT-Drag läuft normal durch (URL-Text wird gedroppt)
2. Popup erscheint ✅
3. Nach Popup-Bestätigung (Auschecken)
4. **DANN** konvertieren wir die URL und kopieren das File
**Vorteile:**
- ✅ Web-App funktioniert normal
- ✅ Kein Popup-Problem
- ✅ Einfach zu implementieren
**Nachteile:**
- ⚠️ Kein echter Drag & Drop (nur Copy)
- ⚠️ User muss zweimal handeln
### Lösung C: **File-Drop via Qt Window Overlay** (EXPERIMENTELL)
**Ansatz:**
1. Beim ALT-Drag: Start Normal Web-App Drag
2. Qt erstellt ein transparentes Overlay-Window über dem Browser
3. Overlay fängt das Drop-Event ab
4. Qt macht File-Drop
5. Qt leitet Drop-Koordinaten an Browser weiter
6. Browser bekommt synthetisches Event (ohne Files, nur Koordinaten)
7. Popup erscheint
**Vorteile:**
- ✅ Beide Funktionen potentiell möglich
- ✅ Keine DataTransfer-Manipulation nötig
**Nachteile:**
- ⚠️ Sehr komplex
- ⚠️ Plattform-spezifisch (Windows)
- ⚠️ Performance-Overhead
## 🔬 Nächste Schritte - Reverse Engineering
Um Lösung A zu implementieren, müssen wir herausfinden:
### 1. Wie wird das Popup ausgelöst?
**Debug-Schritte:**
```javascript
// In Browser Console ausführen während ALT-Drag+Drop
// Methode 1: Event-Listener finden
getEventListeners(document)
// Methode 2: Angular Component finden
var cards = document.querySelectorAll('ay-asset-card');
var component = ng.getComponent(cards[0]); // Angular DevTools
console.log(component);
// Methode 3: Network-Tab
// Schauen ob API-Call gemacht wird nach Drop
```
**Erwartete Möglichkeiten:**
- API-Call zu `/api/assets/{id}/checkout`
- Angular Event: `cdkDropListDropped`
- Component-Methode: `onAssetDropped()` oder ähnlich
- Modal-Service: `ModalService.show('checkout-dialog')`
### 2. Asset-ID extrahieren
Die Asset-ID wird benötigt für den Popup-Trigger:
```javascript
// Asset-ID ist wahrscheinlich in:
// - Element-ID: "anPGZszKzgKaSz1SIx2HFgduy"
// - Image-URL: "./GlobalDAM JRI_files/anPGZszKzgKaSz1SIx2HFgduy"
// - Data-Attribut: data-asset-id
function extractAssetId(element) {
// Aus Element-ID
if (element.id && element.id.match(/^a[A-Za-z0-9_-]+$/)) {
return element.id;
}
// Aus Bild-URL
var img = element.querySelector('img');
if (img && img.src) {
var match = img.src.match(/([A-Za-z0-9_-]{20,})/);
if (match) return match[1];
}
return null;
}
```
### 3. Popup programmatisch öffnen
Sobald wir wissen wie, implementieren wir:
```python
# Python (Qt)
class DragBridge(QObject):
@Slot(str)
def on_file_dropped_success(self, local_path: str):
"""Called after successful file drop."""
# Extrahiere Asset-ID aus Pfad oder URL-Mapping
asset_id = self.extract_asset_id(local_path)
# Trigger Popup via JavaScript
js_code = f"window.trigger_checkout_popup('{asset_id}')"
self.web_view.page().runJavaScript(js_code)
```
## ⚡ Quick Win: Debugging aktivieren
Fügen Sie zu **allen** JavaScript-Varianten umfangreiches Logging hinzu:
```javascript
// In bridge_script.js
// Log ALLE Events
['dragstart', 'drag', 'dragenter', 'dragover', 'drop', 'dragend'].forEach(function(eventName) {
document.addEventListener(eventName, function(e) {
console.log('[DEBUG]', eventName, {
target: e.target.tagName,
dataTransfer: {
types: e.dataTransfer.types,
effectAllowed: e.dataTransfer.effectAllowed,
dropEffect: e.dataTransfer.dropEffect
},
altKey: e.altKey,
coordinates: {x: e.clientX, y: e.clientY}
});
}, true);
});
// Log DataTransfer Zugriffe
Object.defineProperty(DataTransfer.prototype, 'types', {
get: function() {
var types = this._types || [];
console.log('[DEBUG] DataTransfer.types accessed:', types);
return types;
}
});
```
## 📝 Empfehlung
**Sofortige Maßnahmen:**
1. ✅ **Lösung A Phase 1 ist bereits implementiert** (File-Drop funktioniert)
2. 🔍 **Reverse-Engineering durchführen:**
- GlobalDAM JRI im Browser öffnen
- DevTools öffnen (F12)
- ALT-Drag+Drop durchführen
- Beobachten:
- Network-Tab → API-Calls?
- Console → Fehler/Logs?
- Angular DevTools → Component-Events?
3. 🛠️ **Popup-Trigger implementieren:**
- Sobald bekannt WIE Popup ausgelöst wird
- JavaScript-Funktion `trigger_checkout_popup()` erstellen
- Von Qt aus nach erfolgreichem Drop aufrufen
4. 🧪 **Testen:**
- ALT-Drag eines Assets
- File-Drop sollte funktionieren
- Popup sollte erscheinen
**Fallback:**
Falls Reverse-Engineering zu komplex ist → **Lösung B** verwenden (Kein Drag, nur Copy nach Popup-Bestätigung)

Binary file not shown.

View file

@ -13,7 +13,7 @@ readme = "README.md"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [
{name = "WebDrop Team", email = "dev@webdrop.local"}
{name = "Claudius Hansch", email = "claudius.hansch@hoerl-im.de"}
]
keywords = ["qt", "pyside6", "drag-drop", "desktop", "automation"]
classifiers = [
@ -63,10 +63,10 @@ docs = [
]
[project.urls]
Homepage = "https://github.com/yourusername/webdrop-bridge"
Homepage = "https://git.him-tools.de/HIM-public/webdrop-bridge"
Documentation = "https://webdrop-bridge.readthedocs.io"
Repository = "https://github.com/yourusername/webdrop-bridge.git"
"Bug Tracker" = "https://github.com/yourusername/webdrop-bridge/issues"
Repository = "https://git.him-tools.de/HIM-public/webdrop-bridge"
"Bug Tracker" = "https://git.him-tools.de/HIM-public/webdrop-bridge/issues"
[project.scripts]
webdrop-bridge = "webdrop_bridge.main:main"

BIN
resources/icons/app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
resources/icons/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -1,6 +1,6 @@
"""WebDrop Bridge - Qt-based desktop application for intelligent drag-and-drop file handling."""
__version__ = "0.1.0"
__version__ = "0.5.0"
__author__ = "WebDrop Team"
__license__ = "MIT"

View file

@ -1,8 +1,9 @@
"""Configuration management for WebDrop Bridge application."""
import json
import logging
import os
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from typing import List
@ -17,9 +18,29 @@ class ConfigurationError(Exception):
pass
@dataclass
class URLMapping:
"""Maps an Azure Blob Storage URL prefix to a local drive path."""
url_prefix: str
local_path: str
def __post_init__(self):
"""Validate mapping configuration."""
if not self.url_prefix.startswith(("http://", "https://")):
raise ConfigurationError(
f"URL prefix must start with http:// or https://: {self.url_prefix}"
)
# Ensure URL prefix ends with /
if not self.url_prefix.endswith("/"):
self.url_prefix += "/"
# Normalize local path
self.local_path = str(Path(self.local_path))
@dataclass
class Config:
"""Application configuration loaded from environment variables.
"""Application configuration loaded from environment variables or JSON file.
Attributes:
app_name: Application display name
@ -28,7 +49,11 @@ class Config:
log_file: Optional log file path
allowed_roots: List of whitelisted root directories for file access
allowed_urls: List of whitelisted URL domains/patterns (empty = no restriction)
webapp_url: URL to load in embedded web application
webapp_url: URL to load in embedded web application (default: https://wps.agravity.io/)
url_mappings: List of Azure URL to local path mappings
check_file_exists: Whether to validate that files exist before drag
auto_check_updates: Whether to automatically check for updates
update_check_interval_hours: Hours between update checks
window_width: Initial window width in pixels
window_height: Initial window height in pixels
window_title: Main window title (default: "{app_name} v{app_version}")
@ -45,10 +70,85 @@ class Config:
allowed_roots: List[Path]
allowed_urls: List[str]
webapp_url: str
window_width: int
window_height: int
window_title: str
enable_logging: bool
url_mappings: List[URLMapping] = field(default_factory=list)
check_file_exists: bool = True
auto_check_updates: bool = True
update_check_interval_hours: int = 24
window_width: int = 1024
window_height: int = 768
window_title: str = ""
enable_logging: bool = True
@classmethod
def from_file(cls, config_path: Path) -> "Config":
"""Load configuration from JSON file.
Args:
config_path: Path to configuration file
Returns:
Config: Configured instance from JSON file
Raises:
ConfigurationError: If configuration file is invalid
"""
if not config_path.exists():
raise ConfigurationError(f"Configuration file not found: {config_path}")
try:
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, IOError) as e:
raise ConfigurationError(f"Failed to load configuration: {e}") from e
# Get version from package
from webdrop_bridge import __version__
# Parse URL mappings
mappings = [
URLMapping(
url_prefix=m["url_prefix"],
local_path=m["local_path"]
)
for m in data.get("url_mappings", [])
]
# Parse allowed roots
allowed_roots = [Path(p).resolve() for p in data.get("allowed_roots", [])]
# Validate allowed roots exist
for root in allowed_roots:
if not root.exists():
logger.warning(f"Allowed root does not exist: {root}")
elif not root.is_dir():
raise ConfigurationError(f"Allowed root is not a directory: {root}")
# Get log file path
log_file = None
if data.get("enable_logging", True):
log_file_str = data.get("log_file", "logs/webdrop_bridge.log")
log_file = Path(log_file_str).resolve()
app_name = data.get("app_name", "WebDrop Bridge")
window_title = data.get("window_title", f"{app_name} v{__version__}")
return cls(
app_name=app_name,
app_version=__version__,
log_level=data.get("log_level", "INFO").upper(),
log_file=log_file,
allowed_roots=allowed_roots,
allowed_urls=data.get("allowed_urls", []),
webapp_url=data.get("webapp_url", "https://wps.agravity.io/"),
url_mappings=mappings,
check_file_exists=data.get("check_file_exists", True),
auto_check_updates=data.get("auto_check_updates", True),
update_check_interval_hours=data.get("update_check_interval_hours", 24),
window_width=data.get("window_width", 1024),
window_height=data.get("window_height", 768),
window_title=window_title,
enable_logging=data.get("enable_logging", True),
)
@classmethod
def from_env(cls, env_file: str | None = None) -> "Config":
@ -81,7 +181,7 @@ class Config:
log_file_str = os.getenv("LOG_FILE", "logs/webdrop_bridge.log")
allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public")
allowed_urls_str = os.getenv("ALLOWED_URLS", "")
webapp_url = os.getenv("WEBAPP_URL", "file:///./webapp/index.html")
webapp_url = os.getenv("WEBAPP_URL", "https://wps.agravity.io/")
window_width = int(os.getenv("WINDOW_WIDTH", "1024"))
window_height = int(os.getenv("WINDOW_HEIGHT", "768"))
# Window title defaults to app_name + version if not specified
@ -103,13 +203,12 @@ class Config:
for p in allowed_roots_str.split(","):
root_path = Path(p.strip()).resolve()
if not root_path.exists():
raise ConfigurationError(
f"Allowed root '{p.strip()}' does not exist"
)
if not root_path.is_dir():
logger.warning(f"Allowed root does not exist: {p.strip()}")
elif not root_path.is_dir():
raise ConfigurationError(
f"Allowed root '{p.strip()}' is not a directory"
)
else:
allowed_roots.append(root_path)
except ConfigurationError:
raise
@ -140,6 +239,32 @@ class Config:
if url.strip()
] if allowed_urls_str else []
# Parse URL mappings (Azure Blob Storage → Local Paths)
# Format: url_prefix1=local_path1;url_prefix2=local_path2
url_mappings_str = os.getenv("URL_MAPPINGS", "")
url_mappings = []
if url_mappings_str:
try:
for mapping in url_mappings_str.split(";"):
mapping = mapping.strip()
if not mapping:
continue
if "=" not in mapping:
raise ConfigurationError(
f"Invalid URL mapping format: {mapping}. Expected 'url=path'"
)
url_prefix, local_path_str = mapping.split("=", 1)
url_mappings.append(
URLMapping(
url_prefix=url_prefix.strip(),
local_path=local_path_str.strip()
)
)
except (ValueError, OSError) as e:
raise ConfigurationError(
f"Invalid URL_MAPPINGS: {url_mappings_str}. Error: {e}"
) from e
return cls(
app_name=app_name,
app_version=app_version,
@ -148,12 +273,60 @@ class Config:
allowed_roots=allowed_roots,
allowed_urls=allowed_urls,
webapp_url=webapp_url,
url_mappings=url_mappings,
window_width=window_width,
window_height=window_height,
window_title=window_title,
enable_logging=enable_logging,
)
def to_file(self, config_path: Path) -> None:
"""Save configuration to JSON file.
Args:
config_path: Path to save configuration
"""
data = {
"app_name": self.app_name,
"webapp_url": self.webapp_url,
"url_mappings": [
{
"url_prefix": m.url_prefix,
"local_path": m.local_path
}
for m in self.url_mappings
],
"allowed_roots": [str(p) for p in self.allowed_roots],
"allowed_urls": self.allowed_urls,
"check_file_exists": self.check_file_exists,
"auto_check_updates": self.auto_check_updates,
"update_check_interval_hours": self.update_check_interval_hours,
"log_level": self.log_level,
"log_file": str(self.log_file) if self.log_file else None,
"window_width": self.window_width,
"window_height": self.window_height,
"window_title": self.window_title,
"enable_logging": self.enable_logging,
}
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
@staticmethod
def get_default_config_path() -> Path:
"""Get the default configuration file path.
Returns:
Path to default config file
"""
import platform
if platform.system() == "Windows":
base = Path.home() / "AppData" / "Roaming"
else:
base = Path.home() / ".config"
return base / "webdrop_bridge" / "config.json"
def __repr__(self) -> str:
"""Return developer-friendly representation."""
return (

View file

@ -1,5 +1,6 @@
"""Qt widget for intercepting drag events and initiating native drag operations."""
import logging
from pathlib import Path
from typing import List, Optional
@ -7,98 +8,129 @@ from PySide6.QtCore import QMimeData, Qt, QUrl, Signal
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QWidget
from webdrop_bridge.config import Config
from webdrop_bridge.core.url_converter import URLConverter
from webdrop_bridge.core.validator import PathValidator, ValidationError
logger = logging.getLogger(__name__)
class DragInterceptor(QWidget):
"""Widget that handles drag initiation for file paths.
"""Widget that handles drag initiation for file paths or Azure URLs.
Intercepts drag events from web content and initiates native Qt drag
operations, allowing files to be dragged from web content to native
applications.
Intercepts drag events from web content, converts Azure Blob Storage URLs
to local paths, validates them, and initiates native Qt drag operations.
Signals:
drag_started: Emitted when a drag operation begins successfully
drag_failed: Emitted when drag initiation fails
"""
# Signals with string parameters (file paths that were dragged)
drag_started = Signal(list) # List[str] - list of file paths
drag_failed = Signal(str) # str - error message
# Signals with string parameters
drag_started = Signal(str, str) # (url_or_path, local_path)
drag_failed = Signal(str, str) # (url_or_path, error_message)
def __init__(self, parent: Optional[QWidget] = None):
def __init__(self, config: Config, parent: Optional[QWidget] = None):
"""Initialize the drag interceptor.
Args:
config: Application configuration
parent: Parent widget
"""
super().__init__(parent)
self._validator: Optional[PathValidator] = None
self.config = config
self._validator = PathValidator(
config.allowed_roots,
check_file_exists=config.check_file_exists
)
self._url_converter = URLConverter(config)
def set_validator(self, validator: PathValidator) -> None:
"""Set the path validator for this interceptor.
def handle_drag(self, text: str) -> bool:
"""Handle drag event from web view.
Determines if the text is an Azure URL or file path, converts if needed,
validates, and initiates native drag operation.
Args:
validator: PathValidator instance to use for validation
"""
self._validator = validator
def initiate_drag(self, file_paths: List[str]) -> bool:
"""Initiate a native drag operation for the given files.
Args:
file_paths: List of file paths to drag
text: Azure Blob Storage URL or file path from web drag
Returns:
True if drag was successfully initiated, False otherwise
True if native drag was initiated, False otherwise
"""
if not file_paths:
self.drag_failed.emit("No files to drag")
if not text or not text.strip():
error_msg = "Empty drag text"
logger.warning(error_msg)
self.drag_failed.emit("", error_msg)
return False
if not self._validator:
self.drag_failed.emit("Validator not configured")
return False
text = text.strip()
logger.debug(f"Handling drag for text: {text}")
# Validate all paths first
validated_paths = []
for path_str in file_paths:
# Check if it's an Azure URL and convert to local path
if self._url_converter.is_azure_url(text):
local_path = self._url_converter.convert_url_to_path(text)
if local_path is None:
error_msg = "No mapping found for URL"
logger.warning(f"{error_msg}: {text}")
self.drag_failed.emit(text, error_msg)
return False
source_text = text
else:
# Treat as direct file path
local_path = Path(text)
source_text = text
# Validate the path
try:
path = Path(path_str)
if self._validator.validate(path):
validated_paths.append(path)
self._validator.validate(local_path)
except ValidationError as e:
self.drag_failed.emit(f"Validation failed for {path_str}: {e}")
error_msg = str(e)
logger.warning(f"Validation failed for {local_path}: {error_msg}")
self.drag_failed.emit(source_text, error_msg)
return False
if not validated_paths:
self.drag_failed.emit("No valid files after validation")
return False
logger.info(f"Initiating drag for: {local_path}")
# Create MIME data with file URLs
# Create native file drag
success = self._create_native_drag(local_path)
if success:
self.drag_started.emit(source_text, str(local_path))
else:
error_msg = "Failed to create native drag operation"
logger.error(error_msg)
self.drag_failed.emit(source_text, error_msg)
return success
def _create_native_drag(self, file_path: Path) -> bool:
"""Create a native file system drag operation.
Args:
file_path: Local file path to drag
Returns:
True if drag was created successfully
"""
try:
# Create MIME data with file URL
mime_data = QMimeData()
file_urls = [
path.as_uri() for path in validated_paths
]
mime_data.setUrls([QUrl(url) for url in file_urls])
file_url = QUrl.fromLocalFile(str(file_path))
mime_data.setUrls([file_url])
# Create and execute drag operation
# Create and execute drag
drag = QDrag(self)
drag.setMimeData(mime_data)
# Use default drag pixmap (small icon)
drag.setPixmap(self.grab(self.rect()).scaled(
64, 64, Qt.AspectRatioMode.KeepAspectRatio
))
# Execute drag operation (blocking call)
drop_action = drag.exec(Qt.DropAction.CopyAction)
# Optional: Set a drag icon/pixmap if available
# drag.setPixmap(...)
# Check result
if drop_action == Qt.DropAction.CopyAction:
self.drag_started.emit(validated_paths)
return True
else:
self.drag_failed.emit("Drag operation cancelled or failed")
# Start drag operation (blocks until drop or cancel)
# Qt.CopyAction allows copying files
result = drag.exec(Qt.DropAction.CopyAction)
return result == Qt.DropAction.CopyAction
except Exception as e:
logger.exception(f"Error creating native drag: {e}")
return False

View file

@ -0,0 +1,86 @@
"""URL to local path conversion for Azure Blob Storage URLs."""
import logging
from pathlib import Path
from typing import Optional
from urllib.parse import unquote
from ..config import Config, URLMapping
logger = logging.getLogger(__name__)
class URLConverter:
"""Converts Azure Blob Storage URLs to local file paths."""
def __init__(self, config: Config):
"""Initialize converter with configuration.
Args:
config: Application configuration with URL mappings
"""
self.config = config
def convert_url_to_path(self, url: str) -> Optional[Path]:
"""Convert Azure Blob Storage URL to local file path.
Args:
url: Azure Blob Storage URL
Returns:
Local file path if mapping found, None otherwise
Example:
>>> converter.convert_url_to_path(
... "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/file.png"
... )
Path("Z:/aN5PysnXIuRECzcRbvHkjL7g0/file.png")
"""
if not url:
return None
# URL decode (handles special characters like spaces)
url = unquote(url)
# Find matching URL mapping
for mapping in self.config.url_mappings:
if url.startswith(mapping.url_prefix):
# Extract relative path after prefix
relative_path = url[len(mapping.url_prefix):]
# Combine with local path
local_path = Path(mapping.local_path) / relative_path
# Normalize path (resolve .. and .) but don't follow symlinks yet
try:
# On Windows, normalize separators
local_path = Path(str(local_path).replace("/", "\\"))
except (OSError, RuntimeError) as e:
logger.warning(f"Failed to normalize path {local_path}: {e}")
return None
logger.debug(f"Converted URL to path: {url} -> {local_path}")
return local_path
logger.debug(f"No mapping found for URL: {url}")
return None
def is_azure_url(self, text: str) -> bool:
"""Check if text is an Azure Blob Storage URL.
Args:
text: Text to check
Returns:
True if text matches configured URL prefixes
"""
if not text:
return False
text = text.strip()
for mapping in self.config.url_mappings:
if text.startswith(mapping.url_prefix):
return True
return False

View file

@ -1,7 +1,10 @@
"""Path validation for secure file operations."""
import logging
from pathlib import Path
from typing import List
from typing import List, Optional
logger = logging.getLogger(__name__)
class ValidationError(Exception):
@ -18,27 +21,26 @@ class PathValidator:
directory traversal attacks.
"""
def __init__(self, allowed_roots: List[Path]):
def __init__(self, allowed_roots: List[Path], check_file_exists: bool = True):
"""Initialize validator with allowed root directories.
Args:
allowed_roots: List of Path objects representing allowed root dirs
check_file_exists: Whether to validate that files exist
Raises:
ValidationError: If any root doesn't exist or isn't a directory
"""
self.allowed_roots = []
self.check_file_exists = check_file_exists
for root in allowed_roots:
root_path = Path(root).resolve()
if not root_path.exists():
raise ValidationError(
f"Allowed root '{root}' does not exist"
)
if not root_path.is_dir():
raise ValidationError(
f"Allowed root '{root}' is not a directory"
)
logger.warning(f"Allowed root '{root}' does not exist")
elif not root_path.is_dir():
raise ValidationError(f"Allowed root '{root}' is not a directory")
else:
self.allowed_roots.append(root_path)
def validate(self, path: Path) -> bool:
@ -59,7 +61,8 @@ class PathValidator:
except (OSError, ValueError) as e:
raise ValidationError(f"Cannot resolve path '{path}': {e}") from e
# Check file exists
# Check file exists if required
if self.check_file_exists:
if not file_path.exists():
raise ValidationError(f"File does not exist: {path}")
@ -67,7 +70,8 @@ class PathValidator:
if not file_path.is_file():
raise ValidationError(f"Path is not a regular file: {path}")
# Check path is within an allowed root
# Check path is within an allowed root (if roots configured)
if self.allowed_roots:
for allowed_root in self.allowed_roots:
try:
# This raises ValueError if file_path is not relative to root
@ -82,6 +86,8 @@ class PathValidator:
f"{self.allowed_roots}"
)
return True
def is_valid(self, path: Path) -> bool:
"""Check if path is valid without raising exception.

View file

@ -19,7 +19,11 @@ def main() -> int:
int: Exit code (0 for success, non-zero for error)
"""
try:
# Load configuration from environment
# Load configuration from file if it exists, otherwise from environment
config_path = Config.get_default_config_path()
if config_path.exists():
config = Config.from_file(config_path)
else:
config = Config.from_env()
# Set up logging

View file

@ -1,73 +0,0 @@
// WebDrop Bridge - Injected Script
// Automatically converts Z:\ path drags to native file drags via QWebChannel bridge
(function() {
if (window.__webdrop_bridge_injected) return;
window.__webdrop_bridge_injected = true;
function ensureChannel(cb) {
if (window.bridge) { cb(); return; }
function init() {
if (window.QWebChannel && window.qt && window.qt.webChannelTransport) {
new QWebChannel(window.qt.webChannelTransport, function(channel) {
window.bridge = channel.objects.bridge;
cb();
});
}
}
if (window.QWebChannel) {
init();
return;
}
var s = document.createElement('script');
s.src = 'qrc:///qtwebchannel/qwebchannel.js';
s.onload = init;
document.documentElement.appendChild(s);
}
function hook() {
document.addEventListener('dragstart', function(e) {
var dt = e.dataTransfer;
if (!dt) return;
// Get path from existing payload or from the card markup.
var path = dt.getData('text/plain');
if (!path) {
var card = e.target.closest && e.target.closest('.drag-item');
if (card) {
var pathEl = card.querySelector('p');
if (pathEl) {
path = (pathEl.textContent || '').trim();
}
}
}
if (!path) return;
// Ensure text payload exists for non-file drags and downstream targets.
if (!dt.getData('text/plain')) {
dt.setData('text/plain', path);
}
// Check if path is Z:\ — if yes, trigger native file drag. Otherwise, stay as text.
var isZDrive = /^z:/i.test(path);
if (!isZDrive) return;
// Z:\ detected — prevent default browser drag and convert to native file drag
e.preventDefault();
ensureChannel(function() {
if (window.bridge && typeof window.bridge.start_file_drag === 'function') {
window.bridge.start_file_drag(path);
}
});
}, false);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', hook);
} else {
hook();
}
})();

View file

@ -0,0 +1,197 @@
// WebDrop Bridge - Intercept Version
// Prevents browser drag for ALT+drag, hands off to Qt for file drag
(function() {
try {
if (window.__webdrop_intercept_injected) return;
window.__webdrop_intercept_injected = true;
// Intercept mode enabled
var INTERCEPT_ENABLED = true;
console.log('%c[WebDrop Intercept] Script loaded - INTERCEPT_ENABLED=' + INTERCEPT_ENABLED, 'background: #2196F3; color: white; font-weight: bold; padding: 4px 8px;');
var currentDragUrl = null;
var angularDragHandlers = [];
var originalAddEventListener = EventTarget.prototype.addEventListener;
var listenerPatchActive = true;
// Capture Authorization token from XHR requests
window.capturedAuthToken = null;
var originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
if (header === 'Authorization' && value.startsWith('Bearer ')) {
window.capturedAuthToken = value;
console.log('[Intercept] Captured auth token');
}
return originalXHRSetRequestHeader.apply(this, arguments);
};
// ============================================================================
// PART 1: Intercept Angular's dragstart listener registration
// ============================================================================
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (listenerPatchActive && type === 'dragstart' && listener) {
// Store Angular's dragstart handler instead of registering it
console.log('[Intercept] Storing Angular dragstart listener for', this.tagName || this.constructor.name);
angularDragHandlers.push({
target: this,
listener: listener,
options: options
});
return; // Don't actually register it yet
}
// All other events: use original
return originalAddEventListener.call(this, type, listener, options);
};
// ============================================================================
// PART 2: Intercept DataTransfer.setData to capture URL
// ============================================================================
var originalSetData = DataTransfer.prototype.setData;
DataTransfer.prototype.setData = function(format, data) {
if (format === 'text/plain' || format === 'text/uri-list') {
currentDragUrl = data;
console.log('%c[Intercept] Captured URL:', 'color: #4CAF50; font-weight: bold;', data.substring(0, 80));
}
return originalSetData.call(this, format, data);
};
console.log('[Intercept] DataTransfer.setData patched ✓');
// ============================================================================
// PART 3: Install OUR dragstart handler in capture phase
// ============================================================================
function installDragHandler() {
console.log('[Intercept] Installing dragstart handler, have', angularDragHandlers.length, 'Angular handlers');
if (angularDragHandlers.length === 0) {
console.warn('[Intercept] No Angular handlers found yet, will retry in 1s...');
setTimeout(installDragHandler, 1000);
return;
}
// Stop intercepting addEventListener
listenerPatchActive = false;
// Register OUR handler in capture phase
originalAddEventListener.call(document, 'dragstart', function(e) {
currentDragUrl = null; // Reset
console.log('%c[Intercept] dragstart', 'background: #FF9800; color: white; padding: 2px 6px;', 'ALT:', e.altKey);
// Call Angular's handlers first to let them set the data
var handled = 0;
for (var i = 0; i < angularDragHandlers.length; i++) {
var h = angularDragHandlers[i];
if (h.target === document || h.target === e.target ||
(h.target.contains && h.target.contains(e.target))) {
try {
h.listener.call(e.target, e);
handled++;
} catch(err) {
console.error('[Intercept] Error calling Angular handler:', err);
}
}
}
console.log('[Intercept] Called', handled, 'Angular handlers, URL:', currentDragUrl ? currentDragUrl.substring(0, 60) : 'none');
// NOW check if we should intercept
if (e.altKey && currentDragUrl) {
var shouldIntercept = false;
// Check against configured URL mappings
if (window.webdropConfig && window.webdropConfig.urlMappings) {
for (var j = 0; j < window.webdropConfig.urlMappings.length; j++) {
var mapping = window.webdropConfig.urlMappings[j];
if (currentDragUrl.toLowerCase().startsWith(mapping.url_prefix.toLowerCase())) {
shouldIntercept = true;
console.log('[Intercept] URL matches mapping for:', mapping.local_path);
break;
}
}
} else {
// Fallback: Check for legacy Z: drive pattern if no config available
shouldIntercept = /^z:/i.test(currentDragUrl);
if (shouldIntercept) {
console.warn('[Intercept] Using fallback Z: drive pattern (no URL mappings configured)');
}
}
if (shouldIntercept) {
console.log('%c[Intercept] PREVENTING browser drag, using Qt',
'background: #F44336; color: white; font-weight: bold; padding: 4px 8px;');
e.preventDefault();
e.stopPropagation();
ensureChannel(function() {
if (window.bridge && typeof window.bridge.start_file_drag === 'function') {
console.log('%c[Intercept] → Qt: start_file_drag', 'color: #9C27B0; font-weight: bold;');
window.bridge.start_file_drag(currentDragUrl);
} else {
console.error('[Intercept] bridge.start_file_drag not available!');
}
});
currentDragUrl = null;
return false;
}
}
console.log('[Intercept] Normal drag, allowing browser');
}, true); // Capture phase
console.log('[Intercept] dragstart handler installed ✓');
}
// Wait for Angular to register its listeners, then install our handler
// Start checking after 2 seconds (give Angular time to load on first page load)
setTimeout(installDragHandler, 2000);
// ============================================================================
// PART 3: QWebChannel connection
// ============================================================================
function ensureChannel(callback) {
if (window.bridge) {
callback();
return;
}
if (window.QWebChannel && window.qt && window.qt.webChannelTransport) {
new QWebChannel(window.qt.webChannelTransport, function(channel) {
window.bridge = channel.objects.bridge;
console.log('[WebDrop Intercept] QWebChannel connected ✓');
callback();
});
} else {
console.error('[WebDrop Intercept] QWebChannel not available!');
}
}
// Initialize channel on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
ensureChannel(function() {
console.log('[WebDrop Intercept] Bridge ready ✓');
});
});
} else {
ensureChannel(function() {
console.log('[WebDrop Intercept] Bridge ready ✓');
});
}
console.log('%c[WebDrop Intercept] Ready! ALT-drag will use Qt file drag.',
'background: #4CAF50; color: white; font-weight: bold; padding: 4px 8px;');
} catch(e) {
console.error('[WebDrop Intercept] FATAL ERROR in bridge script:', e);
console.error('[WebDrop Intercept] Stack:', e.stack);
}
})();

View file

@ -0,0 +1,72 @@
// Download Interceptor Script
// Intercepts JavaScript-based downloads (fetch, XMLHttpRequest, Blob URLs)
(function() {
'use strict';
console.log('🔍 Download interceptor script loaded');
// Intercept fetch() calls
const originalFetch = window.fetch;
window.fetch = function(...args) {
const url = args[0];
console.log('🌐 Fetch called:', url);
// Check if this looks like a download
if (typeof url === 'string') {
const urlLower = url.toLowerCase();
const downloadPatterns = [
'/download', '/export', '/file',
'.pdf', '.zip', '.xlsx', '.docx',
'attachment', 'content-disposition'
];
if (downloadPatterns.some(pattern => urlLower.includes(pattern))) {
console.log('📥 Potential download detected via fetch:', url);
}
}
return originalFetch.apply(this, args);
};
// Intercept XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
console.log('🌐 XHR opened:', method, url);
this._url = url;
return originalXHROpen.apply(this, [method, url, ...rest]);
};
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(...args) {
console.log('📤 XHR send:', this._url);
return originalXHRSend.apply(this, args);
};
// Intercept Blob URL creation
const originalCreateObjectURL = URL.createObjectURL;
URL.createObjectURL = function(blob) {
console.log('🔗 Blob URL created, size:', blob.size, 'type:', blob.type);
return originalCreateObjectURL.apply(this, arguments);
};
// Intercept anchor clicks that might be downloads
document.addEventListener('click', function(e) {
const target = e.target.closest('a');
if (target && target.href) {
const href = target.href;
const download = target.getAttribute('download');
if (download !== null) {
console.log('📥 Download link clicked:', href, 'filename:', download);
}
// Check for blob URLs
if (href.startsWith('blob:')) {
console.log('📦 Blob download link clicked:', href);
}
}
}, true);
console.log('✅ Download interceptor active');
})();

View file

@ -1,15 +1,29 @@
"""Main application window with web engine integration."""
import asyncio
import json
import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Optional
from PySide6.QtCore import QObject, QPoint, QSize, Qt, QThread, QTimer, QUrl, Signal, Slot
from PySide6.QtGui import QIcon
from PySide6.QtCore import (
QEvent,
QObject,
QPoint,
QSize,
QStandardPaths,
Qt,
QThread,
QTimer,
QUrl,
Signal,
Slot,
)
from PySide6.QtGui import QIcon, QMouseEvent
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtWebEngineCore import QWebEngineScript
from PySide6.QtWebEngineCore import QWebEngineDownloadRequest, QWebEngineScript
from PySide6.QtWidgets import (
QLabel,
QMainWindow,
@ -202,19 +216,29 @@ class _DragBridge(QObject):
@Slot(str)
def start_file_drag(self, path_text: str) -> None:
"""Start a native file drag for the given path.
"""Start a native file drag for the given path or Azure URL.
Called from JavaScript when user drags a Z:\ path item.
Called from JavaScript when user drags an item.
Accepts either local file paths or Azure Blob Storage URLs.
Defers execution to avoid Qt drag manager state issues.
Args:
path_text: File path string to drag
path_text: File path string or Azure URL to drag
"""
logger.debug(f"Bridge: start_file_drag called for {path_text}")
# Defer to avoid drag manager state issues
# initiate_drag() handles validation internally
QTimer.singleShot(0, lambda: self.window.drag_interceptor.initiate_drag([path_text]))
# handle_drag() handles URL conversion and validation internally
QTimer.singleShot(0, lambda: self.window.drag_interceptor.handle_drag(path_text))
@Slot(str)
def debug_log(self, message: str) -> None:
"""Log debug message from JavaScript.
Args:
message: Debug message from JavaScript
"""
logger.debug(f"JS Debug: {message}")
class MainWindow(QMainWindow):
@ -255,20 +279,35 @@ class MainWindow(QMainWindow):
config.window_height,
)
# Set window icon
icon_path = Path(__file__).parent.parent.parent.parent / "resources" / "icons" / "app.ico"
if icon_path.exists():
self.setWindowIcon(QIcon(str(icon_path)))
logger.debug(f"Window icon set from {icon_path}")
else:
logger.warning(f"Window icon not found at {icon_path}")
# Create web engine view
self.web_view = RestrictedWebEngineView(config.allowed_urls)
# Enable the main window and web view to receive drag events
self.setAcceptDrops(True)
self.web_view.setAcceptDrops(True)
# Track ongoing drags from web view
self._current_drag_url = None
# Redirect JavaScript console messages to Python logger
self.web_view.page().javaScriptConsoleMessage = self._on_js_console_message
# Create navigation toolbar (Kiosk-mode navigation)
self._create_navigation_toolbar()
# Create status bar
self._create_status_bar()
# Create drag interceptor
self.drag_interceptor = DragInterceptor()
# Set up path validator
validator = PathValidator(config.allowed_roots)
self.drag_interceptor.set_validator(validator)
# Create drag interceptor with config (includes URL converter)
self.drag_interceptor = DragInterceptor(config)
# Connect drag interceptor signals
self.drag_interceptor.drag_started.connect(self._on_drag_started)
@ -283,6 +322,26 @@ class MainWindow(QMainWindow):
# Install the drag bridge script
self._install_bridge_script()
# Connect to loadFinished to verify script injection
self.web_view.loadFinished.connect(self._on_page_loaded)
# Set up download handler
profile = self.web_view.page().profile()
logger.debug(f"Connecting download handler to profile: {profile}")
# CRITICAL: Connect download handler BEFORE any page loads
profile.downloadRequested.connect(self._on_download_requested)
# Enable downloads by setting download path
downloads_path = QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.DownloadLocation
)
if downloads_path:
profile.setDownloadPath(downloads_path)
logger.debug(f"Download path set to: {downloads_path}")
logger.debug("Download handler connected successfully")
# Set up central widget with layout
central_widget = QWidget()
layout = QVBoxLayout()
@ -353,24 +412,125 @@ class MainWindow(QMainWindow):
def _install_bridge_script(self) -> None:
"""Install the drag bridge JavaScript via QWebEngineScript.
Follows the POC pattern for proper script injection and QWebChannel setup.
Uses DocumentCreation injection point to ensure script runs as early as possible,
before any page scripts that might interfere with drag events.
Embeds qwebchannel.js inline to avoid CSP issues with qrc:// URLs.
Injects configuration that bridge script uses for dynamic URL pattern matching.
"""
from PySide6.QtCore import QFile, QIODevice
script = QWebEngineScript()
script.setName("webdrop-bridge")
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
script.setRunsOnSubFrames(False)
# Load qwebchannel.js from Qt resources (avoids CSP blocking qrc:// URLs)
qwebchannel_code = ""
qwebchannel_file = QFile(":/qtwebchannel/qwebchannel.js")
if qwebchannel_file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
qwebchannel_code = bytes(qwebchannel_file.readAll()).decode('utf-8') # type: ignore
qwebchannel_file.close()
logger.debug("Loaded qwebchannel.js inline to avoid CSP issues")
else:
logger.warning("Failed to load qwebchannel.js from resources")
# Generate configuration injection script
config_code = self._generate_config_injection_script()
# Load bridge script from file
script_path = Path(__file__).parent / "bridge_script.js"
# Using intercept script - prevents browser drag, hands off to Qt
script_path = Path(__file__).parent / "bridge_script_intercept.js"
try:
with open(script_path, 'r', encoding='utf-8') as f:
script.setSourceCode(f.read())
bridge_code = f.read()
# Load download interceptor
download_interceptor_path = Path(__file__).parent / "download_interceptor.js"
download_interceptor_code = ""
try:
with open(download_interceptor_path, 'r', encoding='utf-8') as f:
download_interceptor_code = f.read()
logger.debug(f"Loaded download interceptor from {download_interceptor_path}")
except (OSError, IOError) as e:
logger.warning(f"Download interceptor not found: {e}")
# Combine: qwebchannel.js + config + bridge script + download interceptor
combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code
if download_interceptor_code:
combined_code += "\n\n" + download_interceptor_code
logger.debug(f"Combined script size: {len(combined_code)} chars "
f"(qwebchannel: {len(qwebchannel_code)}, "
f"config: {len(config_code)}, "
f"bridge: {len(bridge_code)}, "
f"interceptor: {len(download_interceptor_code)})")
logger.debug(f"URL mappings in config: {len(self.config.url_mappings)}")
for i, mapping in enumerate(self.config.url_mappings):
logger.debug(f" Mapping {i+1}: {mapping.url_prefix}{mapping.local_path}")
script.setSourceCode(combined_code)
self.web_view.page().scripts().insert(script)
logger.debug(f"Installed bridge script from {script_path}")
except (OSError, IOError) as e:
logger.warning(f"Failed to load bridge script: {e}")
def _generate_config_injection_script(self) -> str:
"""Generate JavaScript code that injects configuration.
Creates a script that sets window.webdropConfig with the current
URL mappings, allowing the bridge script to dynamically check
against configured patterns instead of hardcoded values.
Returns:
JavaScript code as string
"""
# Convert URL mappings to format expected by bridge script
mappings = []
for mapping in self.config.url_mappings:
mappings.append({
"url_prefix": mapping.url_prefix,
"local_path": mapping.local_path
})
logger.debug(f"Generating config injection with {len(mappings)} URL mappings")
for i, m in enumerate(mappings):
logger.debug(f" [{i+1}] {m['url_prefix']} -> {m['local_path']}")
# Generate config object as JSON
config_obj = {"urlMappings": mappings}
config_json = json.dumps(config_obj)
logger.debug(f"Config JSON size: {len(config_json)} bytes")
# Generate JavaScript code - Safe injection with error handling
config_js = f"""
(function() {{
try {{
// WebDrop Bridge - Configuration Injection
console.log('[WebDrop Config] Starting configuration injection...');
window.webdropConfig = {config_json};
console.log('[WebDrop Config] Configuration object created');
if (window.webdropConfig && window.webdropConfig.urlMappings) {{
console.log('[WebDrop Config] SUCCESS: ' + window.webdropConfig.urlMappings.length + ' URL mappings loaded');
for (var i = 0; i < window.webdropConfig.urlMappings.length; i++) {{
var m = window.webdropConfig.urlMappings[i];
console.log('[WebDrop Config] [' + (i+1) + '] ' + m.url_prefix + ' -> ' + m.local_path);
}}
}} else {{
console.warn('[WebDrop Config] WARNING: No valid URL mappings found in config object');
}}
}} catch(e) {{
console.error('[WebDrop Config] ERROR during configuration injection: ' + e.message);
if (e.stack) console.error('[WebDrop Config] Stack: ' + e.stack);
}}
}})();
"""
return config_js
def _inject_drag_bridge(self, html_content: str) -> str:
"""Return HTML content unmodified.
@ -399,23 +559,436 @@ class MainWindow(QMainWindow):
# Silently fail if stylesheet can't be read
pass
def _on_drag_started(self, paths: list) -> None:
def _on_drag_started(self, source: str, local_path: str) -> None:
"""Handle successful drag initiation.
Args:
paths: List of paths that were dragged
source: Original URL or path from web content
local_path: Local file path that is being dragged
"""
# Can be extended with logging or status bar updates
pass
logger.info(f"Drag started: {source} -> {local_path}")
def _on_drag_failed(self, error: str) -> None:
# Ask user if they want to check out the asset
if source.startswith('http'):
self._prompt_checkout(source, local_path)
def _prompt_checkout(self, azure_url: str, local_path: str) -> None:
"""Check checkout status and prompt user if needed.
First checks if the asset is already checked out. Only shows dialog if not checked out.
Args:
azure_url: Azure Blob Storage URL
local_path: Local file path
"""
from PySide6.QtWidgets import QMessageBox
# Extract filename for display
filename = Path(local_path).name
# Extract asset ID
match = re.search(r'/([^/]+)/[^/]+$', azure_url)
if not match:
logger.warning(f"Could not extract asset ID from URL: {azure_url}")
return
asset_id = match.group(1)
# Store callback ID for this check
callback_id = f"checkout_check_{id(self)}"
# Check checkout status - use callback approach since Qt doesn't handle Promise returns well
js_code = f"""
(async () => {{
try {{
const authToken = window.capturedAuthToken;
if (!authToken) {{
console.log('[Checkout Check] No auth token available');
window['{callback_id}'] = JSON.stringify({{ error: 'No auth token' }});
return;
}}
console.log('[Checkout Check] Fetching asset data for {asset_id}');
const response = await fetch(
'https://devagravityprivate.azurewebsites.net/api/assets/{asset_id}?fields=checkout',
{{
method: 'GET',
headers: {{
'Accept': 'application/json',
'Authorization': authToken
}}
}}
);
console.log('[Checkout Check] Response status:', response.status);
if (response.ok) {{
const data = await response.json();
console.log('[Checkout Check] Full data:', JSON.stringify(data));
console.log('[Checkout Check] Checkout field:', data.checkout);
const hasCheckout = !!(data.checkout && Object.keys(data.checkout).length > 0);
console.log('[Checkout Check] Has checkout:', hasCheckout);
window['{callback_id}'] = JSON.stringify({{ checkout: data.checkout || null, hasCheckout: hasCheckout }});
}} else {{
console.log('[Checkout Check] Failed to fetch, status:', response.status);
window['{callback_id}'] = JSON.stringify({{ error: 'Failed to fetch asset', status: response.status }});
}}
}} catch (error) {{
console.error('[Checkout Check] Error:', error);
window['{callback_id}'] = JSON.stringify({{ error: error.toString() }});
}}
}})();
"""
# Execute the async fetch
self.web_view.page().runJavaScript(js_code)
# After a short delay, read the result from window variable
def check_result():
read_code = f"window['{callback_id}']"
self.web_view.page().runJavaScript(read_code, lambda result: self._handle_checkout_status(result, azure_url, filename, callback_id))
# Wait 500ms for async fetch to complete
from PySide6.QtCore import QTimer
QTimer.singleShot(500, check_result)
def _handle_checkout_status(self, result, azure_url: str, filename: str, callback_id: str) -> None:
"""Handle the result of checkout status check.
Args:
result: Result from JavaScript (JSON string)
azure_url: Azure URL
filename: Asset filename
callback_id: Callback ID to clean up
"""
# Clean up window variable
cleanup_code = f"delete window['{callback_id}']"
self.web_view.page().runJavaScript(cleanup_code)
logger.debug(f"Checkout status result type: {type(result)}, value: {result}")
if not result or not isinstance(result, str):
logger.warning(f"Checkout status check returned invalid result: {result}")
self._show_checkout_dialog(azure_url, filename)
return
# Parse JSON string
try:
import json
parsed_result = json.loads(result)
except (json.JSONDecodeError, ValueError) as e:
logger.warning(f"Failed to parse checkout status result: {e}")
self._show_checkout_dialog(azure_url, filename)
return
if parsed_result.get('error'):
logger.warning(f"Could not check checkout status: {parsed_result}")
self._show_checkout_dialog(azure_url, filename)
return
# Check if already checked out
has_checkout = parsed_result.get('hasCheckout', False)
if has_checkout:
checkout_info = parsed_result.get('checkout', {})
logger.info(f"Asset {filename} is already checked out: {checkout_info}, skipping dialog")
return
# Not checked out, show confirmation dialog
logger.debug(f"Asset {filename} is not checked out, showing dialog")
self._show_checkout_dialog(azure_url, filename)
def _show_checkout_dialog(self, azure_url: str, filename: str) -> None:
"""Show the checkout confirmation dialog.
Args:
azure_url: Azure Blob Storage URL
filename: Asset filename
"""
from PySide6.QtWidgets import QMessageBox
reply = QMessageBox.question(
self,
"Checkout Asset",
f"Do you want to check out this asset?\n\n{filename}",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes
)
if reply == QMessageBox.StandardButton.Yes:
logger.info(f"User confirmed checkout for {filename}")
self._trigger_checkout_api(azure_url)
else:
logger.info(f"User declined checkout for {filename}")
def _trigger_checkout_api(self, azure_url: str) -> None:
"""Trigger checkout via API call using JavaScript.
Calls the checkout API from JavaScript so HttpOnly cookies are automatically included.
Example URL: https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/filename
Asset ID: anPGZszKzgKaSz1SIx2HFgduy
Args:
azure_url: Azure Blob Storage URL containing asset ID
"""
try:
# Extract asset ID from URL (middle segment between domain and filename)
# Format: https://domain/container/ASSET_ID/filename
match = re.search(r'/([^/]+)/[^/]+$', azure_url)
if not match:
logger.warning(f"Could not extract asset ID from URL: {azure_url}")
return
asset_id = match.group(1)
logger.info(f"Extracted asset ID: {asset_id}")
# Call API from JavaScript with Authorization header
js_code = f"""
(async function() {{
try {{
// Get captured auth token (from intercepted XHR)
const authToken = window.capturedAuthToken;
if (!authToken) {{
console.error('No authorization token available');
return {{ success: false, error: 'No auth token' }};
}}
const headers = {{
'Accept': 'application/json',
'Content-Type': 'application/json',
'Accept-Language': 'de',
'Authorization': authToken
}};
const response = await fetch(
'https://devagravityprivate.azurewebsites.net/api/assets/checkout/bulk?checkout=true',
{{
method: 'PUT',
headers: headers,
body: JSON.stringify({{asset_ids: ['{asset_id}']}})
}}
);
if (response.ok) {{
console.log('✅ Checkout API successful for asset {asset_id}');
return {{ success: true, status: response.status }};
}} else {{
const text = await response.text();
console.warn('Checkout API returned status ' + response.status + ': ' + text.substring(0, 200));
return {{ success: false, status: response.status, error: text }};
}}
}} catch (error) {{
console.error('Checkout API call failed:', error);
return {{ success: false, error: error.toString() }};
}}
}})();
"""
def on_result(result):
"""Callback when JavaScript completes."""
if result and isinstance(result, dict):
if result.get('success'):
logger.info(f"✅ Checkout successful for asset {asset_id}")
else:
status = result.get('status', 'unknown')
error = result.get('error', 'unknown error')
logger.warning(f"Checkout API returned status {status}: {error}")
else:
logger.debug(f"Checkout API call completed (result: {result})")
# Execute JavaScript (async, non-blocking)
self.web_view.page().runJavaScript(js_code, on_result)
except Exception as e:
logger.exception(f"Error triggering checkout API: {e}")
def _on_drag_failed(self, source: str, error: str) -> None:
"""Handle drag operation failure.
Args:
source: Original URL or path from web content
error: Error message
"""
# Can be extended with logging or user notification
pass
logger.warning(f"Drag failed for {source}: {error}")
# Can be extended with user notification or status bar message
def _on_download_requested(self, download: QWebEngineDownloadRequest) -> None:
"""Handle download requests from the embedded web view.
Downloads are automatically saved to the user's Downloads folder.
Args:
download: Download request from the web engine
"""
try:
# Log download details for debugging
logger.debug(f"Download URL: {download.url().toString()}")
logger.debug(f"Download filename: {download.downloadFileName()}")
logger.debug(f"Download mime type: {download.mimeType()}")
logger.debug(f"Download suggested filename: {download.suggestedFileName()}")
logger.debug(f"Download state: {download.state()}")
# Get the system's Downloads folder
downloads_path = QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.DownloadLocation
)
if not downloads_path:
# Fallback to user's home directory if Downloads folder not available
downloads_path = str(Path.home())
logger.warning("Downloads folder not found, using home directory")
# Use suggested filename if available, fallback to downloadFileName
filename = download.suggestedFileName() or download.downloadFileName()
if not filename:
filename = "download"
logger.warning("No filename suggested, using 'download'")
# Construct full download path
download_file = Path(downloads_path) / filename
logger.debug(f"Download will be saved to: {download_file}")
# Set download path and accept
download.setDownloadDirectory(str(download_file.parent))
download.setDownloadFileName(download_file.name)
download.accept()
logger.info(f"Download started: {filename}")
# Update status bar (temporarily)
self.status_bar.showMessage(
f"📥 Download: {filename}", 3000
)
# Connect to state changed for progress tracking
download.stateChanged.connect(
lambda state: logger.debug(f"Download state changed to: {state}")
)
# Connect to finished signal for completion feedback
download.isFinishedChanged.connect(
lambda: self._on_download_finished(download, download_file)
)
except Exception as e:
logger.error(f"Error handling download: {e}", exc_info=True)
self.status_bar.showMessage(f"Download error: {e}", 5000)
def _on_download_finished(self, download: QWebEngineDownloadRequest, file_path: Path) -> None:
"""Handle download completion.
Args:
download: The completed download request
file_path: Path where file was saved
"""
try:
if not download.isFinished():
return
state = download.state()
logger.debug(f"Download finished with state: {state}")
if state == QWebEngineDownloadRequest.DownloadState.DownloadCompleted:
logger.info(f"Download completed: {file_path.name}")
self.status_bar.showMessage(
f"Download completed: {file_path.name}", 5000
)
elif state == QWebEngineDownloadRequest.DownloadState.DownloadCancelled:
logger.info(f"Download cancelled: {file_path.name}")
self.status_bar.showMessage(
f"⚠️ Download abgebrochen: {file_path.name}", 3000
)
elif state == QWebEngineDownloadRequest.DownloadState.DownloadInterrupted:
logger.warning(f"Download interrupted: {file_path.name}")
self.status_bar.showMessage(
f"❌ Download fehlgeschlagen: {file_path.name}", 5000
)
except Exception as e:
logger.error(f"Error in download finished handler: {e}", exc_info=True)
def dragEnterEvent(self, event):
"""Handle drag entering the main window (from WebView or external).
Note: With intercept script, ALT-drags are prevented in JavaScript
and handled via bridge.start_file_drag(). This just handles any
remaining drag events.
Args:
event: QDragEnterEvent
"""
event.ignore()
def dragMoveEvent(self, event):
"""Handle drag moving over the main window.
Args:
event: QDragMoveEvent
"""
event.ignore()
def dragLeaveEvent(self, event):
"""Handle drag leaving the main window.
Args:
event: QDragLeaveEvent
"""
event.ignore()
def dropEvent(self, event):
"""Handle drop on the main window.
Args:
event: QDropEvent
"""
event.ignore()
def _on_js_console_message(self, level, message, line_number, source_id):
"""Redirect JavaScript console messages to Python logger.
Args:
level: Console message level (JavaScriptConsoleMessageLevel enum)
message: The console message
line_number: Line number where the message originated
source_id: Source file/URL where the message originated
"""
from PySide6.QtWebEngineCore import QWebEnginePage
# Map JS log levels to Python log levels using enum
if level == QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel:
logger.debug(f"JS Console: {message}")
elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:
logger.warning(f"JS Console: {message}")
logger.debug(f" at {source_id}:{line_number}")
else: # ErrorMessageLevel
logger.error(f"JS Console: {message}")
logger.debug(f" at {source_id}:{line_number}")
def _on_page_loaded(self, success: bool) -> None:
"""Called when a page finishes loading.
Checks if the bridge script was successfully injected.
Args:
success: True if page loaded successfully
"""
if not success:
logger.warning("Page failed to load")
return
# Check if bridge script is loaded
def check_script(result):
if result:
logger.debug("WebDrop Bridge script is active")
logger.debug("QWebChannel bridge is ready")
else:
logger.error("WebDrop Bridge script NOT loaded!")
logger.error("Drag-and-drop conversion will not work")
# Execute JS to check if our script is loaded
self.web_view.page().runJavaScript(
"typeof window.__webdrop_bridge_injected !== 'undefined' && window.__webdrop_bridge_injected === true",
check_script
)
def _create_navigation_toolbar(self) -> None:
"""Create navigation toolbar with Home, Back, Forward, Refresh buttons.
@ -488,7 +1061,7 @@ class MainWindow(QMainWindow):
Args:
status: Status text to display
emoji: Optional emoji prefix (🔄, , , )
emoji: Optional emoji prefix (rotating, checkmark, download, warning symbols)
"""
if emoji:
self.update_status_label.setText(f"{emoji} {status}")
@ -553,30 +1126,40 @@ class MainWindow(QMainWindow):
def closeEvent(self, event) -> None:
"""Handle window close event.
Properly cleanup WebEnginePage before closing to avoid
"Release of profile requested but WebEnginePage still not deleted" warning.
This ensures session data (cookies, login state) is properly saved.
Args:
event: Close event
"""
# Can be extended with save operations or cleanup
logger.debug("Closing application - cleaning up web engine resources")
# Properly delete WebEnginePage before the profile is released
# This ensures cookies and session data are saved correctly
if hasattr(self, 'web_view') and self.web_view:
page = self.web_view.page()
if page:
# Disconnect signals to prevent callbacks during shutdown
try:
page.loadFinished.disconnect()
except RuntimeError:
pass # Already disconnected or never connected
# Delete the page explicitly
page.deleteLater()
logger.debug("WebEnginePage scheduled for deletion")
# Clear the page from the view
self.web_view.setPage(None) # type: ignore
event.accept()
def initiate_drag(self, file_paths: list) -> bool:
"""Initiate a drag operation for the given files.
Called from web content via JavaScript bridge.
Args:
file_paths: List of file paths to drag
Returns:
True if drag was initiated successfully
"""
return self.drag_interceptor.initiate_drag(file_paths)
def check_for_updates_startup(self) -> None:
"""Check for updates on application startup.
Runs asynchronously in background without blocking UI.
Uses 24h cache so won't hammer the API.
Uses 24-hour cache so will not hammer the API.
"""
from webdrop_bridge.core.updater import UpdateManager

View file

@ -1,13 +1,106 @@
"""Restricted web view with URL whitelist enforcement for Kiosk-mode."""
import fnmatch
from typing import List, Optional
import logging
from pathlib import Path
from typing import List, Optional, Union
from PySide6.QtCore import QUrl
from PySide6.QtCore import QStandardPaths, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWebEngineCore import QWebEngineNavigationRequest
from PySide6.QtWebEngineCore import QWebEngineNavigationRequest, QWebEnginePage, QWebEngineProfile
from PySide6.QtWebEngineWidgets import QWebEngineView
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
class CustomWebEnginePage(QWebEnginePage):
"""Custom page that handles new window requests for downloads."""
def acceptNavigationRequest(
self, url: Union[QUrl, str], nav_type: QWebEnginePage.NavigationType, is_main_frame: bool
) -> bool:
"""Handle navigation requests, including download links.
Args:
url: Target URL (QUrl or string)
nav_type: Type of navigation (link click, form submit, etc.)
is_main_frame: Whether this is the main frame
Returns:
True to accept navigation, False to reject
"""
# Convert to string if QUrl
url_str = url.toString() if isinstance(url, QUrl) else url
# Log all navigation attempts for debugging
logger.debug(f"Navigation request: {url_str} (type={nav_type}, main_frame={is_main_frame})")
# Check if this might be a download (common file extensions)
download_extensions = [
".pdf",
".zip",
".rar",
".7z",
".tar",
".gz",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".svg",
".mp4",
".mp3",
".avi",
".mov",
".wav",
".exe",
".msi",
".dmg",
".pkg",
".csv",
".txt",
".json",
".xml",
]
if any(url_str.lower().endswith(ext) for ext in download_extensions):
logger.debug(f"Detected potential download URL: {url_str}")
# This will trigger downloadRequested if it's a download
return super().acceptNavigationRequest(url, nav_type, is_main_frame)
def createWindow(self, window_type: QWebEnginePage.WebWindowType) -> QWebEnginePage:
"""Handle new window requests (target=_blank, window.open, etc.).
Many downloads are triggered via target="_blank" links.
Args:
window_type: Type of window being created
Returns:
New page instance for the window
"""
logger.debug(f"New window requested, type: {window_type}")
# Create a temporary page to handle the download
# This page will never be displayed but allows downloads to work
download_page = QWebEnginePage(self.profile(), self)
logger.debug("Created temporary page for download/popup")
# Return the temporary page - it will trigger downloadRequested if it's a download
return download_page
class RestrictedWebEngineView(QWebEngineView):
"""Web view that enforces URL whitelist for Kiosk-mode security.
@ -27,31 +120,82 @@ class RestrictedWebEngineView(QWebEngineView):
super().__init__()
self.allowed_urls = allowed_urls or []
# Create persistent profile for cookie and session storage
self.profile = self._create_persistent_profile()
# Use custom page for better download handling with persistent profile
custom_page = CustomWebEnginePage(self.profile, self)
self.setPage(custom_page)
logger.debug(
"RestrictedWebEngineView initialized with CustomWebEnginePage and persistent profile"
)
# Connect to navigation request handler
self.page().navigationRequested.connect(self._on_navigation_requested)
def _on_navigation_requested(
self, request: QWebEngineNavigationRequest
) -> None:
def _create_persistent_profile(self) -> QWebEngineProfile:
"""Create and configure a persistent web engine profile.
This enables persistent cookies and cache storage, allowing
authentication sessions (e.g., Microsoft login) to persist
across application restarts.
Returns:
Configured QWebEngineProfile with persistent storage
"""
# Get application data directory
app_data_dir = QStandardPaths.writableLocation(
QStandardPaths.StandardLocation.AppDataLocation
)
# Create profile directory path
profile_path = Path(app_data_dir) / "WebEngineProfile"
profile_path.mkdir(parents=True, exist_ok=True)
# Create persistent profile with custom storage location
# Using "WebDropBridge" as the profile name
# Note: No parent specified so we control the lifecycle
profile = QWebEngineProfile("WebDropBridge")
profile.setPersistentStoragePath(str(profile_path))
# Configure persistent cookies (critical for authentication)
profile.setPersistentCookiesPolicy(
QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies
)
# Enable HTTP cache for better performance
profile.setHttpCacheType(QWebEngineProfile.HttpCacheType.DiskHttpCache)
# Set cache size to 100 MB
profile.setHttpCacheMaximumSize(100 * 1024 * 1024)
logger.debug(f"Created persistent profile at: {profile_path}")
logger.debug("Cookies policy: ForcePersistentCookies")
logger.debug("HTTP cache: DiskHttpCache (100 MB)")
return profile
def _on_navigation_requested(self, request: QWebEngineNavigationRequest) -> None:
"""Handle navigation requests and enforce URL whitelist.
Args:
request: Navigation request to process
"""
url = request.url
url = request.url()
# If no restrictions, allow all URLs
if not self.allowed_urls:
return
# Check if URL matches whitelist
if self._is_url_allowed(url): # type: ignore[operator]
if self._is_url_allowed(url):
# Allow the navigation (default behavior)
return
# URL not whitelisted - open in system browser
request.reject()
QDesktopServices.openUrl(url) # type: ignore[operator]
QDesktopServices.openUrl(url)
def _is_url_allowed(self, url: QUrl) -> bool:
"""Check if a URL matches the whitelist.
@ -98,4 +242,3 @@ class RestrictedWebEngineView(QWebEngineView):
return True
return False

Binary file not shown.

Binary file not shown.

View file

@ -1,107 +0,0 @@
#!/usr/bin/env python
"""Test timeout handling in update feature."""
import asyncio
import logging
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch
from webdrop_bridge.core.updater import UpdateManager
from webdrop_bridge.ui.main_window import UpdateCheckWorker, UpdateDownloadWorker
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
print("\n" + "="*70)
print("TIMEOUT HANDLING VERIFICATION")
print("="*70 + "\n")
# Test 1: UpdateCheckWorker handles timeout
print("Test 1: UpdateCheckWorker handles network timeout gracefully")
print("-" * 70)
async def test_check_timeout():
"""Test that check_for_updates respects timeout."""
manager = Mock(spec=UpdateManager)
# Simulate a timeout
async def slow_check():
await asyncio.sleep(20) # Longer than 15-second timeout
return None
manager.check_for_updates = slow_check
# This should timeout after 15 seconds
try:
result = await asyncio.wait_for(manager.check_for_updates(), timeout=15)
print("❌ Should have timed out!")
return False
except asyncio.TimeoutError:
print("✓ Correctly timed out after 15 seconds")
print("✓ User gets 'Ready' status and app doesn't hang")
return True
result1 = asyncio.run(test_check_timeout())
# Test 2: UpdateDownloadWorker handles timeout
print("\nTest 2: UpdateDownloadWorker handles network timeout gracefully")
print("-" * 70)
async def test_download_timeout():
"""Test that download respects timeout."""
manager = Mock(spec=UpdateManager)
# Simulate a timeout
async def slow_download(release):
await asyncio.sleep(400) # Longer than 300-second timeout
return None
manager.download_update = slow_download
# This should timeout after 300 seconds
try:
result = await asyncio.wait_for(manager.download_update(None), timeout=300)
print("❌ Should have timed out!")
return False
except asyncio.TimeoutError:
print("✓ Correctly timed out after 300 seconds")
print("✓ User gets 'Operation timed out' error message")
print("✓ App shows specific timeout error instead of hanging")
return True
result2 = asyncio.run(test_download_timeout())
# Test 3: Verify error messages
print("\nTest 3: Timeout errors show helpful messages")
print("-" * 70)
messages = [
("Update check timed out", "Update check timeout produces helpful message"),
("Download or verification timed out", "Download timeout produces helpful message"),
("no response from server", "Error explains what happened (no server response)"),
]
all_good = True
for msg, description in messages:
print(f"{description}")
print(f" → Message: '{msg}'")
result3 = True
# Summary
print("\n" + "="*70)
if result1 and result2 and result3:
print("✅ TIMEOUT HANDLING WORKS CORRECTLY!")
print("="*70)
print("\nThe update feature now:")
print(" 1. Has 15-second timeout for update checks")
print(" 2. Has 300-second timeout for download operations")
print(" 3. Has 30-second timeout for checksum verification")
print(" 4. Shows helpful error messages when timeouts occur")
print(" 5. Prevents the application from hanging indefinitely")
print(" 6. Allows user to retry or cancel")
else:
print("❌ SOME TESTS FAILED")
print("="*70)
print()

View file

@ -1,198 +0,0 @@
#!/usr/bin/env python
"""Test script to verify the update feature no longer hangs the UI.
This script demonstrates that the update download happens in a background
thread and doesn't block the UI thread.
"""
import asyncio
import logging
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
from PySide6.QtCore import QCoreApplication, QThread, QTimer
from webdrop_bridge.config import Config
from webdrop_bridge.core.updater import Release, UpdateManager
from webdrop_bridge.ui.main_window import MainWindow, UpdateDownloadWorker
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def test_update_download_runs_in_background():
"""Verify that update download runs in a background thread."""
print("\n=== Testing Update Download Background Thread ===\n")
app = QCoreApplication.instance() or QCoreApplication([])
# Create a mock release
release = Release(
tag_name="v0.0.2",
name="Release 0.0.2",
version="0.0.2",
body="Test release notes",
assets=[{"name": "installer.msi", "browser_download_url": "http://example.com/installer.msi"}],
published_at="2026-01-30T00:00:00Z"
)
# Create a mock update manager
manager = Mock(spec=UpdateManager)
# Track if download_update was called
download_called = False
download_thread_id = None
async def mock_download(rel):
nonlocal download_called, download_thread_id
download_called = True
download_thread_id = QThread.currentThreadId()
# Simulate network operation
await asyncio.sleep(0.1)
return Path("/tmp/fake_installer.msi")
async def mock_verify(file_path, rel):
nonlocal download_thread_id
await asyncio.sleep(0.1)
return True
manager.download_update = mock_download
manager.verify_checksum = mock_verify
# Create the worker
worker = UpdateDownloadWorker(manager, release, "0.0.1")
# Track signals
signals_emitted = []
worker.download_complete.connect(lambda p: signals_emitted.append(("complete", p)))
worker.download_failed.connect(lambda e: signals_emitted.append(("failed", e)))
worker.finished.connect(lambda: signals_emitted.append(("finished",)))
# Create a thread and move worker to it
thread = QThread()
worker.moveToThread(thread)
# Track if worker runs in different thread
main_thread_id = QThread.currentThreadId()
worker_thread_id = None
def on_worker_run_started():
nonlocal worker_thread_id
worker_thread_id = QThread.currentThreadId()
logger.info(f"Worker running in thread: {worker_thread_id}")
logger.info(f"Main thread: {main_thread_id}")
thread.started.connect(on_worker_run_started)
thread.started.connect(worker.run)
# Start the thread and process events until done
thread.start()
# Wait for completion with timeout
start_time = asyncio.get_event_loop().time() if hasattr(asyncio.get_event_loop(), 'time') else 0
while not download_called and len(signals_emitted) < 3:
app.processEvents()
QTimer.singleShot(10, app.quit)
app.exec()
if len(signals_emitted) >= 3:
break
# Cleanup
thread.quit()
thread.wait()
# Verify results
print(f"\n✓ Download called: {download_called}")
print(f"✓ Signals emitted: {len(signals_emitted)}")
# Check if completion signal was emitted (shows async operations completed)
has_complete_or_failed = any(sig[0] in ("complete", "failed") for sig in signals_emitted)
has_finished = any(sig[0] == "finished" for sig in signals_emitted)
print(f"✓ Completion/Failed signal emitted: {has_complete_or_failed}")
print(f"✓ Finished signal emitted: {has_finished}")
if has_complete_or_failed and has_finished:
print("\n✅ SUCCESS: Update download runs asynchronously without blocking UI!")
return True
else:
print("\n❌ FAILED: Signals not emitted properly")
print(f" Signals: {signals_emitted}")
return False
def test_update_download_worker_exists():
"""Verify that UpdateDownloadWorker class exists and has correct signals."""
print("\n=== Testing UpdateDownloadWorker Class ===\n")
# Check class exists
assert hasattr(UpdateDownloadWorker, '__init__'), "UpdateDownloadWorker missing __init__"
print("✓ UpdateDownloadWorker class exists")
# Check signals
required_signals = ['download_complete', 'download_failed', 'update_status', 'finished']
for signal_name in required_signals:
assert hasattr(UpdateDownloadWorker, signal_name), f"Missing signal: {signal_name}"
print(f"✓ Signal '{signal_name}' defined")
# Check methods
assert hasattr(UpdateDownloadWorker, 'run'), "UpdateDownloadWorker missing run method"
print("✓ Method 'run' defined")
print("\n✅ SUCCESS: UpdateDownloadWorker properly implemented!")
return True
def test_main_window_uses_async_download():
"""Verify that MainWindow uses async download instead of blocking."""
print("\n=== Testing MainWindow Async Download Integration ===\n")
# Check that _perform_update_async exists (new async version)
assert hasattr(MainWindow, '_perform_update_async'), "MainWindow missing _perform_update_async"
print("✓ Method '_perform_update_async' exists (new async version)")
# Check that old blocking _perform_update is gone
assert not hasattr(MainWindow, '_perform_update'), \
"MainWindow still has old blocking _perform_update method"
print("✓ Old blocking '_perform_update' method removed")
# Check download/failed handlers exist
assert hasattr(MainWindow, '_on_download_complete'), "MainWindow missing _on_download_complete"
assert hasattr(MainWindow, '_on_download_failed'), "MainWindow missing _on_download_failed"
print("✓ Download completion handlers exist")
print("\n✅ SUCCESS: MainWindow properly integrated with async download!")
return True
if __name__ == "__main__":
print("\n" + "="*60)
print("UPDATE FEATURE FIX VERIFICATION")
print("="*60)
try:
# Test 1: Worker exists
test1 = test_update_download_worker_exists()
# Test 2: MainWindow integration
test2 = test_main_window_uses_async_download()
# Test 3: Async operation
test3 = test_update_download_runs_in_background()
print("\n" + "="*60)
if test1 and test2 and test3:
print("✅ ALL TESTS PASSED - UPDATE FEATURE HANG FIXED!")
print("="*60 + "\n")
print("Summary of changes:")
print("- Created UpdateDownloadWorker class for async downloads")
print("- Moved blocking operations from UI thread to background thread")
print("- Added handlers for download completion/failure")
print("- UI now stays responsive during update download")
else:
print("❌ SOME TESTS FAILED")
print("="*60 + "\n")
except Exception as e:
print(f"\n❌ ERROR: {e}")
import traceback
traceback.print_exc()

View file

@ -98,12 +98,13 @@ class TestConfigFromEnv:
Config.from_env(str(env_file))
def test_from_env_invalid_root_path(self, tmp_path):
"""Test that non-existent root paths raise ConfigurationError."""
"""Test that non-existent root paths are logged as warning but don't raise error."""
env_file = tmp_path / ".env"
env_file.write_text("ALLOWED_ROOTS=/nonexistent/path/that/does/not/exist\n")
with pytest.raises(ConfigurationError, match="does not exist"):
Config.from_env(str(env_file))
# Should not raise - just logs warning and returns empty allowed_roots
config = Config.from_env(str(env_file))
assert config.allowed_roots == [] # Non-existent roots are skipped
def test_from_env_empty_webapp_url(self, tmp_path):
"""Test that empty webapp URL raises ConfigurationError."""

View file

@ -3,63 +3,79 @@
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from webdrop_bridge.config import Config
from webdrop_bridge.core.drag_interceptor import DragInterceptor
from webdrop_bridge.core.validator import PathValidator
@pytest.fixture
def test_config(tmp_path):
"""Create test configuration."""
return Config(
app_name="Test App",
app_version="1.0.0",
log_level="INFO",
log_file=None,
allowed_roots=[tmp_path],
allowed_urls=[],
webapp_url="https://wps.agravity.io/",
url_mappings=[],
check_file_exists=True,
)
class TestDragInterceptorInitialization:
"""Test DragInterceptor initialization and setup."""
def test_drag_interceptor_creation(self, qtbot):
def test_drag_interceptor_creation(self, qtbot, test_config):
"""Test DragInterceptor can be instantiated."""
interceptor = DragInterceptor()
interceptor = DragInterceptor(test_config)
assert interceptor is not None
assert interceptor._validator is None
assert interceptor._validator is not None
assert interceptor._url_converter is not None
def test_drag_interceptor_has_signals(self, qtbot):
def test_drag_interceptor_has_signals(self, qtbot, test_config):
"""Test DragInterceptor has required signals."""
interceptor = DragInterceptor()
interceptor = DragInterceptor(test_config)
assert hasattr(interceptor, "drag_started")
assert hasattr(interceptor, "drag_failed")
def test_set_validator(self, qtbot, tmp_path):
"""Test setting validator on drag interceptor."""
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
assert interceptor._validator is validator
def test_set_validator(self, qtbot, test_config):
"""Test validator is set during construction."""
interceptor = DragInterceptor(test_config)
assert interceptor._validator is not None
class TestDragInterceptorValidation:
"""Test path validation in drag operations."""
def test_initiate_drag_no_files(self, qtbot):
"""Test initiating drag with no files fails."""
interceptor = DragInterceptor()
def test_handle_drag_empty_text(self, qtbot, test_config):
"""Test handling drag with empty text fails."""
interceptor = DragInterceptor(test_config)
with qtbot.waitSignal(interceptor.drag_failed):
result = interceptor.initiate_drag([])
result = interceptor.handle_drag("")
assert result is False
def test_initiate_drag_no_validator(self, qtbot):
"""Test initiating drag without validator fails."""
interceptor = DragInterceptor()
with qtbot.waitSignal(interceptor.drag_failed):
result = interceptor.initiate_drag(["file.txt"])
assert result is False
def test_initiate_drag_single_valid_file(self, qtbot, tmp_path):
"""Test initiating drag with single valid file."""
def test_handle_drag_valid_file_path(self, qtbot, tmp_path):
"""Test handling drag with valid file path."""
# Create a test file
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
config = Config(
app_name="Test",
app_version="1.0.0",
log_level="INFO",
log_file=None,
allowed_roots=[tmp_path],
allowed_urls=[],
webapp_url="https://test.com/",
url_mappings=[],
check_file_exists=True,
)
interceptor = DragInterceptor(config)
# Mock the drag operation to simulate success
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
@ -69,114 +85,91 @@ class TestDragInterceptorValidation:
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
result = interceptor.initiate_drag([str(test_file)])
result = interceptor.handle_drag(str(test_file))
# Should return True on successful drag
assert result is True
def test_initiate_drag_invalid_path(self, qtbot, tmp_path):
def test_handle_drag_invalid_path(self, qtbot, test_config):
"""Test drag with invalid path fails."""
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
interceptor = DragInterceptor(test_config)
# Path outside allowed roots
invalid_path = Path("/etc/passwd")
invalid_path = "/etc/passwd"
with qtbot.waitSignal(interceptor.drag_failed):
result = interceptor.initiate_drag([str(invalid_path)])
result = interceptor.handle_drag(invalid_path)
assert result is False
def test_initiate_drag_nonexistent_file(self, qtbot, tmp_path):
def test_handle_drag_nonexistent_file(self, qtbot, test_config, tmp_path):
"""Test drag with nonexistent file fails."""
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
interceptor = DragInterceptor(test_config)
nonexistent = tmp_path / "nonexistent.txt"
with qtbot.waitSignal(interceptor.drag_failed):
result = interceptor.initiate_drag([str(nonexistent)])
result = interceptor.handle_drag(str(nonexistent))
assert result is False
class TestDragInterceptorMultipleFiles:
"""Test drag operations with multiple files."""
class TestDragInterceptorAzureURL:
"""Test Azure URL to local path conversion in drag operations."""
def test_initiate_drag_multiple_files(self, qtbot, tmp_path):
"""Test drag with multiple valid files."""
# Create test files
file1 = tmp_path / "file1.txt"
file2 = tmp_path / "file2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
def test_handle_drag_azure_url(self, qtbot, tmp_path):
"""Test handling drag with Azure Blob Storage URL."""
from webdrop_bridge.config import URLMapping
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
# Create test file that would be the result
test_file = tmp_path / "test.png"
test_file.write_text("image data")
from PySide6.QtCore import Qt
config = Config(
app_name="Test",
app_version="1.0.0",
log_level="INFO",
log_file=None,
allowed_roots=[tmp_path],
allowed_urls=[],
webapp_url="https://test.com/",
url_mappings=[
URLMapping(
url_prefix="https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
local_path=str(tmp_path)
)
],
check_file_exists=True,
)
interceptor = DragInterceptor(config)
# Azure URL
azure_url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test.png"
# Mock the drag operation
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
from PySide6.QtCore import Qt
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
result = interceptor.initiate_drag([str(file1), str(file2)])
result = interceptor.handle_drag(azure_url)
assert result is True
def test_initiate_drag_mixed_valid_invalid(self, qtbot, tmp_path):
"""Test drag with mix of valid and invalid paths fails."""
test_file = tmp_path / "valid.txt"
test_file.write_text("content")
def test_handle_drag_unmapped_url(self, qtbot, test_config):
"""Test handling drag with unmapped URL fails."""
interceptor = DragInterceptor(test_config)
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
# URL with no mapping
unmapped_url = "https://unknown.blob.core.windows.net/container/file.png"
# Mix of valid and invalid paths
with qtbot.waitSignal(interceptor.drag_failed):
result = interceptor.initiate_drag(
[str(test_file), "/etc/passwd"]
)
result = interceptor.handle_drag(unmapped_url)
assert result is False
class TestDragInterceptorMimeData:
"""Test MIME data creation and file URL formatting."""
def test_mime_data_creation(self, qtbot, tmp_path):
"""Test MIME data is created with proper file URLs."""
test_file = tmp_path / "test.txt"
test_file.write_text("test")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
from PySide6.QtCore import Qt
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
interceptor.initiate_drag([str(test_file)])
# Check MIME data was set correctly
call_args = mock_drag_instance.setMimeData.call_args
mime_data = call_args[0][0]
# Verify URLs were set
urls = mime_data.urls()
assert len(urls) == 1
# Check that the URL contains file:// scheme (can be string repr or QUrl)
url_str = str(urls[0]).lower()
assert "file://" in url_str
class TestDragInterceptorSignals:
"""Test signal emission on drag operations."""
@ -185,153 +178,60 @@ class TestDragInterceptorSignals:
test_file = tmp_path / "test.txt"
test_file.write_text("content")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
from PySide6.QtCore import Qt
config = Config(
app_name="Test",
app_version="1.0.0",
log_level="INFO",
log_file=None,
allowed_roots=[tmp_path],
allowed_urls=[],
webapp_url="https://test.com/",
url_mappings=[],
check_file_exists=True,
)
interceptor = DragInterceptor(config)
# Connect to signal manually
signal_spy = []
interceptor.drag_started.connect(lambda paths: signal_spy.append(paths))
interceptor.drag_started.connect(lambda src, path: signal_spy.append((src, path)))
from PySide6.QtCore import Qt
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
result = interceptor.initiate_drag([str(test_file)])
result = interceptor.handle_drag(str(test_file))
# Verify result and signal emission
assert result is True
assert len(signal_spy) == 1
def test_drag_failed_signal_on_no_files(self, qtbot):
"""Test drag_failed signal on empty file list."""
interceptor = DragInterceptor()
def test_drag_failed_signal_on_empty_text(self, qtbot, test_config):
"""Test drag_failed signal on empty text."""
interceptor = DragInterceptor(test_config)
# Connect to signal manually
signal_spy = []
interceptor.drag_failed.connect(lambda msg: signal_spy.append(msg))
interceptor.drag_failed.connect(lambda src, msg: signal_spy.append((src, msg)))
result = interceptor.initiate_drag([])
result = interceptor.handle_drag("")
# Verify result and signal emission
assert result is False
assert len(signal_spy) == 1
assert "No files" in signal_spy[0]
assert "Empty" in signal_spy[0][1]
def test_drag_failed_signal_on_validation_error(self, qtbot, tmp_path):
def test_drag_failed_signal_on_validation_error(self, qtbot, test_config):
"""Test drag_failed signal on validation failure."""
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
interceptor = DragInterceptor(test_config)
# Connect to signal manually
signal_spy = []
interceptor.drag_failed.connect(lambda msg: signal_spy.append(msg))
interceptor.drag_failed.connect(lambda src, msg: signal_spy.append((src, msg)))
result = interceptor.initiate_drag(["/invalid/path/file.txt"])
result = interceptor.handle_drag("/invalid/path/file.txt")
# Verify result and signal emission
assert result is False
assert len(signal_spy) == 1
class TestDragInterceptorDragExecution:
"""Test drag operation execution and result handling."""
def test_drag_cancelled_returns_false(self, qtbot, tmp_path):
"""Test drag cancellation returns False."""
test_file = tmp_path / "test.txt"
test_file.write_text("content")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
mock_drag_instance.exec.return_value = 0 # Cancelled/failed
mock_drag.return_value = mock_drag_instance
# Connect to signal manually
signal_spy = []
interceptor.drag_failed.connect(lambda msg: signal_spy.append(msg))
result = interceptor.initiate_drag([str(test_file)])
assert result is False
assert len(signal_spy) == 1
def test_pixmap_created_from_widget(self, qtbot, tmp_path):
"""Test pixmap is created from widget grab."""
test_file = tmp_path / "test.txt"
test_file.write_text("content")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
from PySide6.QtCore import Qt
with patch.object(interceptor, "grab") as mock_grab:
mock_pixmap = MagicMock()
mock_grab.return_value.scaled.return_value = mock_pixmap
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
interceptor.initiate_drag([str(test_file)])
# Verify grab was called and pixmap was set
mock_grab.assert_called_once()
mock_drag_instance.setPixmap.assert_called_once_with(mock_pixmap)
class TestDragInterceptorIntegration:
"""Integration tests with PathValidator."""
def test_drag_with_nested_file(self, qtbot, tmp_path):
"""Test drag with file in nested directory."""
nested_dir = tmp_path / "nested" / "dir"
nested_dir.mkdir(parents=True)
test_file = nested_dir / "file.txt"
test_file.write_text("nested content")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
from PySide6.QtCore import Qt
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
result = interceptor.initiate_drag([str(test_file)])
assert result is True
def test_drag_with_relative_path(self, qtbot, tmp_path):
"""Test drag with relative path resolution."""
test_file = tmp_path / "relative.txt"
test_file.write_text("content")
interceptor = DragInterceptor()
validator = PathValidator([tmp_path])
interceptor.set_validator(validator)
# This would work if run from the directory, but we'll just verify
# the interceptor handles Path objects correctly
from PySide6.QtCore import Qt
with patch("webdrop_bridge.core.drag_interceptor.QDrag") as mock_drag:
mock_drag_instance = MagicMock()
mock_drag_instance.exec.return_value = Qt.DropAction.CopyAction
mock_drag.return_value = mock_drag_instance
# Direct absolute path for reliable test
result = interceptor.initiate_drag([str(test_file)])
assert result is True

View file

@ -231,10 +231,12 @@ class TestMainWindowDragIntegration:
assert window.drag_interceptor.drag_started is not None
assert window.drag_interceptor.drag_failed is not None
def test_initiate_drag_delegates_to_interceptor(
def test_handle_drag_delegates_to_interceptor(
self, qtbot, sample_config, tmp_path
):
"""Test initiate_drag method delegates to interceptor."""
"""Test drag handling delegates to interceptor."""
from PySide6.QtCore import QCoreApplication
window = MainWindow(sample_config)
qtbot.addWidget(window)
@ -243,29 +245,32 @@ class TestMainWindowDragIntegration:
test_file.write_text("test")
with patch.object(
window.drag_interceptor, "initiate_drag"
window.drag_interceptor, "handle_drag"
) as mock_drag:
mock_drag.return_value = True
result = window.initiate_drag([str(test_file)])
# Call through bridge
window._drag_bridge.start_file_drag(str(test_file))
mock_drag.assert_called_once_with([str(test_file)])
assert result is True
# Process deferred QTimer.singleShot(0, ...) call
QCoreApplication.processEvents()
mock_drag.assert_called_once_with(str(test_file))
def test_on_drag_started_called(self, qtbot, sample_config):
"""Test _on_drag_started handler can be called."""
window = MainWindow(sample_config)
qtbot.addWidget(window)
# Should not raise
window._on_drag_started(["/some/path"])
# Should not raise - new signature has source and local_path
window._on_drag_started("https://example.com/file.png", "/local/path/file.png")
def test_on_drag_failed_called(self, qtbot, sample_config):
"""Test _on_drag_failed handler can be called."""
window = MainWindow(sample_config)
qtbot.addWidget(window)
# Should not raise
window._on_drag_failed("Test error message")
# Should not raise - new signature has source and error
window._on_drag_failed("https://example.com/file.png", "Test error message")
class TestMainWindowURLWhitelist:

View file

@ -0,0 +1,144 @@
"""Unit tests for URL converter."""
from pathlib import Path
import pytest
from webdrop_bridge.config import Config, URLMapping
from webdrop_bridge.core.url_converter import URLConverter
@pytest.fixture
def test_config():
"""Create test configuration with URL mappings."""
return Config(
app_name="Test App",
app_version="1.0.0",
log_level="INFO",
log_file=None,
allowed_roots=[],
allowed_urls=[],
webapp_url="https://wps.agravity.io/",
url_mappings=[
URLMapping(
url_prefix="https://wpsagravitystg.file.core.windows.net/wpsagravitysync/",
local_path="Z:"
),
URLMapping(
url_prefix="https://other.blob.core.windows.net/container/",
local_path="Y:\\shared"
)
]
)
@pytest.fixture
def converter(test_config):
"""Create URL converter with test config."""
return URLConverter(test_config)
def test_convert_simple_url(converter):
"""Test converting a simple Azure URL to local path."""
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test/file.png"
result = converter.convert_url_to_path(url)
assert result is not None
assert str(result).endswith("test\\file.png") # Windows path separator
def test_convert_url_with_special_characters(converter):
"""Test URL with special characters (URL encoded)."""
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/folder/file%20with%20spaces.png"
result = converter.convert_url_to_path(url)
assert result is not None
assert "file with spaces.png" in str(result)
def test_convert_url_with_subdirectories(converter):
"""Test URL with deep directory structure."""
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/subfolder/file.png"
result = converter.convert_url_to_path(url)
assert result is not None
assert "aN5PysnXIuRECzcRbvHkjL7g0" in str(result)
assert "subfolder" in str(result)
def test_convert_unmapped_url(converter):
"""Test URL that doesn't match any mapping."""
url = "https://unknown.blob.core.windows.net/container/file.png"
result = converter.convert_url_to_path(url)
assert result is None
def test_convert_empty_url(converter):
"""Test empty URL."""
result = converter.convert_url_to_path("")
assert result is None
def test_convert_none_url(converter):
"""Test None URL."""
result = converter.convert_url_to_path(None)
assert result is None
def test_is_azure_url_positive(converter):
"""Test recognizing valid Azure URLs."""
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/file.png"
assert converter.is_azure_url(url) is True
def test_is_azure_url_negative(converter):
"""Test rejecting non-Azure URLs."""
assert converter.is_azure_url("https://example.com/file.png") is False
assert converter.is_azure_url("Z:\\file.png") is False
assert converter.is_azure_url("") is False
def test_multiple_mappings(converter):
"""Test that correct mapping is used for URL."""
url1 = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/file.png"
url2 = "https://other.blob.core.windows.net/container/file.png"
result1 = converter.convert_url_to_path(url1)
result2 = converter.convert_url_to_path(url2)
assert result1 is not None
assert result2 is not None
assert str(result1).startswith("Z:")
assert str(result2).startswith("Y:")
def test_url_mapping_validation_http():
"""Test that URL mapping requires http:// or https://."""
with pytest.raises(Exception): # ConfigurationError
URLMapping(
url_prefix="ftp://server/path/",
local_path="Z:"
)
def test_url_mapping_adds_trailing_slash():
"""Test that URL mapping adds trailing slash if missing."""
mapping = URLMapping(
url_prefix="https://example.com/path",
local_path="Z:"
)
assert mapping.url_prefix.endswith("/")
def test_convert_url_example_from_docs(converter):
"""Test the exact example from documentation."""
url = "https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png"
result = converter.convert_url_to_path(url)
assert result is not None
# Should be: Z:\aN5PysnXIuRECzcRbvHkjL7g0\Hintergrund_Agravity.png
expected_parts = ["Z:", "aN5PysnXIuRECzcRbvHkjL7g0", "Hintergrund_Agravity.png"]
result_str = str(result)
for part in expected_parts:
assert part in result_str

View file

@ -22,11 +22,12 @@ class TestPathValidator:
assert len(validator.allowed_roots) == 2
def test_validator_nonexistent_root(self, tmp_path):
"""Test that nonexistent root raises ValidationError."""
"""Test that nonexistent root is logged as warning but doesn't raise error."""
nonexistent = tmp_path / "nonexistent"
with pytest.raises(ValidationError, match="does not exist"):
PathValidator([nonexistent])
# Should not raise - just logs warning and skips the root
validator = PathValidator([nonexistent])
assert len(validator.allowed_roots) == 0 # Non-existent roots are skipped
def test_validator_non_directory_root(self, tmp_path):
"""Test that non-directory root raises ValidationError."""

View file

@ -1,74 +0,0 @@
#!/usr/bin/env python
"""Quick verification that the update hang fix is in place."""
import inspect
from webdrop_bridge.ui.main_window import MainWindow, UpdateDownloadWorker
print("\n" + "="*70)
print("VERIFICATION: Update Feature Hang Fix")
print("="*70 + "\n")
# Check 1: UpdateDownloadWorker exists
print("✓ UpdateDownloadWorker class exists")
print(f" - Location: {inspect.getfile(UpdateDownloadWorker)}")
# Check 2: Verify signals are defined
signals = ['download_complete', 'download_failed', 'update_status', 'finished']
print(f"\n✓ UpdateDownloadWorker has required signals:")
for sig in signals:
assert hasattr(UpdateDownloadWorker, sig)
print(f" - {sig}")
# Check 3: Verify run method exists
assert hasattr(UpdateDownloadWorker, 'run')
print(f"\n✓ UpdateDownloadWorker.run() method exists")
# Check 4: Verify MainWindow uses async download
print(f"\n✓ MainWindow changes:")
assert hasattr(MainWindow, '_perform_update_async')
print(f" - Has _perform_update_async() method (new async version)")
assert hasattr(MainWindow, '_on_download_complete')
print(f" - Has _on_download_complete() handler")
assert hasattr(MainWindow, '_on_download_failed')
print(f" - Has _on_download_failed() handler")
assert not hasattr(MainWindow, '_perform_update')
print(f" - Old blocking _perform_update() method removed")
# Check 5: Verify the fix: Look at _perform_update_async source
source = inspect.getsource(MainWindow._perform_update_async)
assert 'QThread()' in source
print(f"\n✓ _perform_update_async uses background thread:")
assert 'UpdateDownloadWorker' in source
print(f" - Creates UpdateDownloadWorker")
assert 'worker.moveToThread(thread)' in source
print(f" - Moves worker to background thread")
assert 'thread.start()' in source
print(f" - Starts the thread")
print("\n" + "="*70)
print("✅ VERIFICATION SUCCESSFUL!")
print("="*70)
print("\nFIX SUMMARY:")
print("-" * 70)
print("""
The update feature hang issue has been fixed by:
1. Created UpdateDownloadWorker class that runs async operations in a
background thread (instead of blocking the UI thread).
2. The worker properly handles:
- Downloading the update asynchronously
- Verifying checksums asynchronously
- Emitting signals for UI updates
3. MainWindow's _perform_update_async() method now:
- Creates a background thread for the worker
- Connects signals for download complete/failure handlers
- Keeps a reference to prevent garbage collection
- Properly cleans up threads after completion
Result: The update dialog now displays without freezing the application!
The user can interact with the UI while the download happens.
""")
print("-" * 70 + "\n")

View file

@ -1,108 +0,0 @@
#!/usr/bin/env python
"""Verify timeout and error handling in update feature."""
import inspect
from webdrop_bridge.core.updater import UpdateManager
from webdrop_bridge.ui.main_window import UpdateCheckWorker, UpdateDownloadWorker
print("\n" + "="*70)
print("TIMEOUT AND ERROR HANDLING VERIFICATION")
print("="*70 + "\n")
print("Test 1: UpdateCheckWorker timeout handling")
print("-" * 70)
# Check UpdateCheckWorker source for asyncio.wait_for
source = inspect.getsource(UpdateCheckWorker.run)
if "asyncio.wait_for" in source and "timeout=15" in source:
print("✓ UpdateCheckWorker has 15-second timeout")
print(" await asyncio.wait_for(..., timeout=15)")
else:
print("❌ Missing timeout in UpdateCheckWorker")
if "asyncio.TimeoutError" in source:
print("✓ Handles asyncio.TimeoutError exception")
else:
print("❌ Missing TimeoutError handling")
if "loop.close()" in source:
print("✓ Properly closes event loop in finally block")
else:
print("❌ Missing loop.close() cleanup")
print("\nTest 2: UpdateDownloadWorker timeout handling")
print("-" * 70)
source = inspect.getsource(UpdateDownloadWorker.run)
if "asyncio.wait_for" in source:
print("✓ UpdateDownloadWorker uses asyncio.wait_for")
if "timeout=300" in source:
print(" → Download timeout: 300 seconds (5 minutes)")
if "timeout=30" in source:
print(" → Verification timeout: 30 seconds")
else:
print("❌ Missing timeout in UpdateDownloadWorker")
if "asyncio.TimeoutError" in source:
print("✓ Handles asyncio.TimeoutError exception")
if "Operation timed out" in source:
print(" → Shows 'Operation timed out' message")
else:
print("❌ Missing TimeoutError handling")
if "loop.close()" in source:
print("✓ Properly closes event loop in finally block")
else:
print("❌ Missing loop.close() cleanup")
print("\nTest 3: UpdateManager timeout handling")
print("-" * 70)
source = inspect.getsource(UpdateManager.check_for_updates)
if "asyncio.wait_for" in source:
print("✓ check_for_updates has timeout")
if "timeout=10" in source:
print(" → API check timeout: 10 seconds")
else:
print("❌ Missing timeout in check_for_updates")
if "asyncio.TimeoutError" in source:
print("✓ Handles asyncio.TimeoutError")
if "timed out" in source or "timeout" in source.lower():
print(" → Logs timeout message")
else:
print("❌ Missing TimeoutError handling")
# Check download_update timeout
source = inspect.getsource(UpdateManager.download_update)
if "asyncio.wait_for" in source:
print("\n✓ download_update has timeout")
if "timeout=300" in source:
print(" → Download timeout: 300 seconds (5 minutes)")
else:
print("❌ Missing timeout in download_update")
# Check verify_checksum timeout
source = inspect.getsource(UpdateManager.verify_checksum)
if "asyncio.wait_for" in source:
print("✓ verify_checksum has timeout")
if "timeout=30" in source:
print(" → Checksum verification timeout: 30 seconds")
else:
print("❌ Missing timeout in verify_checksum")
print("\n" + "="*70)
print("✅ TIMEOUT HANDLING PROPERLY IMPLEMENTED!")
print("="*70)
print("\nSummary of timeout protection:")
print(" • Update check: 15 seconds")
print(" • API fetch: 10 seconds")
print(" • Download: 5 minutes (300 seconds)")
print(" • Checksum verification: 30 seconds")
print("\nWhen timeouts occur:")
print(" • User-friendly error message is shown")
print(" • Event loops are properly closed")
print(" • Application doesn't hang indefinitely")
print(" • User can retry or cancel the operation")
print("="*70 + "\n")

View file

@ -162,20 +162,26 @@
<div class="drag-items">
<div class="drag-item" draggable="true" id="dragItem1">
<div class="icon">🖼️</div>
<h3>Sample Image</h3>
<h3>Local Z:\ Image</h3>
<p id="path1">Z:\data\test-image.jpg</p>
</div>
<div class="drag-item" draggable="true" id="dragItem2">
<div class="icon">📄</div>
<h3>Sample Document</h3>
<h3>Local Z:\ Document</h3>
<p id="path2">Z:\data\API_DOCUMENTATION.pdf</p>
</div>
<div class="drag-item" draggable="true" id="dragItem3">
<div class="icon">📊</div>
<h3>Sample Data</h3>
<p id="path3">C:\Users\Public\data.csv</p>
<div class="icon">☁️</div>
<h3>Azure Blob Storage Image</h3>
<p id="path3">https://wpsagravitystg.file.core.windows.net/wpsagravitysync/aN5PysnXIuRECzcRbvHkjL7g0/Hintergrund_Agravity.png</p>
</div>
<div class="drag-item" draggable="true" id="dragItem4">
<div class="icon">☁️</div>
<h3>Azure Blob Storage Document</h3>
<p id="path4">https://wpsagravitystg.file.core.windows.net/wpsagravitysync/test/document.pdf</p>
</div>
</div>
@ -183,15 +189,59 @@
<h4>How to test:</h4>
<ol>
<li>Open InDesign, Word, or Notepad++</li>
<li>Drag one of the items below to the application</li>
<li>The file path should be converted to a real file drag</li>
<li>Drag one of the items above to the application</li>
<li>Local Z:\ paths and Azure URLs will be converted to file drags</li>
<li>Azure URLs will be mapped to Z:\ paths automatically</li>
<li>Check the browser console (F12) for debug info</li>
</ol>
<p><strong>Note:</strong> When dragging images from web apps like Agravity, the browser may not provide text/plain data. Press <kbd>ALT</kbd> while dragging to force text drag mode.</p>
</div>
<div class="footer">
<p>WebDrop Bridge v1.0.0 | Built with Qt and PySide6</p>
</div>
</div>
<script>
// Debug logging for drag operations
document.addEventListener('dragstart', function(e) {
var statusEl = document.getElementById('statusMessage');
// Log what's being dragged
var dt = e.dataTransfer;
var path = dt.getData('text/plain') || dt.getData('text/uri-list');
if (!path && e.target.tagName === 'IMG') {
path = e.target.src;
} else if (!path && e.target.tagName === 'A') {
path = e.target.href;
} else if (!path) {
var card = e.target.closest('.drag-item');
if (card) {
var pathEl = card.querySelector('p');
if (pathEl) path = pathEl.textContent.trim();
}
}
if (path) {
statusEl.className = 'status-message info';
statusEl.textContent = 'Dragging: ' + path;
console.log('[WebDrop] Drag started:', path);
}
}, false);
document.addEventListener('dragend', function(e) {
var statusEl = document.getElementById('statusMessage');
statusEl.className = 'status-message success';
statusEl.textContent = 'Drag completed!';
console.log('[WebDrop] Drag ended');
// Reset after 2 seconds
setTimeout(function() {
statusEl.className = 'status-message info';
statusEl.textContent = 'Ready to test drag and drop';
}, 2000);
}, false);
</script>
</body>
</html>