Mobile polish, release validation script, bump to v4.1.2

Mobile-responsive CSS improvements:
- Larger touch targets for drop zones and buttons (56px min)
- Touch feedback with active states for touch devices
- Camera hint text on mobile ("Tap to take photo or choose file")
- Mode buttons stack vertically on small screens
- Full-width download buttons on mobile
- Navbar doesn't stick on mobile to save screen space

Release validation script (scripts/validate-release.sh):
- Automated pre-release checks: ruff, imports, encode/decode sanity
- Optional Docker build/test (--docker flag)
- Optional Pi smoke test via SSH (--pi flag)
- Pass/fail summary with exit codes

Other:
- Version bump to 4.1.2 (pyproject.toml, constants.py, __init__.py)
- Fixed ruff import sorting in cli.py
- Updated PLAN-4.1.2.md (all 9 features complete)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-05 20:34:23 -05:00
parent 2d3ed8a79a
commit 6c3bc995f1
8 changed files with 577 additions and 9 deletions

5
.gitignore vendored
View File

@@ -64,8 +64,9 @@ htmlcov/
# Output test files.
test_data/*.png
# Dev scripts (local convenience scripts)
scripts/
# Dev scripts (local convenience scripts - except validate-release.sh)
scripts/*
!scripts/validate-release.sh
# Web UI auth database and SSL certs
frontends/web/instance/

View File

@@ -7,7 +7,7 @@ Polish and UX improvements after the 4.1.1 stability release.
## 1. Real Progress Bar for Encode/Decode
**Status:** Planned
**Status:** Done
**Problem:** Users see elapsed time but no indication of how far along the operation is. Long DCT encodes on Pi can take 2-3 minutes with no feedback.
@@ -76,7 +76,7 @@ Polish and UX improvements after the 4.1.1 stability release.
## 3. Mobile-Responsive Polish
**Status:** Planned
**Status:** Done
**Problem:** UI works on mobile but has rough edges - cramped buttons, hard-to-tap targets, awkward layouts on small screens.
@@ -203,7 +203,7 @@ Polish and UX improvements after the 4.1.1 stability release.
## 8. Release Validation Script
**Status:** Planned
**Status:** Done
**Problem:** Manual release checklist is error-prone. Need automated validation.
@@ -245,6 +245,6 @@ Polish and UX improvements after the 4.1.1 stability release.
## Notes
- Keep 4.1.2 focused - 9 features (6 done)
- Keep 4.1.2 focused - 9 features (9 done)
- Don't break DCT compatibility (4.1.1 RS format is stable)
- Test on Pi before release

View File

@@ -1442,3 +1442,260 @@ footer {
padding: 0.35rem 0.75rem;
background: rgba(0, 0, 0, 0.1);
}
/* ============================================================================
MOBILE RESPONSIVE IMPROVEMENTS
============================================================================ */
/* Mobile-specific drop zone improvements */
@media (max-width: 768px) {
/* Larger drop zones on mobile for easier touch targets */
.drop-zone {
padding: 2rem 1.5rem;
min-height: 140px;
}
/* Larger touch target for upload icons */
.drop-zone-label i {
font-size: 2.5rem !important;
}
/* Touch feedback - active state */
.drop-zone:active {
border-color: var(--gradient-start);
background: rgba(102, 126, 234, 0.15);
transform: scale(0.98);
}
/* Mode buttons - stack vertically on very small screens */
.d-flex.gap-2:has(.mode-btn) {
flex-direction: column;
}
.mode-btn {
padding: 1rem;
min-height: 56px; /* iOS touch target minimum */
}
/* Full-width primary buttons */
.btn-primary.btn-lg {
padding: 1rem 1.5rem;
font-size: 1.1rem;
min-height: 56px;
}
/* Security factor boxes - more padding for touch */
.security-box {
padding: 1.25rem;
}
/* Form controls - larger for touch */
.form-control,
.form-select {
padding: 0.75rem 1rem;
font-size: 1rem;
min-height: 48px;
}
/* Input groups - consistent sizing */
.input-group .form-control {
min-height: 48px;
}
.input-group .btn {
min-width: 48px;
padding: 0.75rem;
}
/* Password toggle button - easier to tap */
[data-toggle-password] {
min-width: 52px;
}
/* PIN input - larger on mobile */
.pin-input-container .form-control {
font-size: 1.4rem;
letter-spacing: 4px;
padding: 0.875rem 1rem;
}
/* Passphrase input - comfortable mobile size */
.passphrase-input {
font-size: 1rem !important;
padding: 0.875rem 1rem !important;
}
/* Card headers - compact on mobile */
.card-header h5 {
font-size: 1.1rem;
}
/* Alert info panel - readable text */
.alert.small {
font-size: 0.9rem;
}
/* Bottom info icons - larger tap targets */
.row.text-center .col-4 {
padding: 0.5rem;
}
.row.text-center .col-4 i {
font-size: 2rem !important;
}
/* Capacity panel badges - easier to read */
#capacityPanel .badge {
font-size: 0.8rem;
padding: 0.4rem 0.6rem;
}
/* Payload type toggle - full width buttons */
.btn-group[role="group"] {
flex-direction: row;
}
.btn-group .btn {
padding: 0.75rem 0.5rem;
font-size: 0.95rem;
}
/* Textarea - comfortable height */
textarea.form-control {
min-height: 120px;
}
/* Channel select - full width */
#channelSelect {
font-size: 1rem;
}
}
/* Very small screens (iPhone SE, etc.) */
@media (max-width: 375px) {
.drop-zone {
padding: 1.5rem 1rem;
}
.mode-btn {
padding: 0.875rem;
font-size: 0.9rem;
}
.mode-btn .text-muted {
display: none; /* Hide secondary text on tiny screens */
}
.card-header h5 {
font-size: 1rem;
}
/* Stack security factor row */
.row:has(.security-box) > .col-md-6 {
margin-bottom: 1rem;
}
}
/* Touch device optimizations */
@media (hover: none) and (pointer: coarse) {
/* Remove hover effects that don't work on touch */
.btn-primary:hover {
transform: none;
}
.feature-card:hover {
transform: none;
}
.card-link:hover .feature-card {
transform: none;
}
/* Add active states instead */
.btn-primary:active {
transform: scale(0.98);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.feature-card:active {
transform: scale(0.98);
}
/* Drop zone active feedback */
.drop-zone:active {
border-color: var(--gradient-start);
background: rgba(102, 126, 234, 0.1);
}
/* Mode button active state */
.mode-btn:active {
background: rgba(255, 255, 255, 0.12);
border-color: var(--gradient-start);
}
}
/* Camera hint for mobile - shows on file inputs */
@media (max-width: 768px) {
.drop-zone-label span.text-muted {
display: block;
}
/* Add camera icon hint on mobile */
.drop-zone-label::after {
content: "Tap to take photo or choose file";
display: block;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
margin-top: 0.5rem;
}
/* Hide the default text and show mobile version */
.drop-zone-label > span.text-muted {
display: none;
}
}
/* Navbar mobile adjustments */
@media (max-width: 768px) {
.navbar {
padding: 0.5rem 1rem;
}
.navbar-brand img {
height: 32px;
}
/* Sticky header shouldn't eat too much space */
.navbar.sticky-top {
position: relative; /* Don't stick on mobile - saves screen space */
}
}
/* Results page mobile adjustments */
@media (max-width: 768px) {
/* Download button - full width on mobile */
.btn-success.btn-lg,
a.btn-success.btn-lg {
width: 100%;
padding: 1rem;
font-size: 1.1rem;
}
/* QR codes - appropriate sizing */
.qr-scan-container {
max-width: 280px;
margin: 0 auto;
}
/* Message display - readable on mobile */
.alert-message {
font-size: 0.9rem;
padding: 1rem;
word-break: break-word;
}
/* Result icon - slightly smaller on mobile */
.result-icon {
font-size: 3rem;
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "stegasoo"
version = "4.1.1"
version = "4.1.2"
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
readme = "README.md"
license = "MIT"

307
scripts/validate-release.sh Executable file
View File

@@ -0,0 +1,307 @@
#!/bin/bash
# =============================================================================
# Stegasoo Release Validation Script
# =============================================================================
# Automated pre-release validation to catch issues before tagging a release.
#
# Usage:
# ./scripts/validate-release.sh # Local validation only
# ./scripts/validate-release.sh --pi # Include Pi smoke test
# PI_IP=192.168.0.4 ./scripts/validate-release.sh --pi
#
# Exit codes:
# 0 = All tests passed
# 1 = One or more tests failed
# =============================================================================
# Don't use set -e as we need to handle test failures gracefully
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# Default Pi IP (can be overridden via environment)
PI_IP="${PI_IP:-192.168.0.4}"
PI_USER="${PI_USER:-alee}"
INCLUDE_PI=false
INCLUDE_DOCKER=true
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--pi)
INCLUDE_PI=true
shift
;;
--no-docker)
INCLUDE_DOCKER=false
shift
;;
--help|-h)
echo "Usage: $0 [--pi] [--no-docker]"
echo ""
echo "Options:"
echo " --pi Include Pi smoke test (requires SSH access)"
echo " --no-docker Skip Docker build/test"
echo ""
echo "Environment:"
echo " PI_IP Pi IP address (default: 192.168.0.4)"
echo " PI_USER Pi SSH user (default: alee)"
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Track results
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
FAILED_TESTS=()
# Helper functions
pass() {
echo -e "${GREEN}[PASS]${NC} $1"
((TESTS_PASSED++))
((TESTS_RUN++))
}
fail() {
echo -e "${RED}[FAIL]${NC} $1"
FAILED_TESTS+=("$1")
((TESTS_FAILED++))
((TESTS_RUN++))
}
skip() {
echo -e "${YELLOW}[SKIP]${NC} $1"
}
section() {
echo ""
echo -e "${CYAN}━━━ $1 ━━━${NC}"
}
# =============================================================================
# Header
# =============================================================================
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Stegasoo Release Validation ║${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════╝${NC}"
echo ""
# Get version from pyproject.toml
VERSION=$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)
echo -e "Version: ${YELLOW}${VERSION}${NC}"
echo -e "Branch: ${YELLOW}$(git branch --show-current)${NC}"
echo ""
# =============================================================================
# 1. Code Quality Checks
# =============================================================================
section "Code Quality"
# Ruff linting
if command -v ./venv/bin/ruff &> /dev/null; then
echo -n "Running ruff check... "
if ./venv/bin/ruff check src/ frontends/ --quiet 2>/dev/null; then
pass "Ruff linting"
else
fail "Ruff linting (run: ./venv/bin/ruff check src/ frontends/)"
fi
else
skip "Ruff not installed"
fi
# =============================================================================
# 2. Unit Tests (if they exist)
# =============================================================================
section "Unit Tests"
if ls tests/test_*.py 1> /dev/null 2>&1; then
echo -n "Running pytest... "
if ./venv/bin/pytest tests/ -q --tb=no 2>/dev/null; then
pass "Pytest unit tests"
else
fail "Pytest unit tests"
fi
else
skip "No unit tests found (tests/test_*.py)"
fi
# =============================================================================
# 3. Import Tests
# =============================================================================
section "Import Tests"
# Test core library import
echo -n "Testing stegasoo import... "
if ./venv/bin/python -c "from stegasoo import encode, decode; print('OK')" 2>/dev/null | grep -q OK; then
pass "Core library import"
else
fail "Core library import"
fi
# Test DCT support
echo -n "Testing DCT support... "
if ./venv/bin/python -c "from stegasoo import has_dct_support; assert has_dct_support(), 'No DCT'; print('OK')" 2>/dev/null | grep -q OK; then
pass "DCT support available"
else
fail "DCT support (scipy/jpegio missing?)"
fi
# Test CLI import
echo -n "Testing CLI import... "
if ./venv/bin/python -c "from stegasoo.cli import main; print('OK')" 2>/dev/null | grep -q OK; then
pass "CLI module import"
else
fail "CLI module import"
fi
# =============================================================================
# 4. Encode/Decode Sanity Test
# =============================================================================
section "Encode/Decode Test"
echo -n "Running encode/decode sanity check... "
SANITY_RESULT=$(./venv/bin/python << 'EOF' 2>&1
import sys
sys.path.insert(0, 'src')
from stegasoo import encode, decode
with open('test_data/carrier.jpg', 'rb') as f:
carrier = f.read()
with open('test_data/ref.jpg', 'rb') as f:
ref = f.read()
# LSB test
result = encode(message="sanity test", reference_photo=ref, carrier_image=carrier,
passphrase="test", pin="123456", embed_mode="lsb")
decoded = decode(stego_image=result.stego_image, reference_photo=ref,
passphrase="test", pin="123456", embed_mode="lsb")
assert decoded.message == "sanity test", f"LSB mismatch: {decoded.message}"
# DCT test
result = encode(message="dct sanity", reference_photo=ref, carrier_image=carrier,
passphrase="dct", pin="654321", embed_mode="dct")
decoded = decode(stego_image=result.stego_image, reference_photo=ref,
passphrase="dct", pin="654321", embed_mode="dct")
assert decoded.message == "dct sanity", f"DCT mismatch: {decoded.message}"
print("OK")
EOF
)
if echo "$SANITY_RESULT" | grep -q "OK"; then
pass "Encode/decode sanity (LSB + DCT)"
else
fail "Encode/decode sanity: $SANITY_RESULT"
fi
# =============================================================================
# 5. Docker Build & Test (optional)
# =============================================================================
if $INCLUDE_DOCKER; then
section "Docker"
if command -v docker &> /dev/null || command -v sudo &> /dev/null; then
DOCKER_CMD="docker"
if ! docker info &>/dev/null 2>&1; then
DOCKER_CMD="sudo docker"
fi
echo -n "Building Docker image... "
if $DOCKER_CMD build -t stegasoo:validate -q . >/dev/null 2>&1; then
pass "Docker build"
# Test container starts
echo -n "Testing container startup... "
CONTAINER_ID=$($DOCKER_CMD run -d -p 15000:5000 stegasoo:validate 2>/dev/null)
sleep 3
if curl -s -o /dev/null -w "%{http_code}" http://localhost:15000/ 2>/dev/null | grep -qE "200|302"; then
pass "Container responds to HTTP"
else
fail "Container HTTP response"
fi
# Cleanup
$DOCKER_CMD stop "$CONTAINER_ID" >/dev/null 2>&1 || true
$DOCKER_CMD rm "$CONTAINER_ID" >/dev/null 2>&1 || true
else
fail "Docker build"
fi
# Cleanup test image
$DOCKER_CMD rmi stegasoo:validate >/dev/null 2>&1 || true
else
skip "Docker not available"
fi
else
skip "Docker tests (use --docker to enable)"
fi
# =============================================================================
# 6. Pi Smoke Test (optional)
# =============================================================================
if $INCLUDE_PI; then
section "Pi Smoke Test"
echo -n "Testing SSH connectivity to $PI_USER@$PI_IP... "
if ssh -o ConnectTimeout=5 -o BatchMode=yes "$PI_USER@$PI_IP" "echo OK" 2>/dev/null | grep -q OK; then
pass "SSH connectivity"
echo -n "Checking stegasoo service status... "
if ssh "$PI_USER@$PI_IP" "systemctl is-active stegasoo" 2>/dev/null | grep -q active; then
pass "Stegasoo service running"
echo -n "Running smoke test on Pi... "
SMOKE_RESULT=$(ssh "$PI_USER@$PI_IP" "cd /home/$PI_USER/stegasoo && bash tests/smoke-test.sh --quick 2>&1" || echo "FAILED")
if echo "$SMOKE_RESULT" | grep -qE "All tests passed|PASS"; then
pass "Pi smoke test"
else
fail "Pi smoke test"
fi
else
fail "Stegasoo service not running"
fi
else
fail "SSH connectivity to Pi"
fi
else
skip "Pi smoke test (use --pi to enable)"
fi
# =============================================================================
# Summary
# =============================================================================
echo ""
echo -e "${CYAN}━━━ Summary ━━━${NC}"
echo ""
echo -e "Tests run: ${TESTS_RUN}"
echo -e "Passed: ${GREEN}${TESTS_PASSED}${NC}"
echo -e "Failed: ${RED}${TESTS_FAILED}${NC}"
if [ ${#FAILED_TESTS[@]} -gt 0 ]; then
echo ""
echo -e "${RED}Failed tests:${NC}"
for test in "${FAILED_TESTS[@]}"; do
echo -e " - $test"
done
fi
echo ""
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All validation checks passed!${NC}"
echo -e " Ready to tag release ${VERSION}"
exit 0
else
echo -e "${RED}✗ Validation failed - fix issues before release${NC}"
exit 1
fi

View File

@@ -7,7 +7,7 @@ Changes in v4.0.0:
- encode() and decode() now accept channel_key parameter
"""
__version__ = "4.0.1"
__version__ = "4.1.2"
# Core functionality
# Channel key management (v4.0.0)

View File

@@ -105,6 +105,7 @@ def encode(
stegasoo encode photo.png -r ref.jpg -f secret.pdf -o encoded.png
"""
from PIL import Image
from .encode import encode as stegasoo_encode
from .encode import encode_file as stegasoo_encode_file
@@ -1108,7 +1109,9 @@ def admin_recover(db_path, password):
stegasoo admin recover --db /path/to/stegasoo.db
"""
import sqlite3
from argon2 import PasswordHasher
from .recovery import verify_recovery_key
# Try default paths if not specified

View File

@@ -25,7 +25,7 @@ from pathlib import Path
# VERSION
# ============================================================================
__version__ = "4.1.0"
__version__ = "4.1.2"
# ============================================================================
# FILE FORMAT