feat: Implement default welcome page for missing web application

- Added a professional HTML welcome page displayed when no web application is configured.
- Enhanced `_load_webapp()` method to support improved path resolution for both development and bundled modes.
- Updated error handling to show the welcome page instead of a bare error message when the webapp file is not found.
- Modified unit tests to verify the welcome page is displayed in error scenarios.

build: Complete Windows and macOS build scripts

- Created `build_windows.py` for building Windows executable and optional MSI installer using PyInstaller.
- Developed `build_macos.sh` for creating macOS application bundle and DMG image.
- Added logging and error handling to build scripts for better user feedback.

docs: Add build and icon requirements documentation

- Created `PHASE_3_BUILD_SUMMARY.md` detailing the build process, results, and next steps.
- Added `resources/icons/README.md` outlining icon requirements and creation guidelines.

chore: Sync remotes script for repository maintenance

- Introduced `sync_remotes.ps1` PowerShell script to fetch updates from origin and upstream remotes.
This commit is contained in:
claudi 2026-01-28 12:59:33 +01:00
parent 90dc09eb4d
commit f0c96f15b8
10 changed files with 1415 additions and 39 deletions

View file

@ -0,0 +1,295 @@
#!/bin/bash
# Build macOS DMG package using PyInstaller
#
# This script builds the WebDrop Bridge application for macOS and creates
# a distributable DMG image.
#
# Requirements:
# - PyInstaller 6.0+
# - Python 3.10+
# - Xcode Command Line Tools (for code signing)
# - create-dmg (optional, for custom DMG: brew install create-dmg)
#
# Usage:
# bash build_macos.sh [--sign] [--notarize]
set -e # Exit on error
# Configuration
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BUILD_DIR="$PROJECT_ROOT/build"
DIST_DIR="$BUILD_DIR/dist/macos"
TEMP_BUILD="$BUILD_DIR/temp/macos"
SPECS_DIR="$BUILD_DIR/specs"
SPEC_FILE="$BUILD_DIR/webdrop_bridge.spec"
APP_NAME="WebDropBridge"
DMG_VOLUME_NAME="WebDrop Bridge"
VERSION="1.0.0"
# Parse arguments
SIGN_APP=0
NOTARIZE_APP=0
while [[ $# -gt 0 ]]; do
case $1 in
--sign)
SIGN_APP=1
shift
;;
--notarize)
NOTARIZE_APP=1
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}${NC} $1"
}
log_success() {
echo -e "${GREEN}${NC} $1"
}
log_warning() {
echo -e "${YELLOW}⚠️${NC} $1"
}
log_error() {
echo -e "${RED}${NC} $1"
}
# Main build function
main() {
echo "=========================================="
echo "🚀 WebDrop Bridge macOS Build"
echo "=========================================="
echo ""
# Check prerequisites
check_prerequisites
# Clean previous builds
clean_builds
# Build with PyInstaller
build_executable
# Create DMG
create_dmg
# Optional: Sign and notarize
if [ $SIGN_APP -eq 1 ]; then
sign_app
fi
if [ $NOTARIZE_APP -eq 1 ]; then
notarize_app
fi
echo ""
echo "=========================================="
log_success "Build completed successfully"
echo "=========================================="
echo ""
log_info "Output: $DIST_DIR/"
ls -lh "$DIST_DIR"
}
check_prerequisites() {
log_info "Checking prerequisites..."
# Check Python
if ! command -v python3 &> /dev/null; then
log_error "Python 3 not found"
exit 1
fi
log_success "Python 3 found: $(python3 --version)"
# Check PyInstaller
if ! python3 -m pip show pyinstaller &> /dev/null; then
log_error "PyInstaller not installed. Run: pip install pyinstaller"
exit 1
fi
log_success "PyInstaller installed"
# Check spec file
if [ ! -f "$SPEC_FILE" ]; then
log_error "Spec file not found: $SPEC_FILE"
exit 1
fi
log_success "Spec file found"
echo ""
}
clean_builds() {
log_info "Cleaning previous builds..."
for dir in "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"; do
if [ -d "$dir" ]; then
rm -rf "$dir"
log_success "Removed $dir"
fi
done
mkdir -p "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"
echo ""
}
build_executable() {
log_info "Building macOS executable with PyInstaller..."
echo ""
python3 -m PyInstaller \
--distpath="$DIST_DIR" \
--buildpath="$TEMP_BUILD" \
--specpath="$SPECS_DIR" \
"$SPEC_FILE"
if [ ! -d "$DIST_DIR/$APP_NAME.app" ]; then
log_error "Application bundle not created"
exit 1
fi
log_success "Application bundle built successfully"
log_info "Output: $DIST_DIR/$APP_NAME.app"
echo ""
}
create_dmg() {
log_info "Creating DMG package..."
echo ""
DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg"
# Remove existing DMG
if [ -f "$DMG_FILE" ]; then
rm -f "$DMG_FILE"
fi
# Check if create-dmg is available
if command -v create-dmg &> /dev/null; then
log_info "Using create-dmg for professional DMG..."
create-dmg \
--volname "$DMG_VOLUME_NAME" \
--icon-size 128 \
--window-size 512 400 \
--app-drop-link 380 200 \
"$DMG_FILE" \
"$DIST_DIR/$APP_NAME.app"
else
log_warning "create-dmg not found, using hdiutil (less stylish)"
log_info "For professional DMG: brew install create-dmg"
# Create temporary DMG directory structure
DMG_TEMP="$TEMP_BUILD/dmg_contents"
mkdir -p "$DMG_TEMP"
# Copy app bundle
cp -r "$DIST_DIR/$APP_NAME.app" "$DMG_TEMP/"
# Create symlink to Applications folder
ln -s /Applications "$DMG_TEMP/Applications"
# Create DMG
hdiutil create \
-volname "$DMG_VOLUME_NAME" \
-srcfolder "$DMG_TEMP" \
-ov \
-format UDZO \
"$DMG_FILE"
# Clean up
rm -rf "$DMG_TEMP"
fi
if [ ! -f "$DMG_FILE" ]; then
log_error "DMG file not created"
exit 1
fi
# Get file size
SIZE=$(du -h "$DMG_FILE" | cut -f1)
log_success "DMG created successfully"
log_info "Output: $DMG_FILE (Size: $SIZE)"
echo ""
}
sign_app() {
log_info "Signing application..."
echo ""
# Get signing identity from environment or use ad-hoc
SIGNING_ID="${APPLE_SIGNING_ID:--}"
codesign \
--deep \
--force \
--verify \
--verbose \
--options=runtime \
--sign "$SIGNING_ID" \
"$DIST_DIR/$APP_NAME.app"
if [ $? -ne 0 ]; then
log_error "Code signing failed"
exit 1
fi
log_success "Application signed successfully"
echo ""
}
notarize_app() {
log_info "Notarizing application..."
echo ""
# Requires:
# - APPLE_ID environment variable
# - APPLE_PASSWORD environment variable (app-specific password)
# - APPLE_TEAM_ID environment variable
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
log_error "APPLE_ID and APPLE_PASSWORD environment variables required for notarization"
return 1
fi
DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg"
# Upload for notarization
log_info "Uploading to Apple Notarization Service..."
xcrun notarytool submit "$DMG_FILE" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "${APPLE_TEAM_ID}" \
--wait
if [ $? -ne 0 ]; then
log_error "Notarization failed"
return 1
fi
# Staple the notarization
xcrun stapler staple "$DMG_FILE"
log_success "Application notarized successfully"
echo ""
}
# Run main function
main

