diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100644
index 0000000..fc89eb0
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,5 @@
+#!/bin/sh
+# Auto-updates the "Last updated" footer in staged Markdown docs.
+# Enable once per clone with: git config core.hooksPath .githooks
+
+python "$(git rev-parse --show-toplevel)/scripts/update_docs_dates.py"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f6b35a5..8cfbe8b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -9,10 +9,15 @@ on:
- 'CHANGELOG.md'
- 'LICENSE'
- '*.md'
+ workflow_dispatch: {}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
# ─────────────────────────────────────────────────────────────────────────────
# TEST BUILD (develop only) — Verify code compiles and tests pass
@@ -63,6 +68,8 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
+ env:
+ ELECTRON_CACHE: ${{ github.workspace }}/.electron-cache
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
@@ -97,35 +104,53 @@ jobs:
cd frontend
npm ci
npm run build
+ - name: Cache Electron download
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.ELECTRON_CACHE }}
+ key: electron-${{ runner.os }}-${{ hashFiles('electron/package-lock.json') }}
- name: Test electron setup
run: |
cd electron
npm ci --include=dev
# ─────────────────────────────────────────────────────────────────────────────
- # RELEASE BUILD (main only) — Create and publish stable release
+ # BETA RELEASE (develop only) — Package and publish a prerelease build
# ─────────────────────────────────────────────────────────────────────────────
- get-version:
- if: github.ref == 'refs/heads/main'
+ get-version-beta:
+ if: github.ref == 'refs/heads/develop'
+ needs: [test-python, test-frontend, test-electron]
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
- version: ${{ steps.ver.outputs.version }}
+ base_version: ${{ steps.ver.outputs.base_version }}
+ beta_version: ${{ steps.ver.outputs.beta_version }}
steps:
- uses: actions/checkout@v6
- id: ver
+ env:
+ GH_TOKEN: ${{ github.token }}
run: |
- VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' python/config.py)
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- echo "Building MetaLens v$VERSION"
+ BASE=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' python/config.py)
+ # Count existing beta releases for this exact base version and increment —
+ # keeps beta.N scoped to the version instead of the workflow's global run number.
+ LAST=$(gh release list --repo "${{ github.repository }}" --limit 200 \
+ | grep -oP "v${BASE}-beta\.\K[0-9]+" | sort -n | tail -1)
+ NEXT=$(( ${LAST:-0} + 1 ))
+ BETA="${BASE}-beta.${NEXT}"
+ echo "base_version=$BASE" >> "$GITHUB_OUTPUT"
+ echo "beta_version=$BETA" >> "$GITHUB_OUTPUT"
+ echo "Building MetaLens beta v$BETA"
- build-windows:
- if: github.ref == 'refs/heads/main'
- needs: get-version
+ build-windows-beta:
+ if: github.ref == 'refs/heads/develop'
+ needs: get-version-beta
runs-on: windows-latest
permissions:
contents: read
+ env:
+ ELECTRON_CACHE: ${{ github.workspace }}/.electron-cache
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
@@ -160,28 +185,35 @@ jobs:
npm run build
- name: Copy frontend dist into electron
run: cp -r frontend/dist electron/frontend
- - name: Sync electron version
+ - name: Sync electron version to beta
run: |
cd electron
- npm version ${{ needs.get-version.outputs.version }} --no-git-tag-version --allow-same-version
+ npm version ${{ needs.get-version-beta.outputs.beta_version }} --no-git-tag-version --allow-same-version
+ - name: Cache Electron download
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.ELECTRON_CACHE }}
+ key: electron-${{ runner.os }}-${{ hashFiles('electron/package-lock.json') }}
- name: Package with Electron Forge
run: |
cd electron
npm ci --include=dev
npx electron-forge make
- - name: Upload Windows artifact
+ - name: Upload Windows beta artifact
uses: actions/upload-artifact@v7
with:
- name: windows-installer
+ name: windows-installer-beta
path: electron/out/make/**/*.exe
if-no-files-found: error
- build-linux:
- if: github.ref == 'refs/heads/main'
- needs: get-version
+ build-linux-beta:
+ if: github.ref == 'refs/heads/develop'
+ needs: get-version-beta
runs-on: ubuntu-latest
permissions:
contents: read
+ env:
+ ELECTRON_CACHE: ${{ github.workspace }}/.electron-cache
steps:
- uses: actions/checkout@v6
- name: Install system dependencies
@@ -217,12 +249,17 @@ jobs:
npm run build
- name: Copy frontend dist into electron
run: cp -r frontend/dist electron/frontend
- - name: Sync electron version
+ - name: Sync electron version to beta
run: |
cd electron
- npm version ${{ needs.get-version.outputs.version }} --no-git-tag-version --allow-same-version
+ npm version ${{ needs.get-version-beta.outputs.beta_version }} --no-git-tag-version --allow-same-version
- name: Configure RPM macros
run: echo '%_build_id_links none' >> ~/.rpmmacros
+ - name: Cache Electron download
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.ELECTRON_CACHE }}
+ key: electron-${{ runner.os }}-${{ hashFiles('electron/package-lock.json') }}
- name: Package with Electron Forge
run: |
cd electron
@@ -230,7 +267,7 @@ jobs:
npx electron-forge make
- name: Create Linux tar.gz
run: |
- VERSION=${{ needs.get-version.outputs.version }}
+ VERSION=${{ needs.get-version-beta.outputs.beta_version }}
APP_DIR=$(find electron/out -maxdepth 1 -type d -name "MetaLens-linux-*" | head -1)
if [ -n "$APP_DIR" ]; then
tar -czf "electron/out/make/MetaLens-${VERSION}-Linux.tar.gz" \
@@ -239,19 +276,19 @@ jobs:
else
echo "Warning: no MetaLens-linux-* directory found, skipping tar.gz"
fi
- - name: Upload Linux artifacts
+ - name: Upload Linux beta artifacts
uses: actions/upload-artifact@v7
with:
- name: linux-packages
+ name: linux-packages-beta
path: |
electron/out/make/**/*.deb
electron/out/make/**/*.rpm
electron/out/make/**/*.tar.gz
if-no-files-found: error
- release:
- if: github.ref == 'refs/heads/main'
- needs: [get-version, build-windows, build-linux]
+ release-beta:
+ if: github.ref == 'refs/heads/develop'
+ needs: [get-version-beta, build-windows-beta, build-linux-beta]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -261,7 +298,89 @@ jobs:
path: artifacts
- name: List artifacts
run: find artifacts -type f
- - name: Create GitHub Release
+ - name: Create beta GitHub Release
+ uses: softprops/action-gh-release@v3
+ with:
+ tag_name: v${{ needs.get-version-beta.outputs.beta_version }}
+ name: MetaLens v${{ needs.get-version-beta.outputs.beta_version }} (beta)
+ draft: false
+ prerelease: true
+ generate_release_notes: true
+ make_latest: false
+ files: |
+ artifacts/windows-installer-beta/**
+ artifacts/linux-packages-beta/**
+
+ # ─────────────────────────────────────────────────────────────────────────────
+ # STABLE RELEASE (main only) — Promote the matching beta build, no rebuild
+ # ─────────────────────────────────────────────────────────────────────────────
+ get-version:
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ version: ${{ steps.ver.outputs.version }}
+ steps:
+ - uses: actions/checkout@v6
+ - id: ver
+ run: |
+ VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' python/config.py)
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "Building MetaLens v$VERSION"
+
+ find-latest-beta:
+ if: github.ref == 'refs/heads/main'
+ needs: get-version
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ beta_tag: ${{ steps.find.outputs.beta_tag }}
+ steps:
+ - id: find
+ env:
+ GH_TOKEN: ${{ github.token }}
+ VERSION: ${{ needs.get-version.outputs.version }}
+ run: |
+ TAG=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 200 --json tagName \
+ --jq '.[].tagName' \
+ | grep -E "^v${VERSION}-beta\.[0-9]+$" \
+ | sort -t. -k4 -n \
+ | tail -1)
+ if [ -z "$TAG" ]; then
+ echo "::error::Nessuna release beta trovata per v${VERSION}. Genera prima una beta da develop con la stessa VERSION in python/config.py."
+ exit 1
+ fi
+ echo "beta_tag=$TAG" >> "$GITHUB_OUTPUT"
+ echo "Promuovendo $TAG a stable v${VERSION}"
+
+ promote-stable:
+ if: github.ref == 'refs/heads/main'
+ needs: [get-version, find-latest-beta]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Download beta release assets
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ mkdir -p promoted
+ gh release download "${{ needs.find-latest-beta.outputs.beta_tag }}" \
+ --repo "$GITHUB_REPOSITORY" --dir promoted
+ - name: Strip beta suffix from filenames
+ run: |
+ cd promoted
+ # Each maker encodes the beta suffix differently: "-beta.N" (exe/tar.gz),
+ # "beta.N" with no separator (rpm, dash stripped for Version field rules),
+ # ".beta.N" (deb, dot-separated per Debian version conventions).
+ for f in *; do
+ new=$(echo "$f" | sed -E 's/[-.]?beta\.?[0-9]+//')
+ if [ "$f" != "$new" ]; then mv -- "$f" "$new"; fi
+ done
+ ls -la
+ - name: Create stable GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ needs.get-version.outputs.version }}
@@ -270,6 +389,4 @@ jobs:
prerelease: false
generate_release_notes: true
make_latest: true
- files: |
- artifacts/windows-installer/**
- artifacts/linux-packages/**
+ files: promoted/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93ce368..d1cd357 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,32 @@
# Changelog
All notable changes to MetaLens are documented in this file.
+The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) — `MAJOR.MINOR.PATCH`.
-Format: [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH`
+---
+
+## [Unreleased] — Next Release
+
+### Roadmap
+
+- [ ] **CSV/JSON Export** — Export metadata for the current file or folder
+- [ ] **Batch Edit** — Apply a field change to multiple selected files at once
+- [ ] **Search/Filter Bar** — Filter the file list by name or extension
---
-## [Unreleased]
+## [0.2.1] — 2026-07-14
+
+### Dependencies
+- electron: 43.0.0 → 43.1.0 (fixed crash on replacing an open application menu, Chromium 150.0.7871.47, Node.js 24.18.0)
+- vite: 8.1.3 → 8.1.4 (oxc minifier preference for legacy builds, StackBlitz build workaround, SSR stacktrace alignment)
+- uvicorn: 0.49.0 → 0.51.0 (near-zero-downtime worker reload on SIGHUP, removed colorama from standard extra)
+- lucide-react: 1.23.0 → 1.24.0 (new icons, several upstream icon fixes)
-### Planned
-- Export metadata as CSV/JSON
-- Batch edit on multiple selected files
-- Search/filter bar in file list
+### Quality
+- All tests passing: 47 passed, 1 skipped
+- Frontend build and Electron startup verified with updated dependencies
+- No regressions
---
@@ -194,9 +209,9 @@ Format: [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH`
### Documentation
- Added comprehensive security documentation (2000+ lines)
- - `SECURITY_ANALYSIS_Q_AND_A.md`: Deep threat model analysis
- `python/SECURITY.md`: Security design and implementation guide
- `python/core/PATH_SECURITY_USAGE.md`: API reference for developers
+ - *(internal threat-model notes were later removed from the repo as temporary development files — see [0.1.2])*
---
@@ -222,3 +237,7 @@ Format: [Semantic Versioning](https://semver.org/) — `MAJOR.MINOR.PATCH`
- About dialog with version from `/health`
- GitHub Actions CI: automated Windows (.exe) + Linux (.deb/.rpm) builds on push to `main`
- MIT License — © 2026 Graziano Mariella
+
+---
+
+*← [Back to README](./README.md)*
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..81f90cc
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# 🤝 Code of Conduct
+
+## Our Commitment
+
+We are committed to creating a welcoming, inclusive, and respectful community around MetaLens. This Code of Conduct outlines our expectations for all members of our community, regardless of background, experience level, or identity.
+
+---
+
+## ✅ Expected Behavior
+
+- **Be respectful** — treat all community members with dignity and kindness
+- **Be inclusive** — welcome and support people of all backgrounds
+- **Be collaborative** — work together constructively toward shared goals
+- **Be patient** — understand that people have different levels of expertise and may need help
+- **Give credit** — acknowledge the contributions of others
+- **Listen actively** — take time to understand different perspectives
+- **Provide constructive feedback** — help others improve without being dismissive
+
+---
+
+## ❌ Unacceptable Behavior
+
+The following behaviors are not tolerated in our community:
+
+- Harassment, discrimination, or bullying of any kind (based on race, gender, sexual orientation, disability, age, religion, etc.)
+- Offensive, derogatory, or hateful comments or jokes
+- Unwanted sexual advances or inappropriate comments
+- Threats, intimidation, or aggressive behavior
+- Trolling, spam, or deliberate disruption
+- Sharing others' private information without consent (doxxing)
+- Any form of abuse or violence
+
+---
+
+## 🌍 Creating a Welcoming Environment
+
+To foster a positive community:
+
+- Use inclusive language — avoid gendered terms where possible
+- Respect pronouns and names — use what people ask you to use
+- Acknowledge mistakes — if you say something harmful, apologize sincerely and learn
+- Challenge harmful behavior — speak up if you witness violations
+- Support newcomers — help people feel welcome, especially those new to open source
+
+---
+
+## 📢 Reporting & Enforcement
+
+If you witness or experience unacceptable behavior:
+
+1. **GitHub Issues** — [Report via GitHub Issues](https://github.com/overwrite00/MetaLens/issues) for documentation or policy concerns
+2. **GitHub Discussions** — [Report via GitHub Discussions](https://github.com/overwrite00/MetaLens/discussions) for community concerns
+3. **Direct Contact** — reach out to maintainers privately if needed
+
+We take all reports seriously and will respond promptly. Violators may be warned, temporarily banned, or permanently removed from the community at the maintainers' discretion.
+
+---
+
+## 🔍 Scope
+
+This Code of Conduct applies to:
+- GitHub Issues and Pull Requests
+- GitHub Discussions
+- Project documentation and comments
+- Any community spaces associated with MetaLens
+
+---
+
+## 📝 Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, and is used under the Creative Commons Attribution 4.0 International License.
+
+---
+
+*Last updated: 2026-07-14*
+*← [Contributing](./CONTRIBUTING.md) | [Security →](./SECURITY.md)*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..185589e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,225 @@
+# 🎯 Contributing to MetaLens
+
+Thank you for your interest in contributing to MetaLens! We welcome bug reports, feature requests, documentation improvements, and code contributions from the community.
+
+---
+
+## 🎯 Welcome
+
+MetaLens is an open-source universal file metadata manager. Whether you're fixing a bug, improving documentation, adding a new metadata handler, or enhancing the UI, your contributions make MetaLens better for everyone.
+
+Before contributing, please review this guide and our [Code of Conduct](./CODE_OF_CONDUCT.md).
+
+---
+
+## 🔀 Branch Strategy
+
+- **Main branch**: `main` (protected, release-only — tagged `vX.Y.Z` with GitHub Release binaries)
+- **Development branch**: `develop` (main integration branch, PRs target here)
+- **Feature branches**: Create from `develop`, e.g., `feature/csv-export`
+
+### Submitting Code
+
+1. **Fork** the repository on GitHub
+2. **Clone** your fork locally
+3. **Create a feature branch** from `develop`:
+ ```bash
+ git checkout develop
+ git pull origin develop
+ git checkout -b feature/your-feature-name
+ ```
+4. **Make your changes** and test thoroughly
+5. **Commit** with clear messages (see [Code Standards](#-code-standards))
+6. **Push** to your fork
+7. **Create a Pull Request** targeting `develop` (NOT `main`)
+
+> [!WARNING]
+> ⚠️ PRs to `main` will be rejected. Always target `develop`.
+
+---
+
+## 🛠️ Development Setup
+
+### Prerequisites
+
+- **Python**: 3.11, 3.12, or 3.13 (see [REQUIREMENTS.md](./docs/REQUIREMENTS.md))
+- **Node.js**: 20+
+- **npm**: 10+
+- **Git**
+
+### Setup
+
+```bash
+git clone https://github.com/YOUR-USERNAME/MetaLens.git
+cd MetaLens
+
+# Python sidecar
+cd python
+python -m venv .venv
+source .venv/bin/activate # Linux
+# .venv\Scripts\activate # Windows
+pip install -r requirements.txt
+
+# Electron + frontend
+cd ../electron && npm install
+cd ../frontend && npm install
+
+# Run in development mode
+cd ../electron && npm start
+```
+
+Then enable the repo's git hooks (one-time per clone) so the `Last updated` footer in docs stays in sync automatically:
+
+```bash
+git config core.hooksPath .githooks
+```
+
+---
+
+## 📝 Code Standards
+
+### Python
+
+- Follow the handler pattern in `python/core/handlers/` — one file per category, implementing `BaseMetadataHandler`
+- Register new handlers in `python/core/registry.py`
+- Add type hints on public functions
+
+### Frontend
+
+- Components live in `frontend/src/components/`, shared logic in `frontend/src/hooks/`
+- Follow the existing Tailwind v4 cyber color palette (`frontend/src/styles/globals.css`) — never hardcode colors outside the defined CSS variables
+
+### Commit Message Format
+
+Follow the conventional commits format:
+
+```
+type(scope): description
+
+[optional body]
+```
+
+**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
+**Scope**: `handlers`, `api`, `ui`, `electron`, `deps`, etc.
+
+**Examples**:
+- `feat(handlers): add support for HEIC images`
+- `fix(diff): correct field alignment when types differ`
+- `docs(readme): update supported formats table`
+
+---
+
+## ✅ Testing
+
+### Running Tests
+
+All Python changes must pass the test suite:
+
+```bash
+pytest python/tests/ -v
+```
+
+### Test Requirements
+
+- **New handlers or routes must include tests** in `python/tests/`
+- **All tests must pass**: no regressions allowed
+- After frontend changes, verify the production build still compiles:
+ ```bash
+ cd frontend && npm run build
+ ```
+
+---
+
+## 📤 Submitting a Pull Request
+
+### PR Description Template
+
+```markdown
+## Summary
+Brief description of changes
+
+## Related Issue
+Fixes #(issue number) or Relates to #(issue number)
+
+## Type of Change
+- [ ] Bug fix (non-breaking)
+- [ ] New feature
+- [ ] Breaking change
+- [ ] Documentation update
+
+## Testing
+- [ ] Added tests for new functionality
+- [ ] `pytest python/tests/ -v` passes locally
+- [ ] Tested on Windows/Linux (if applicable)
+
+## Checklist
+- [ ] Code follows style guidelines
+- [ ] CLAUDE.md / CHANGELOG.md updated (if user-facing change)
+- [ ] Version bumped in `python/config.py` (if applicable — see versioning rules below)
+```
+
+### PR Guidelines
+
+- **Keep PRs focused** — one feature or bug fix per PR
+- **Reference related issues** — use `Fixes #123` to link issues
+- **Be responsive** — address feedback promptly
+- **Don't force-push** — makes reviewing history difficult
+
+---
+
+## 🔢 Versioning
+
+MetaLens uses `MAJOR.MINOR.PATCH`, with `python/config.py` → `VERSION` as the single source of truth.
+
+| Component | When it bumps |
+|---|---|
+| **PATCH** | Bug fix, dependency update, behavior correction |
+| **MINOR** | New feature, new handler, new panel |
+| **MAJOR** | Breaking change, milestone 1.0 |
+
+If your change bumps the version, update all four locations: `python/config.py`, `CHANGELOG.md`, `electron/package.json`, `frontend/package.json`.
+
+---
+
+## 🔧 Dependency Updates
+
+Dependabot proposes routine version bumps automatically, targeting `develop`. Security-alert PRs may target `main` due to a GitHub platform limitation — this is expected, not a misconfiguration.
+
+To update manually:
+
+```bash
+# Python
+pip list --outdated
+pip install --upgrade package_name
+pip freeze > python/requirements.txt
+
+# Node (electron/ or frontend/)
+npm outdated
+npm update package_name
+```
+
+Then test thoroughly before committing:
+
+```bash
+pytest python/tests/ -v
+cd frontend && npm run build
+```
+
+---
+
+## ❓ Questions?
+
+- **Documentation**: Check [docs/](./docs/) folder
+- **Bugs**: [GitHub Issues](https://github.com/overwrite00/MetaLens/issues)
+- **Security**: See [SECURITY.md](./SECURITY.md)
+
+---
+
+## 🙏 Thank You
+
+Thank you for contributing to MetaLens! Your efforts help make file metadata management more accessible and transparent for everyone.
+
+---
+
+*Last updated: 2026-07-14*
+*← [Code of Conduct](./CODE_OF_CONDUCT.md) | [Security →](./SECURITY.md)*
diff --git a/README.md b/README.md
index 1138e0a..9675dd5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
-# MetaLens
+# 🔍 MetaLens
-
+[](../../releases/latest)
+[](../../releases)


@@ -8,73 +9,110 @@
-**Universal File Metadata Manager** — read, edit, delete and compare metadata across all major file formats.
+**Universal File Metadata Manager** — read, edit, delete and compare metadata across all major file formats, entirely offline.
+
+> [!TIP]
+> 💡 **No account, no cloud, no setup.** Download a binary, double-click, and start browsing — MetaLens never touches the network.
---
-## Features
+## 📚 Documentation Index
-- **File-manager UI**: folder tree + file list + metadata detail panel
-- **All major formats**: images (JPEG/PNG/TIFF/WebP/RAW), audio (MP3/FLAC/OGG/M4A/WAV), video (MP4/MKV/MOV), documents (PDF/DOCX/XLSX/PPTX/DOC/XLS), any file (filesystem metadata)
-- **Full operations**: read, edit, delete individual fields, compare/diff two files
-- **Atomic writes**: temp-file + rename — no file corruption on failure
-- **Undo stack**: 50 operations per session
-- **File integrity hashes**: on-demand MD5 / SHA-1 / SHA-256 / SHA-512 / BLAKE2b with one-click copy
-- **Cyber dark theme**: professional dark UI with cyan/blue accents
-- **Compiled binaries**: no Python or Node.js required to run
+| 📄 Document | 📝 Purpose |
+| ------------------------------------------- | -------------------------------------------- |
+| [📋 REQUIREMENTS.md](docs/REQUIREMENTS.md) | System requirements and prerequisites |
+| [🚀 INSTALLATION.md](docs/INSTALLATION.md) | Step-by-step installation & build-from-source |
+| [📖 USAGE.md](docs/USAGE.md) | How to use the application |
+| [📡 API.md](docs/API.md) | Sidecar REST API reference for developers |
---
-## Download
+## ⚡ Quick Start
-See [Releases](../../releases) for pre-built binaries:
+### 🪟 Windows / 🐧 Linux
-| Platform | Download |
-|---|---|
-| Windows 11 | `MetaLens-vX.Y.Z-Setup-Windows.exe` |
-| Linux (deb) | `MetaLens-vX.Y.Z-Linux.deb` |
-| Linux (rpm) | `MetaLens-vX.Y.Z-Linux.rpm` |
-| Linux (tar) | `MetaLens-vX.Y.Z-Linux.tar.gz` |
+1. Grab the latest installer for your platform from [Releases](../../releases)
+2. Run it — Windows SmartScreen may warn since the build is unsigned; click **"More info" → "Run anyway"**
+3. MetaLens opens with a folder tree on the left — click a folder, then a file, to see its metadata
----
+> ⏱️ **No install wait, no dependency download.** Python and Node.js are bundled into the binary — nothing to set up.
-## Build from Source
+> [!IMPORTANT]
+> ✅ For full details, see [INSTALLATION.md](docs/INSTALLATION.md)
-### Prerequisites
-- Python 3.11–3.13
-- Node.js 20+
-- npm 10+
+---
-### Steps
+## 🎯 What It Does
-```bash
-# 1. Clone the repository
-git clone https://github.com/YOUR_USERNAME/MetaLens.git
-cd MetaLens
-
-# 2. Install Python dependencies
-cd python
-python -m venv .venv
-source .venv/bin/activate # Linux
-# .venv\Scripts\activate # Windows
-pip install -r requirements.txt
-
-# 3. Install Node dependencies
-cd ../electron && npm install
-cd ../frontend && npm install
-
-# 4. Run in development mode
-cd ../electron && npm start
-
-# 5. Build for production
-cd python && pyinstaller main.py --onefile --name metalens-sidecar
-cd ../frontend && npm run build
-cd ../electron && npm run make
```
+File (image / audio / video / document / anything)
+ │
+ ▼
+┌─────────────────────────────────────────────┐
+│ 🖼️ Images → EXIF, IPTC, XMP (+ RAW read) │
+│ 🎵 Audio → ID3, Vorbis comments, tags │
+│ 🎬 Video → container metadata │
+│ 📄 PDF → document info dictionary │
+│ 📊 Office → DOCX/XLSX/PPTX properties │
+│ 📁 Any file → filesystem timestamps, │
+│ permissions, xattr │
+└─────────────────────────────────────────────┘
+ │
+ ▼
+ 👁️ View · ✏️ Edit · 🗑️ Delete · ⚖️ Diff · 🔑 Hash
+```
+
+---
+
+## ✨ Key Features
+
+- 🗂️ **File-manager UI** — folder tree + file list + metadata detail panel
+- 🖼️ **All major formats** — images (JPEG/PNG/TIFF/WebP/RAW), audio (MP3/FLAC/OGG/M4A/WAV), video (MP4/MKV/MOV), documents (PDF/DOCX/XLSX/PPTX/DOC/XLS), and any other file via filesystem metadata
+- ✏️ **Full operations** — read, edit, delete individual fields, compare/diff two files
+- 🔒 **Atomic writes** — temp-file + rename, no file corruption on failure
+- ↩️ **Undo stack** — 50 operations per session
+- 🔑 **File integrity hashes** — on-demand MD5 / SHA-1 / SHA-256 / SHA-512 / BLAKE2b with one-click copy
+- 🎨 **Cyber dark theme** — professional dark UI with cyan/blue accents
+- 📦 **Compiled binaries** — no Python or Node.js required to run
+- 💾 **Offline-first** — zero network calls, zero telemetry
+- 🆓 **Free & open-source** — MIT license
---
-## Supported Formats
+## 🔧 Version
+
+**v0.2.1** — Routine dependency updates (Electron 43.1, Vite 8.1.4, uvicorn 0.51, lucide-react 1.24). No changes to application behavior or the API.
+
+📖 **See full version history** → [CHANGELOG.md](./CHANGELOG.md)
+
+---
+
+## 📋 System Requirements
+
+- **Windows** 10/11 (64-bit) or a modern **Linux** distro (`.deb`/`.rpm`/`.tar.gz`)
+- **RAM** 512 MB free
+- **Disk** ~300 MB installed
+- No Python or Node.js needed to *run* the app — only to build it from source
+
+> [!IMPORTANT]
+> ✅ For complete requirements, see [REQUIREMENTS.md](docs/REQUIREMENTS.md)
+
+---
+
+## 🚀 Getting Started
+
+### 1️⃣ Install
+Follow [INSTALLATION.md](docs/INSTALLATION.md) for Windows/Linux binaries or building from source.
+
+### 2️⃣ Start Browsing
+Learn the interface in [USAGE.md](docs/USAGE.md).
+
+### 💻 For Developers
+Explore the sidecar API in [API.md](docs/API.md).
+
+---
+
+## 🗂️ Supported Formats
| Category | Formats | Read | Write |
|---|---|---|---|
@@ -89,9 +127,9 @@ cd ../electron && npm run make
| Any file | Filesystem timestamps, permissions, xattr | ✅ | ✅ |
| Other | Hundreds of formats via hachoir | ✅ | — |
-### File Integrity
+### 🔑 File Integrity
-Compute cryptographic hashes on demand from the **Metadata** tab — no impact on browsing performance:
+Compute cryptographic hashes on demand from the **Metadata** panel — no impact on browsing performance:
| Algorithm | Notes |
|---|---|
@@ -103,7 +141,13 @@ Compute cryptographic hashes on demand from the **Metadata** tab — no impact o
---
-## Architecture
+## 🏗️ Architecture
+
+| Layer | Technology | Notes |
+|---|---|---|
+| **Desktop shell** | Electron (Node.js main process) | Spawns the Python sidecar, native menus & dialogs |
+| **Renderer UI** | React 19, Vite, TailwindCSS v4 | Folder tree, file list, metadata detail panel |
+| **Sidecar** | Python 3.11–3.13, FastAPI, uvicorn | Metadata extraction/write engine, `127.0.0.1` only |
```
Electron Main (Node.js)
@@ -116,7 +160,8 @@ Python Sidecar (FastAPI)
├── /read — full metadata extraction
├── /write — atomic metadata write-back
├── /delete — field removal
- └── /diff — metadata comparison
+ ├── /diff — metadata comparison
+ └── /hash — on-demand file hashing
React Frontend (Vite + TailwindCSS)
├── FolderPanel — folder tree navigation
@@ -124,16 +169,70 @@ React Frontend (Vite + TailwindCSS)
└── DetailPanel — View / Edit / Diff tabs
```
+📖 Full endpoint reference → [API.md](docs/API.md)
+
+---
+
+## 📊 Test Suite
+
+✅ **48 automated tests** covering handlers, the handler registry, and path-security validation.
+
+```bash
+pytest python/tests/ -v
+```
+
---
-## License
+## 🔐 Privacy & Security
-MIT License — © 2026 Graziano Mariella
+- 🛡️ **No cloud dependencies** — everything runs locally
+- 🔒 **No telemetry** — zero data collection
+- 📁 **Localhost-only sidecar** — the Python API never binds beyond `127.0.0.1`
+- 🔓 **Open source** — fully auditable code
+- ⚡ **Fully offline** — no internet connection required, ever
-See [LICENSE](LICENSE) for full text.
+📖 See [SECURITY.md](./SECURITY.md) for the full security model.
---
-## Credits
+## 🤝 Contributing
+
+Contributions are welcome! Please:
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/your-feature`)
+3. Commit changes with clear messages
+4. Push to your fork
+5. Open a Pull Request to `develop` branch
+
+📖 See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide.
+
+> [!NOTE]
+> 📖 All PRs should target the **`develop`** branch, not `main`.
+
+---
+
+## 📜 License
+
+Distributed under the **MIT License**. See [LICENSE](LICENSE) for details.
+
+---
+
+## 🙋 Support
+
+- 📖 **Documentation:** See [docs/](docs/) folder
+- 🐛 **Report issues:** [GitHub Issues](https://github.com/overwrite00/MetaLens/issues)
+- 💬 **Questions:** Open a [GitHub Discussion](https://github.com/overwrite00/MetaLens/discussions)
+
+---
+
+## 👨💻 Credits
+
+Developed by **Graziano Mariella**
+
+Distributed with MIT License · [View License](LICENSE)
+
+---
-Developed by **Graziano Mariella**.
+*Last updated: 2026-07-14*
+*← [Requirements](docs/REQUIREMENTS.md) | [Docs →](docs/)*
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..9fc06ef
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,108 @@
+# 🔒 Security Policy
+
+## Security Model
+
+MetaLens is designed with security-first principles:
+
+- **Fully offline** — all metadata reading and writing happens on your machine; there is no network call anywhere in the app
+- **Localhost-only sidecar** — the Python (FastAPI) backend binds to `127.0.0.1` only, on a dynamically chosen port, never `0.0.0.0`
+- **Electron hardening** — `nodeIntegration: false`, `contextIsolation: true`; the renderer only reaches the OS through a narrow `contextBridge` API (folder/file dialogs, drive listing), never raw Node/`fs` access
+- **Atomic writes** — every metadata write copies the file to a `.ml_tmp` temp file, applies the change there, then swaps it in with `os.replace()` (atomic on POSIX and Windows). A crash mid-write leaves the original file untouched
+- **Path validation** — a dedicated `core.path_security` module validates and normalizes every path (resolves symlinks, blocks traversal) before any filesystem access, across all API routes
+- **Transparent code** — all handler and analysis logic is open-source and auditable
+
+---
+
+## 🛡️ Privacy Principles
+
+### What We Don't Do
+
+- ❌ No user accounts or authentication
+- ❌ No telemetry or usage tracking
+- ❌ No data collection or analytics
+- ❌ No cloud processing or storage
+- ❌ No auto-update phone-home (updates are manual, via GitHub Releases)
+
+### Data Control
+
+- ✅ Your files and their metadata never leave your machine
+- ✅ Nothing is written anywhere except the file you explicitly edit
+- ✅ The undo stack lives in memory only — it is cleared when you close the app, never written to disk
+- ✅ Full source code transparency
+
+---
+
+## 🔐 Technical Security
+
+### Metadata Handlers
+
+- Uses established, audited libraries per format: Pillow/piexif (images), mutagen (audio/video), pypdf (PDF), python-docx/openpyxl/python-pptx (Office), olefile (legacy Office), hachoir (fallback)
+- Read-only handlers (RAW images, legacy Office, MKV/AVI) are enforced at the API level — `/write` and `/delete` return `422` if the handler doesn't support the operation
+- Filesystem access always goes through `validate_file_path`/`validate_directory_path` before touching disk
+
+### IPC Boundary
+
+- The Electron renderer talks to the OS only through the `preload.js` `contextBridge` surface (dialogs, path helpers, drive listing) — no direct filesystem or process access
+- The renderer talks to the Python sidecar only over HTTP to `127.0.0.1:{dynamic_port}`, never a hardcoded port
+
+---
+
+## ⚠️ Known Limitations
+
+### What MetaLens Does NOT Do
+
+- **Code-signed installers** — releases are currently unsigned (no purchased certificate). Windows SmartScreen will warn on first run; this is expected, not a compromise indicator. See [Installation Guide](./docs/INSTALLATION.md) for how to proceed safely
+- **Malware/content scanning** — MetaLens edits metadata; it does not scan file contents for malicious payloads
+- **Encryption** — files and their metadata are stored and edited in plain form on disk, as with any local file manager
+- **Guarantee against all data loss** — while writes are atomic, always keep backups of files you can't afford to lose
+
+### Defense-in-Depth Recommended
+
+For files handled outside MetaLens, also rely on your OS's built-in protections (Windows Defender / your Linux distro's package signing) and keep regular backups.
+
+---
+
+## 📢 Responsible Disclosure
+
+If you discover a security vulnerability in MetaLens:
+
+### Reporting Process
+
+1. **Do not open a public GitHub issue** — this exposes the vulnerability before a fix ships
+2. **Use GitHub Security Advisory**:
+ - Navigate to: https://github.com/overwrite00/MetaLens/security/advisories
+ - Click "Report a vulnerability"
+ - Describe the vulnerability in detail, including reproduction steps
+
+### Timeline
+
+- **48 hours**: acknowledgment of your report
+- **2 weeks** (critical): target fix timeline for critical vulnerabilities
+- **30 days** (high): target fix timeline for high-severity vulnerabilities
+- **Coordinated disclosure**: we'll work with you on a disclosure timeline
+
+### Recognition
+
+We appreciate responsible disclosure and will credit you in the security advisory (unless you prefer anonymity).
+
+---
+
+## 🔄 Security Updates
+
+- **Subscribe to releases**: Watch the [Releases](https://github.com/overwrite00/MetaLens/releases) page
+- **Check security advisories**: [GitHub Security Advisory](https://github.com/overwrite00/MetaLens/security/advisories)
+- **Update regularly**: dependency security fixes are tracked in [CHANGELOG.md](./CHANGELOG.md)
+
+---
+
+## 📚 Additional Resources
+
+- [Code of Conduct](./CODE_OF_CONDUCT.md) — community guidelines
+- [Contributing Guide](./CONTRIBUTING.md) — how to contribute securely
+- [REQUIREMENTS.md](./docs/REQUIREMENTS.md) — dependency information
+- [CHANGELOG.md](./CHANGELOG.md) — version history and security fixes
+
+---
+
+*Last updated: 2026-07-14*
+*← [Contributing](./CONTRIBUTING.md) | [Back to README →](./README.md)*
diff --git a/docs/API.md b/docs/API.md
new file mode 100644
index 0000000..63dabd8
--- /dev/null
+++ b/docs/API.md
@@ -0,0 +1,190 @@
+# 📡 API Reference — MetaLens
+
+REST API exposed by the Python (FastAPI) sidecar that the Electron main process talks to over `127.0.0.1`.
+
+> [!IMPORTANT]
+> 🔒 The sidecar binds to `127.0.0.1` **only**, on a port chosen dynamically at launch (never `0.0.0.0`, never a fixed port). It is not reachable from other machines and is not intended to be called directly by end users — this reference is for developers extending MetaLens.
+
+---
+
+## 📚 Table of Contents
+
+1. [GET /health](#get-health)
+2. [GET /list](#get-list)
+3. [GET /read](#get-read)
+4. [POST /write](#post-write)
+5. [POST /delete](#post-delete)
+6. [GET /diff](#get-diff)
+7. [GET /hash](#get-hash)
+8. [📦 Data Models](#-data-models)
+
+---
+
+## GET `/health`
+
+Health check + version, used by the About dialog and by Electron to know the sidecar is ready.
+
+**Response**
+```json
+{ "status": "ok", "version": "0.2.1", "app": "MetaLens" }
+```
+
+---
+
+## GET `/list`
+
+Lists the contents of a directory with detected handler per file.
+
+| Param | Type | Description |
+|---|---|---|
+| `path` | string (query) | Absolute directory path |
+
+**Response**
+```json
+{
+ "path": "/home/user/Pictures",
+ "items": [
+ { "name": "photo.jpg", "path": "/home/user/Pictures/photo.jpg", "is_dir": false, "size": 2048301, "mtime": 1752480000, "ext": ".jpg", "handler": "image" }
+ ]
+}
+```
+
+`handler` is `"—"` when no dedicated handler matches (filesystem-only metadata still applies).
+
+---
+
+## GET `/read`
+
+Reads full metadata for a single file. Resolves the matching handler, then merges filesystem metadata as a fallback layer for any key not already populated by the handler.
+
+| Param | Type | Description |
+|---|---|---|
+| `path` | string (query) | Absolute file path |
+
+**Response** — a [`MetadataRecord`](#-data-models)
+
+---
+
+## POST `/write`
+
+Atomically writes one or more metadata fields back to the file.
+
+**Body**
+```json
+{
+ "path": "/home/user/Pictures/photo.jpg",
+ "fields": [
+ { "key": "Artist", "value": "Jane Doe" }
+ ]
+}
+```
+
+**Response**
+```json
+{ "ok": true, "errors": [] }
+```
+
+Returns **422** if the file's handler reports `supports_write: false` (e.g. RAW images, legacy Office formats, MKV/AVI).
+
+---
+
+## POST `/delete`
+
+Removes the specified metadata keys from the file.
+
+**Body**
+```json
+{ "path": "/home/user/Pictures/photo.jpg", "keys": ["GPSLatitude", "GPSLongitude"] }
+```
+
+**Response**
+```json
+{ "ok": true, "errors": [] }
+```
+
+Returns **422** if the handler reports `supports_delete: false`.
+
+---
+
+## GET `/diff`
+
+Compares metadata between two files.
+
+| Param | Type | Description |
+|---|---|---|
+| `a` | string (query) | Absolute path to the first file |
+| `b` | string (query) | Absolute path to the second file |
+
+**Response** — a [`DiffResult`](#-data-models)
+
+---
+
+## GET `/hash`
+
+Computes cryptographic hashes for a file on demand, streaming it in 1 MB chunks (no full read into memory).
+
+| Param | Type | Description |
+|---|---|---|
+| `path` | string (query) | Absolute file path |
+| `algorithms` | string (query, optional) | Comma-separated list. Default: `md5,sha1,sha256,sha512,blake2b` |
+
+**Response**
+```json
+{
+ "path": "/home/user/Pictures/photo.jpg",
+ "size": 2048301,
+ "hashes": { "md5": "...", "sha1": "...", "sha256": "...", "sha512": "...", "blake2b": "..." }
+}
+```
+
+Returns **400** if an unsupported algorithm name is requested.
+
+---
+
+## ⚠️ Error Handling
+
+All path-based endpoints validate input through `validate_file_path` / `validate_directory_path` before touching the filesystem (existence checks, traversal protection) and return **400** on violation.
+
+---
+
+## 📦 Data Models
+
+Defined in `python/core/models.py`.
+
+### `MetadataField`
+| Field | Type | Notes |
+|---|---|---|
+| `key` | string | Raw field identifier |
+| `label` | string | Human-readable label |
+| `value` | any | Current value |
+| `raw_value` | any | Unformatted underlying value |
+| `editable` | bool | Whether `/write` accepts this field |
+| `deletable` | bool | Whether `/delete` accepts this key |
+| `field_type` | enum | `str \| int \| float \| datetime \| bytes \| enum` |
+| `source` | enum | `exif \| iptc \| xmp \| id3 \| vorbis \| pdf \| filesystem \| ...` |
+| `choices` | array | Present only for `enum` fields |
+
+### `MetadataRecord`
+| Field | Type |
+|---|---|
+| `file_path` | string |
+| `file_size` | int |
+| `mtime` | float |
+| `handler_name` | string |
+| `fields` | `MetadataField[]` |
+| `read_errors` | string[] |
+| `supports_write` | bool |
+| `supports_delete` | bool |
+
+### `DiffResult`
+| Field | Type |
+|---|---|
+| `file_a` / `file_b` | string |
+| `only_in_a` / `only_in_b` | `MetadataField[]` |
+| `changed` | `[MetadataField, MetadataField][]` |
+| `identical` | `MetadataField[]` |
+
+---
+
+*Last updated: 2026-07-14*
+*← [Usage](./USAGE.md) | [Back to README →](../README.md)*
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
new file mode 100644
index 0000000..8ec844a
--- /dev/null
+++ b/docs/INSTALLATION.md
@@ -0,0 +1,153 @@
+# 🚀 Installation Guide — MetaLens
+
+Step-by-step instructions for installing MetaLens on Windows and Linux, plus building it from source.
+
+> [!IMPORTANT]
+> 💻 **Prerequisites:** Review [REQUIREMENTS.md](./REQUIREMENTS.md) before starting.
+
+---
+
+## 📋 Table of Contents
+
+1. [🪟 Windows Installation](#-windows-installation)
+2. [🐧 Linux Installation](#-linux-installation)
+3. [✅ Verify Installation](#-verify-installation)
+4. [🛠️ Build from Source](#-build-from-source)
+5. [🔧 Troubleshooting](#-troubleshooting)
+
+---
+
+## 🪟 Windows Installation
+
+1. Go to the [Releases](../../../releases) page
+2. Download **`MetaLens-vX.Y.Z-Setup-Windows.exe`** (latest version)
+3. Double-click the downloaded file
+
+> [!WARNING]
+> ⚠️ **Installers are not code-signed.** Windows SmartScreen will show "Windows protected your PC". Click **"More info"** → **"Run anyway"** to proceed. This is expected for an unsigned open-source build — see [SECURITY.md](../SECURITY.md) for details.
+
+4. The installer (Squirrel.Windows) runs per-user — no admin rights required
+5. MetaLens launches automatically after installation and adds a Start Menu shortcut
+
+---
+
+## 🐧 Linux Installation
+
+### 📦 Debian / Ubuntu (`.deb`)
+
+```bash
+wget https://github.com/overwrite00/MetaLens/releases/download/vX.Y.Z/MetaLens-vX.Y.Z-Linux.deb
+sudo apt install ./MetaLens-vX.Y.Z-Linux.deb
+```
+
+### 📦 Fedora / RHEL (`.rpm`)
+
+```bash
+wget https://github.com/overwrite00/MetaLens/releases/download/vX.Y.Z/MetaLens-vX.Y.Z-Linux.rpm
+sudo rpm -i MetaLens-vX.Y.Z-Linux.rpm
+```
+
+### 📦 Any distro (`.tar.gz`)
+
+```bash
+wget https://github.com/overwrite00/MetaLens/releases/download/vX.Y.Z/MetaLens-vX.Y.Z-Linux.tar.gz
+tar -xzf MetaLens-vX.Y.Z-Linux.tar.gz
+cd MetaLens-vX.Y.Z-Linux
+./metalens
+```
+
+> [!NOTE]
+> 📖 Replace `vX.Y.Z` with the version shown on the [Releases](../../../releases) page.
+
+---
+
+## ✅ Verify Installation
+
+Launch MetaLens, then open **Help → About** (or the About dialog) to confirm the version number matches the one you installed — it's read live from the sidecar's `/health` endpoint.
+
+You should see:
+- ✅ A folder tree on the left (drives and home directory)
+- ✅ An empty file list until you select a folder
+- ✅ No errors in the window
+
+---
+
+## 🛠️ Build from Source
+
+Only needed for development or if no pre-built binary exists for your platform.
+
+```bash
+# 1. Clone the repository
+git clone https://github.com/overwrite00/MetaLens.git
+cd MetaLens
+
+# 2. Install Python dependencies
+cd python
+python -m venv .venv
+source .venv/bin/activate # Linux
+# .venv\Scripts\activate # Windows
+pip install -r requirements.txt
+
+# 3. Install Node dependencies
+cd ../electron && npm install
+cd ../frontend && npm install
+
+# 4. Run in development mode
+cd ../electron && npm start
+
+# 5. Build for production
+cd ../python && pyinstaller main.py --onefile --name metalens-sidecar
+cd ../frontend && npm run build
+cd ../electron && npm run make
+```
+
+`npm run make` (Electron Forge) produces the Windows `.exe`, Linux `.deb`/`.rpm` under `electron/out/make/`.
+
+---
+
+## 🔧 Troubleshooting
+
+### ❌ "Windows protected your PC" (SmartScreen)
+
+**Cause:** The installer is not code-signed (no certificate purchased for this open-source project).
+
+**Solution:** Click **"More info"** → **"Run anyway"**. Verify you downloaded from the official [Releases](../../../releases) page if in doubt.
+
+---
+
+### ❌ `.deb`/`.rpm` install fails with dependency errors
+
+**Cause:** Missing system libraries used by Electron (e.g. `libgtk-3-0`, `libnotify4`).
+
+**Solution:**
+```bash
+sudo apt --fix-broken install # Debian/Ubuntu, after a failed .deb install
+```
+
+---
+
+### ❌ App window opens but the file list never loads
+
+**Cause:** The Python sidecar failed to start (rare — usually a port conflict or antivirus blocking the bundled executable).
+
+**Solution:** Restart the app. If it persists, check that no other process is holding the port MetaLens tries to bind on `127.0.0.1`, and confirm your antivirus isn't quarantining `metalens-sidecar.exe`.
+
+---
+
+### ❌ `pyinstaller` or `npm run make` fails while building from source
+
+**Cause:** Wrong Python/Node version, or dependencies not installed in the right subfolder.
+
+**Solution:** Confirm Python 3.11–3.13 and Node.js 20+ per [REQUIREMENTS.md](./REQUIREMENTS.md), and re-run `pip install -r requirements.txt` / `npm install` in the exact folders shown above.
+
+---
+
+## ✅ What's Next?
+
+- **First time?** → Learn the interface in [USAGE.md](./USAGE.md)
+- **Developer?** → [API.md](./API.md)
+
+---
+
+*Last updated: 2026-07-14*
+*← [Requirements](./REQUIREMENTS.md) | [Usage →](./USAGE.md)*
diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md
new file mode 100644
index 0000000..aa0242c
--- /dev/null
+++ b/docs/REQUIREMENTS.md
@@ -0,0 +1,93 @@
+# 📋 System Requirements — MetaLens
+
+Requirements for running the pre-built MetaLens application and, separately, for building it from source.
+
+> [!TIP]
+> 🚀 **Just want to use the app?** You don't need Python or Node.js — download a pre-built binary and skip straight to the [Installation Guide](./INSTALLATION.md).
+
+---
+
+## 📊 Quick Reference
+
+| 📦 Use case | ✅ Requirement |
+|---|---|
+| **Run the compiled app** | Windows 10/11 (64-bit) or a modern Linux distro — nothing else |
+| **Build from source** | Python 3.11–3.13, Node.js 20+, npm 10+ |
+
+---
+
+## 🖥️ Operating System Support
+
+### 🪟 Windows
+- **Minimum:** Windows 10 (64-bit)
+- **Tested on:** Windows 10, Windows 11
+- **Installer:** `MetaLens-vX.Y.Z-Setup-Windows.exe` (Squirrel.Windows)
+- **Status:** ✅ Fully supported
+
+### 🐧 Linux
+- **Packages:** `.deb` (Debian/Ubuntu), `.rpm` (Fedora/RHEL), `.tar.gz` (any distro)
+- **Status:** ✅ Fully supported
+
+### 🍎 macOS
+- **Status:** ❌ Not currently packaged — no macOS build is produced by CI. Building from source with `npm run make` is possible but unverified.
+
+---
+
+## 💻 Hardware Requirements
+
+| 🔧 Resource | 📊 Requirement |
+|---|---|
+| **Processor** | Any modern 64-bit CPU |
+| **RAM** | 512 MB free (Electron + Python sidecar) |
+| **Storage** | ~300 MB for the installed application |
+
+---
+
+## ❌ What You DON'T Need
+
+| ❌ | ℹ️ Why you don't need it |
+|---|---|
+| **Python** | The sidecar ships as a compiled binary (`metalens-sidecar[.exe]`) inside the installer |
+| **Node.js** | The frontend is pre-built and bundled into the Electron app |
+| **Internet connection** | MetaLens works fully offline — no cloud dependency, no telemetry |
+| **Admin/root rights** | Windows installer runs per-user (Squirrel); Linux packages install standard desktop entries |
+
+---
+
+## 🛠️ Building From Source
+
+Only needed if you want to modify MetaLens or package it yourself.
+
+| 📦 Tool | ✅ Version | 📝 Notes |
+|---|---|---|
+| **Python** | 3.11 – 3.13 | Used to run/package the FastAPI sidecar |
+| **Node.js** | 20+ | Runs Electron and the Vite build |
+| **npm** | 10+ | Installs both `electron/` and `frontend/` dependencies |
+
+See [Installation Guide → Build from Source](./INSTALLATION.md#-build-from-source) for exact steps.
+
+---
+
+## 🔒 Data Privacy & Security
+
+> [!IMPORTANT]
+> 🛡️ **Your files never leave your machine.** MetaLens has no network calls of any kind.
+
+- ✅ **No cloud transmission** — all metadata reads/writes happen locally
+- ✅ **No telemetry** — zero data collection or external tracking
+- ✅ **Localhost-only sidecar** — the Python API binds to `127.0.0.1` only, never `0.0.0.0`
+- ✅ **Open source** — full source code available for audit (MIT license)
+- ✅ **Offline capable** — no internet connection required at any point
+
+---
+
+## 🚀 Next Steps
+
+1. **🆕 New to MetaLens?** → [Installation Guide](./INSTALLATION.md)
+2. **📖 Ready to browse files?** → [Usage Guide](./USAGE.md)
+3. **💻 Developer?** → [API Reference](./API.md)
+
+---
+
+*Last updated: 2026-07-14*
+*← [README](../README.md) | [Installation →](./INSTALLATION.md)*
diff --git a/docs/USAGE.md b/docs/USAGE.md
new file mode 100644
index 0000000..ec374fa
--- /dev/null
+++ b/docs/USAGE.md
@@ -0,0 +1,105 @@
+# 📖 Usage Guide — MetaLens
+
+Learn how to browse files, inspect and edit metadata, compute hashes, and compare two files.
+
+> [!TIP]
+> 💡 **First time?** Install the app first — see the [Installation Guide](./INSTALLATION.md).
+
+---
+
+## 📚 Table of Contents
+
+1. [🌐 Open the App](#-open-the-app)
+2. [📁 Browse Folders](#-browse-folders)
+3. [📄 Select a File](#-select-a-file)
+4. [👁️ View Metadata](#-view-metadata)
+5. [✏️ Edit Metadata](#-edit-metadata)
+6. [🗑️ Delete Metadata Fields](#-delete-metadata-fields)
+7. [🔑 Compute File Hashes](#-compute-file-hashes)
+8. [⚖️ Compare Two Files (Diff)](#-compare-two-files-diff)
+9. [↩️ Undo](#-undo)
+
+---
+
+## 🌐 Open the App
+
+Launch MetaLens from the Start Menu (Windows) or your application launcher (Linux). The window opens at 1280×800 with three panels: a folder tree on the left, a file list in the center, and an empty detail panel on the right.
+
+---
+
+## 📁 Browse Folders
+
+The **left panel** (`FolderPanel`) lists your available drives and home directory. Click any entry to expand it and navigate into subfolders — MetaLens reads directly from the local filesystem, nothing is uploaded or synced anywhere.
+
+---
+
+## 📄 Select a File
+
+The **center panel** (`FilePanel`) shows every file in the currently selected folder, with an icon colored by extension. Click a file to load its metadata into the detail panel on the right.
+
+> [!NOTE]
+> 📖 Every file type is supported in some form — recognized formats get a dedicated handler (EXIF, ID3, PDF info, Office properties, …); anything else falls back to filesystem metadata (name, size, timestamps, permissions).
+
+---
+
+## 👁️ View Metadata
+
+The **detail panel** (`DetailPanel`) opens on the **View** tab by default, showing a read-only, grouped table (`MetadataTable`) of every field MetaLens extracted — grouped and color-coded by source (EXIF, IPTC, XMP, ID3, Vorbis comments, PDF info, filesystem, …).
+
+---
+
+## ✏️ Edit Metadata
+
+Switch to the **Edit** tab to open `MetadataEditor`:
+
+- Editable fields show an input matching their type (text, number, date, or a dropdown for enumerated values)
+- Change a value and save — MetaLens writes it back **atomically**: it copies the file to a `.ml_tmp` temp file, applies the change there, then atomically replaces the original (`os.replace`) so a crash or write error never corrupts your file
+- If the file's handler doesn't support writing (e.g. RAW images, legacy Office `.doc`/`.xls`/`.ppt`, MKV/AVI video), the Edit tab shows a **read-only** notice instead of an editable form
+
+---
+
+## 🗑️ Delete Metadata Fields
+
+Individual fields marked as **deletable** can be removed from the Edit tab. Deletion uses the same atomic write-back as editing — the original file is only touched once the operation succeeds.
+
+---
+
+## 🔑 Compute File Hashes
+
+From the **Metadata** area of the detail panel, compute cryptographic hashes **on demand** (not automatically, to avoid slowing down browsing):
+
+| Algorithm | Notes |
+|---|---|
+| MD5 | Legacy, widely used for checksums |
+| SHA-1 | Legacy, used by Git and many tools |
+| SHA-256 | Modern standard, recommended |
+| SHA-512 | High-security workloads |
+| BLAKE2b | Fast modern algorithm, built into Python |
+
+Each computed hash has a one-click **copy to clipboard** button.
+
+---
+
+## ⚖️ Compare Two Files (Diff)
+
+Switch to the **Diff** tab (`DiffView`) to compare two files side by side:
+
+- Fields only present in file A, only in file B, changed between the two, and identical fields are each shown in their own section
+- The diff view also includes its own hash comparison section, so you can confirm at a glance whether two files are byte-identical
+
+---
+
+## ↩️ Undo
+
+MetaLens keeps an in-memory undo stack of your last **50** write/delete operations for the current session (`useUndoStack`). It is **not** persisted to disk — closing the app clears it. Trigger undo from the **Edit** menu or its keyboard shortcut.
+
+---
+
+## ℹ️ Checking Your Version
+
+Open **Help → About** to see the running app version, read live from the Python sidecar's `/health` endpoint — useful when reporting a bug.
+
+---
+
+*Last updated: 2026-07-14*
+*← [Installation](./INSTALLATION.md) | [API Reference →](./API.md)*
diff --git a/electron/forge.config.js b/electron/forge.config.js
index 274a7ce..5c41b2e 100644
--- a/electron/forge.config.js
+++ b/electron/forge.config.js
@@ -8,6 +8,10 @@ const iconExists = ['.ico', '.icns', '.png'].some(ext => fs.existsSync(iconPath
// Read version from package.json — synced by the CI workflow from python/config.py
const { version } = require('./package.json')
+// RPM's Version field forbids "-" (used for beta tags like 0.2.1-beta.5),
+// so the rpm maker gets a sanitized copy while every other maker keeps the real semver.
+const rpmSafeVersion = version.replace(/-/g, '')
+
module.exports = {
packagerConfig: {
name: 'MetaLens',
@@ -28,6 +32,10 @@ module.exports = {
config: {
name: 'MetaLens',
setupIcon: path.join(__dirname, '..', 'frontend', 'public', 'icon.ico'),
+ // Squirrel names the generated file " Setup.exe" by default, which
+ // reads as a traditional installer — it's actually a self-contained
+ // Squirrel bootstrapper, so give it a neutral name instead.
+ setupExe: `MetaLens-${version}-win-x64.exe`,
},
},
{ name: '@electron-forge/maker-zip', platforms: ['darwin'] },
@@ -57,6 +65,7 @@ module.exports = {
license: 'MIT',
icon: path.join(__dirname, '..', 'frontend', 'public', 'icon.png'),
categories: ['Utility', 'FileManager'],
+ version: rpmSafeVersion,
},
},
},
diff --git a/electron/package-lock.json b/electron/package-lock.json
index 70d61c1..a3d7a79 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "metalens",
- "version": "0.1.5",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metalens",
- "version": "0.1.5",
+ "version": "0.2.0",
"license": "MIT",
"devDependencies": {
"@electron-forge/cli": "^7.6.0",
@@ -14,7 +14,7 @@
"@electron-forge/maker-rpm": "^7.6.0",
"@electron-forge/maker-squirrel": "^7.6.0",
"@electron-forge/maker-zip": "^7.11.2",
- "electron": "^43.0.0"
+ "electron": "^43.1.0"
}
},
"node_modules/@electron-forge/cli": {
@@ -2399,9 +2399,9 @@
"license": "MIT"
},
"node_modules/electron": {
- "version": "43.0.0",
- "resolved": "https://registry.npmjs.org/electron/-/electron-43.0.0.tgz",
- "integrity": "sha512-PV60GsWU6qufhuOhw3n+Yix3WPDcqDtBqE8orbEQGQGHEkgp9o/JCPgb7L4vIL0r1HnfPdqSRtboOTqbDkcFDQ==",
+ "version": "43.1.0",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-43.1.0.tgz",
+ "integrity": "sha512-DPfxpQLd4NL3BJ8DBxYAfmLUKKesF5Rx9dQx5FyczAP8bhOPScjHE48GArVeXu68LlAainuwkmQTQvdZwpIIAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/electron/package.json b/electron/package.json
index 7ce6af4..8b0f572 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -1,6 +1,6 @@
{
"name": "metalens",
- "version": "0.2.0",
+ "version": "0.2.1",
"description": "Universal File Metadata Manager",
"author": "Graziano Mariella",
"license": "MIT",
@@ -15,6 +15,6 @@
"@electron-forge/maker-rpm": "^7.6.0",
"@electron-forge/maker-squirrel": "^7.6.0",
"@electron-forge/maker-zip": "^7.11.2",
- "electron": "^43.0.0"
+ "electron": "^43.1.0"
}
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a49694c..ffbcabc 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,14 +1,14 @@
{
"name": "metalens-frontend",
- "version": "0.1.5",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metalens-frontend",
- "version": "0.1.5",
+ "version": "0.2.0",
"dependencies": {
- "lucide-react": "^1.23.0",
+ "lucide-react": "^1.24.0",
"react": "^19.1.0",
"react-dom": "^19.2.7"
},
@@ -16,7 +16,7 @@
"@tailwindcss/vite": "^4.3.2",
"@vitejs/plugin-react": "^6.0.3",
"tailwindcss": "^4.3.2",
- "vite": "^8.1.3"
+ "vite": "^8.1.4"
}
},
"node_modules/@emnapi/core": {
@@ -1083,9 +1083,9 @@
}
},
"node_modules/lucide-react": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
- "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
+ "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1287,16 +1287,16 @@
"optional": true
},
"node_modules/vite": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
- "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
+ "version": "8.1.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
+ "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
- "picomatch": "^4.0.4",
+ "picomatch": "^4.0.5",
"postcss": "^8.5.16",
- "rolldown": "~1.1.3",
+ "rolldown": "~1.1.4",
"tinyglobby": "^0.2.17"
},
"bin": {
diff --git a/frontend/package.json b/frontend/package.json
index cdfe91f..5b81c51 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "metalens-frontend",
- "version": "0.2.0",
+ "version": "0.2.1",
"private": true,
"type": "module",
"scripts": {
@@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
- "lucide-react": "^1.23.0",
+ "lucide-react": "^1.24.0",
"react": "^19.1.0",
"react-dom": "^19.2.7"
},
@@ -17,6 +17,6 @@
"@tailwindcss/vite": "^4.3.2",
"@vitejs/plugin-react": "^6.0.3",
"tailwindcss": "^4.3.2",
- "vite": "^8.1.3"
+ "vite": "^8.1.4"
}
}
diff --git a/python/config.py b/python/config.py
index b7f1629..b38ac19 100644
--- a/python/config.py
+++ b/python/config.py
@@ -1,7 +1,7 @@
from dataclasses import dataclass
-VERSION = "0.2.0"
+VERSION = "0.2.1"
APP_NAME = "MetaLens"
SIDECAR_HOST = "127.0.0.1"
diff --git a/python/requirements.txt b/python/requirements.txt
index 2c384fc..7d78d96 100644
--- a/python/requirements.txt
+++ b/python/requirements.txt
@@ -3,7 +3,7 @@
# Web server
fastapi==0.139.0
-uvicorn==0.49.0
+uvicorn==0.51.0
# Images
Pillow==12.3.0
diff --git a/scripts/update_docs_dates.py b/scripts/update_docs_dates.py
new file mode 100644
index 0000000..3ed90f5
--- /dev/null
+++ b/scripts/update_docs_dates.py
@@ -0,0 +1,58 @@
+"""Update the '*Last updated: YYYY-MM-DD*' footer of staged Markdown docs to today.
+
+Invoked by the .githooks/pre-commit hook. Only touches files that are staged
+for commit and already contain a footer line matching the expected pattern.
+"""
+
+import datetime
+import re
+import subprocess
+import sys
+
+FOOTER_PATTERN = re.compile(r"^(\*Last updated: )\d{4}-\d{2}-\d{2}(\*[ \t]*)$", re.MULTILINE)
+
+
+def staged_markdown_files() -> list[str]:
+ output = subprocess.run(
+ ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
+ capture_output=True,
+ text=True,
+ check=True,
+ ).stdout
+ return [line for line in output.splitlines() if line.endswith(".md")]
+
+
+def update_file(path: str, today: str) -> bool:
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ new_content, count = FOOTER_PATTERN.subn(rf"\g<1>{today}\g<2>", content)
+ if count == 0 or new_content == content:
+ return False
+
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ return True
+
+
+def main() -> int:
+ today = datetime.date.today().isoformat()
+ changed = []
+
+ for path in staged_markdown_files():
+ try:
+ if update_file(path, today):
+ changed.append(path)
+ except FileNotFoundError:
+ continue
+
+ if changed:
+ subprocess.run(["git", "add", *changed], check=True)
+ for path in changed:
+ print(f"Updated 'Last updated' footer in {path} -> {today}")
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())