View file

@ -0,0 +1,321 @@
"""Build Windows installer (MSI) using PyInstaller.
This script builds the WebDrop Bridge application for Windows using PyInstaller.
It creates both a standalone executable and optionally an MSI installer.
Requirements:
- PyInstaller 6.0+
- Python 3.10+
- For MSI: WiX Toolset (optional, requires separate installation)
Usage:
python build_windows.py [--msi] [--code-sign]
"""
import sys
import subprocess
import os
import shutil
from pathlib import Path
from datetime import datetime
# Fix Unicode output on Windows
if sys.platform == "win32":
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
class WindowsBuilder:
"""Build Windows installer using PyInstaller."""
def __init__(self):
"""Initialize builder paths."""
self.project_root = Path(__file__).parent.parent.parent
self.build_dir = self.project_root / "build"
self.dist_dir = self.build_dir / "dist" / "windows"
self.temp_dir = self.build_dir / "temp" / "windows"
self.spec_file = self.build_dir / "webdrop_bridge.spec"
self.version = self._get_version()
def _get_version(self) -> str:
"""Get version from config.py."""
config_file = self.project_root / "src" / "webdrop_bridge" / "config.py"
for line in config_file.read_text().split("\n"):
if "app_version" in line and "1.0.0" in line:
# Extract default version from config
return "1.0.0"
return "1.0.0"
def clean(self):
"""Clean previous builds."""
print("🧹 Cleaning previous builds...")
for path in [self.dist_dir, self.temp_dir]:
if path.exists():
shutil.rmtree(path)
print(f" Removed {path}")
def build_executable(self) -> bool:
"""Build executable using PyInstaller."""
print("\n🔨 Building Windows executable with PyInstaller...")
self.dist_dir.mkdir(parents=True, exist_ok=True)
self.temp_dir.mkdir(parents=True, exist_ok=True)
# PyInstaller command using spec file
cmd = [
sys.executable,
"-m",
"PyInstaller",
"--distpath",
str(self.dist_dir),
"--workpath",
str(self.temp_dir),
str(self.spec_file),
]
print(f" Command: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=str(self.project_root))
if result.returncode != 0:
print("❌ PyInstaller build failed")
return False
exe_path = self.dist_dir / "WebDropBridge.exe"
if not exe_path.exists():
print(f"❌ Executable not found at {exe_path}")
return False
print("✅ Executable built successfully")
print(f"📦 Output: {exe_path}")
print(f" Size: {exe_path.stat().st_size / 1024 / 1024:.1f} MB")
return True
def create_msi(self) -> bool:
"""Create MSI installer using WiX Toolset.
This requires WiX Toolset to be installed:
https://wixtoolset.org/releases/
"""
print("\n📦 Creating MSI installer with WiX...")
# Check if WiX is installed
heat_exe = shutil.which("heat.exe")
candle_exe = shutil.which("candle.exe")
light_exe = shutil.which("light.exe")
if not all([heat_exe, candle_exe, light_exe]):
print("⚠️ WiX Toolset not found in PATH")
print(" Install from: https://wixtoolset.org/releases/")
print(" Or use: choco install wixtoolset")
return False
# Create WiX source file
if not self._create_wix_source():
return False
# Compile and link
wix_obj = self.build_dir / "WebDropBridge.wixobj"
msi_output = self.dist_dir / f"WebDropBridge-{self.version}-Setup.msi"
# Run candle (compiler)
candle_cmd = [
str(candle_exe),
"-o",
str(wix_obj),
str(self.build_dir / "WebDropBridge.wxs"),
]
print(f" Compiling WiX source...")
result = subprocess.run(candle_cmd)
if result.returncode != 0:
print("❌ WiX compilation failed")
return False
# Run light (linker)
light_cmd = [
str(light_exe),
"-o",
str(msi_output),
str(wix_obj),
]
print(f" Linking MSI installer...")
result = subprocess.run(light_cmd)
if result.returncode != 0:
print("❌ MSI linking failed")
return False
if not msi_output.exists():
print(f"❌ MSI not found at {msi_output}")
return False
print("✅ MSI installer created successfully")
print(f"📦 Output: {msi_output}")
print(f" Size: {msi_output.stat().st_size / 1024 / 1024:.1f} MB")
return True
def _create_wix_source(self) -> bool:
"""Create WiX source file for MSI generation."""
wix_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="{self.version}"
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
<ComponentRef Id="MainExecutable" />
<ComponentRef Id="ProgramMenuShortcut" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="MainExecutable" Guid="*">
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\\WebDropBridge.exe" KeyPath="yes"/>
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ProgramMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="ApplicationProgramsFolderRemove"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\WebDropBridge"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Product>
</Wix>
'''
wix_file = self.build_dir / "WebDropBridge.wxs"
wix_file.write_text(wix_content)
print(f" Created WiX source: {wix_file}")
return True
def sign_executable(self, cert_path: str, password: str) -> bool:
"""Sign executable with certificate (optional).
Args:
cert_path: Path to code signing certificate
password: Certificate password
Returns:
True if signing successful
"""
print("\n🔐 Signing executable...")
signtool = shutil.which("signtool.exe")
if not signtool:
print("⚠️ signtool.exe not found (part of Windows SDK)")
print(" Skipping code signing")
return True
exe_path = self.dist_dir / "WebDropBridge.exe"
cmd = [
signtool,
"sign",
"/f",
cert_path,
"/p",
password,
"/t",
"http://timestamp.comodoca.com/authenticode",
str(exe_path),
]
result = subprocess.run(cmd)
if result.returncode != 0:
print("❌ Code signing failed")
return False
print("✅ Executable signed successfully")
return True
def build(self, create_msi: bool = False, sign: bool = False) -> bool:
"""Run complete build process.
Args:
create_msi: Whether to create MSI installer
sign: Whether to sign executable (requires certificate)
Returns:
True if build successful
"""
start_time = datetime.now()
print("=" * 60)
print("🚀 WebDrop Bridge Windows Build")
print("=" * 60)
self.clean()
if not self.build_executable():
return False
if create_msi:
if not self.create_msi():
print("⚠️ MSI creation failed, but executable is available")
if sign:
# Would need certificate path from environment
cert_path = os.getenv("CODE_SIGN_CERT")
if cert_path:
self.sign_executable(cert_path, os.getenv("CODE_SIGN_PASSWORD", ""))
elapsed = (datetime.now() - start_time).total_seconds()
print("\n" + "=" * 60)
print(f"✅ Build completed in {elapsed:.1f}s")
print("=" * 60)
return True
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(
description="Build WebDrop Bridge for Windows"
)
parser.add_argument(
"--msi",
action="store_true",
help="Create MSI installer (requires WiX Toolset)",
)
parser.add_argument(
"--sign",
action="store_true",
help="Sign executable (requires CODE_SIGN_CERT environment variable)",
)
args = parser.parse_args()
builder = WindowsBuilder()
success = builder.build(create_msi=args.msi, sign=args.sign)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,55 @@
# Sync script to keep origin and upstream remotes in sync
# Usage: .\sync_remotes.ps1 [--push-to-origin]
param(
[switch]$PushToOrigin
)
$ErrorActionPreference = "Stop"
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent (Split-Path -Parent $scriptPath)
Write-Host "🔄 WebDrop Bridge - Remote Sync Script" -ForegroundColor Cyan
Write-Host "Repository: $repoRoot`n" -ForegroundColor Gray
# Change to repo directory
Push-Location $repoRoot
try {
# Fetch from both remotes
Write-Host "📥 Fetching from origin..." -ForegroundColor Yellow
git fetch origin
Write-Host "📥 Fetching from upstream..." -ForegroundColor Yellow
git fetch upstream
# Show status
Write-Host "`n📊 Remote Status:" -ForegroundColor Cyan
git remote -v
# Show branch comparison
Write-Host "`n📋 Branch Comparison:" -ForegroundColor Cyan
Write-Host "Local branches vs origin:" -ForegroundColor Gray
git log --oneline origin/main -5 | ForEach-Object { Write-Host " origin: $_" }
Write-Host ""
git log --oneline upstream/main -5 | ForEach-Object { Write-Host " upstream: $_" }
# Optionally push to origin
if ($PushToOrigin) {
Write-Host "`n📤 Pushing current branch to origin..." -ForegroundColor Yellow
$currentBranch = git rev-parse --abbrev-ref HEAD
git push origin $currentBranch
Write-Host "✅ Pushed $currentBranch to origin" -ForegroundColor Green
} else {
Write-Host "`n💡 Tip: Use --push-to-origin flag to push current branch to origin" -ForegroundColor Gray
}
Write-Host "`n✅ Sync complete!" -ForegroundColor Green
}
catch {
Write-Host "`n❌ Error: $_" -ForegroundColor Red
exit 1
}
finally {
Pop-Location
}