A modern web-based file management application built with Flask. This application provides a user-friendly interface for browsing, searching, and managing files in a specified directory, with support for metadata tracking, filtering, and sorting.
File Management
AI Image Processing (OCR + Description)
/processing dashboard: queue counts by task/status, paginated and
sortable jobs table with per-job filters (task, status, model,
filename), host/model breakdown, and bulk actions.Advanced Filtering & Sorting
finished_at of the
file's current OCR or VLM run)User Interface
File Operations
data/ directory; WAL mode, FTS5)/
├── app/
│ ├── __init__.py # App initialization, schema migration, worker startup
│ ├── models.py # Database models (File, Tag, ProcessingRun, Setting)
│ ├── routes.py # Route handlers (incl. /processing endpoints)
│ ├── settings.py # Configuration (incl. Ollama / worker settings)
│ ├── worker.py # Background OCR/VLM worker (calls Ollama over LAN)
│ ├── fts.py # SQLite FTS5 full-text search index helpers
│ ├── static/js/ # Frontend JS
│ └── templates/
│ ├── base.html # Layout + nav
│ ├── index.html # File browser
│ ├── processing.html # Processing dashboard
│ └── api.html # API documentation
├── storage/ # File storage directory (thumbnails/ lives inside it)
├── data/ # SQLite database (bind-mounted)
├── requirements.txt # Python dependencies
├── test_smoke.py # End-to-end smoke test (run with: python test_smoke.py)
├── .env.example # Sample environment configuration
├── docker-compose.mbp.yaml # Local (Mac) Docker Compose
├── docker-compose.synology.yaml # Synology NAS Docker Compose
└── README.md # This file
id: Integer (Primary Key)filename: String (Secure filename)original_filename: Stringfile_type: String (image, video, other)mime_type: Stringsize: Integer (bytes)created_at: DateTime (File creation time)last_modified_at: DateTime (File modification time)added_at: DateTime (When added to DB)updated_at: DateTime (DB record update time)notes: Textcurrent_ocr_run_id: FK to most recent successful OCR run (nullable)current_vlm_run_id: FK to most recent successful VLM run (nullable)tags: Many-to-many relationship with Tagid: Integer (Primary Key)name: String (Unique)created_at: DateTimeTiny key/value store for runtime app settings (e.g. the worker pause flag).
key: String (Primary Key) — e.g. worker_pausedvalue: Textupdated_at: DateTimeTracks each OCR or VLM processing attempt for a file. History is preserved across re-runs so you can compare outputs from different models.
id: Integer (Primary Key)file_id: FK to Filetask: String (ocr | vlm)status: String (pending | running | done | failed | cancelled)model: String (e.g. qwen3.6:35b)host: String (which machine ran the job)prompt: Text (prompt used for this run, captured for auditability)output: Text (model output)error: Text (error message if failed)attempt: Integerstarted_at, finished_at, duration_ms, created_atThe (status, task) and (file_id, task) indexes keep the worker poll and
per-file history lookups fast.
Clone the repository:
git clone https://github.com/mcotton/listing.git
cd listing
Create the database directory (file storage is bind-mounted from your existing screenshots/recordings directory — see the compose file):
mkdir -p data
Set up environment variables:
cp .env.example .env
# Edit .env with your configuration
Start the application:
docker compose -f docker-compose.mbp.yaml up -d
Access the application at http://localhost:5005 (Synology: port 9008)
Two compose files are provided:
docker-compose.mbp.yaml — local development on a Mac. Uses
host.docker.internal for Ollama by default.docker-compose.synology.yaml — Synology NAS deployment. Set OLLAMA_URL
to the LAN address of your Mac/CUDA workstation running Ollama.(An untracked docker-compose.yaml can be used as a local override.)
The container runs gunicorn with a single worker process — the background worker's run-claim is not multi-process safe. Threads handle request concurrency.
See the file contents for the full set of environment variables passed to the container.
Copy .env.example to .env and edit as needed.
Core:
FLASK_APP: Application entry point (default: app)FLASK_ENV: Environment (development/production)STORAGE_PATH: Path to file storage directoryTHUMBNAILS_PATH: Path to thumbnail cacheDATABASE_URL: SQLAlchemy DB URL (code default sqlite:///files.db; the
compose files set sqlite:////app/data/files.db — note the four slashes
for an absolute path)SECRET_KEY: Flask secret key for session managementDOCKER_DEFAULT_PLATFORM: Docker platform (e.g., linux/amd64, linux/arm64)AI processing (Ollama):
OLLAMA_URL: URL of the machine running ollama serve on the LAN, e.g.
http://192.168.1.50:11434. From the dev compose on a Mac, use
http://host.docker.internal:11434. The Ollama server must bind to
0.0.0.0 (set OLLAMA_HOST=0.0.0.0:11434 before ollama serve) for
containers on other hosts to reach it.OLLAMA_VLM_MODEL: Vision model tag, e.g. qwen3.6:35b. Must be pulled
on the Ollama host (ollama pull <tag>).OLLAMA_OCR_MODEL: Optional separate model for OCR runs. Leave unset to
reuse OLLAMA_VLM_MODEL. Can also be set per profile in the UI.OLLAMA_NUM_CTX: Ollama context window (default 8192; Ollama's own
default of 4096 overflows on image + prompt).OCR_PROMPT_DEFAULT / VLM_PROMPT_DEFAULT: Prompts used for each task.
The prompt is recorded on each run so changes are auditable.PROCESSING_HOST: Identifier recorded on each run (default: hostname).WORKER_ENABLED: Toggle the background worker (default true).WORKER_POLL_INTERVAL_SEC, WORKER_TIMEOUT_SEC, WORKER_MAX_ATTEMPTS:
Worker tuning knobs.<screenrecordings dir>:/app/storage: File storage directory./data:/app/data: SQLite database (/app/data/files.db)The Mac dev compose file (docker-compose.mbp.yaml) includes a health
check that:
Create a virtual environment:
python -m venv venv
source venv/bin/activate # Linux/Mac
# or
.\venv\Scripts\activate # Windows
Install dependencies:
pip install -r requirements.txt
Run the development server:
flask run
Run the smoke test (uses a throwaway temp DB/storage, no setup needed):
python test_smoke.py
To build for a specific platform (e.g., ARM64 for Synology):
DOCKER_DEFAULT_PLATFORM=linux/arm64 docker compose -f docker-compose.synology.yaml up -d
There is no authentication — every endpoint, including destructive ones
(POST /clear-database, DELETE /files/<id>), is open. Run this on a
trusted LAN only. If it must be reachable from outside, put it behind a
reverse proxy with basic auth and HTTPS.
/scan and queued (one pending
ProcessingRun per task: OCR + VLM). Existing images that are missing a
completed run for either task are also re-queued automatically on each
scan, so a scan doubles as a backfill.pending runs, claims one, base64-encodes the
image, and POSTs to OLLAMA_URL/api/generate with images:[…].done, the output is stored, and the
File.current_ocr_run_id / current_vlm_run_id pointer is updated so
list queries stay one query.pending
without burning attempts and retry on the next poll.WORKER_MAX_ATTEMPTS then mark the run
failed (visible on the dashboard, can be retried with one click).Concurrency is single-worker by design — Ollama serializes per model, and
SQLite doesn't support SKIP LOCKED. If you ever want multiple workers
across hosts, switch the queue to Postgres.
On the machine that will run inference (Mac or CUDA workstation):
# Allow LAN connections
OLLAMA_HOST=0.0.0.0:11434 ollama serve
# Pull the model
ollama pull qwen3.6:35b
Verify reachability from the Flask container:
docker compose exec web python -c "import requests; print(requests.get('http://<ollama-host>:11434/api/tags').text)"
Dashboard:
GET /processing — dashboard pageGET /processing/status — JSON: counts by task/status, host/model breakdown, config (incl. paused state)Job listing (paginated, filterable, sortable):
GET /processing/runs?page=&per_page=&task=&status=&model=&file_search=&sort_by=&sort_order=
sort_by accepts: id, task, status, model, duration_ms, attempt, created_at, started_at, finished_atWorker / queue control:
POST /processing/pause — pause worker (state persisted)POST /processing/resume — resume workerPOST /processing/retry-failed — reset all failed runs to pendingPOST /processing/cancel-pending — cancel every pending run (running runs untouched)POST /processing/clear — body { status?, task? } — delete runs (excludes running); omit fields to clear everything non-runningPOST /processing/reprocess-all — enqueue OCR+VLM for every image lacking pending/running runsPer-run actions:
POST /processing/runs/<id>/cancel — cancel a pending runPOST /processing/runs/<id>/retry — reset a failed/cancelled run to pendingPer-file:
GET /files/<id>/runs — full run history for a filePOST /files/<id>/reprocess — body { "tasks": ["ocr","vlm"] }GET /files/<id>/metadata — now also returns ocr and vlm run snapshotsFile listing filters/sort:
GET /files?has_ocr=1&has_vlm=0 — filter by AI processing stateGET /files?sort_by=ocr_updated|vlm_updated — sort by AI run finished_atGET /files?search=… — FTS5 full-text search (see Search section below)The file listing's search parameter is backed by a SQLite FTS5 virtual
table (file_fts) that indexes:
filename (the original filename)notesocr_text (output of the file's current OCR run, if any)vlm_text (output of the file's current VLM/description run, if any)The index is keyed by rowid = file.id and is kept in sync at well-defined
points in app/fts.py:
/scan) — insert or updatedone run — update with the new OCR or description text/clear-database — full rebuildIf the file_fts table is empty on startup but file has rows (e.g. you
deployed this version on top of an existing DB), the index is automatically
backfilled.
Query handling: each whitespace-separated token in the user input is
escaped, wrapped in double quotes, and given an FTS5 prefix wildcard, then
the tokens are ANDed (FTS5's default). For example, screen 2025 becomes
"screen"* "2025"*. If the SQLite build lacks FTS5 (rare), search
gracefully falls back to the legacy ILIKE match on filename only.
The SQLite database lives in a dedicated bind-mounted data/ directory
so it can be backed up independently of the source tree and the storage
volume.
| compose file | host path |
|---|---|
docker-compose.mbp.yaml (Mac dev) | ./data/files.db |
docker-compose.synology.yaml | /volume2/docker/listing/data/files.db |
Inside the container the path is always /app/data/files.db
(DATABASE_URL=sqlite:////app/data/files.db).
# Backup (use sqlite3 .backup for a consistent copy while running)
sqlite3 ./data/files.db ".backup './data/files.db.bak'"
# Or stop the container first, then a plain cp is safe:
docker compose stop && cp ./data/files.db ./data/files.db.bak && docker compose start
# Restore
docker compose stop && cp ./data/files.db.bak ./data/files.db && docker compose start
Migrating from a previous deployment (DB at instance/files.db):
docker compose down
mkdir -p data
mv instance/files.db data/files.db # path may differ if you customised earlier
docker compose up -d --build
View container logs:
docker compose -f docker-compose.mbp.yaml logs -f
To update the application:
Pull latest changes:
git pull
Rebuild and restart:
docker compose -f docker-compose.mbp.yaml down
docker compose -f docker-compose.mbp.yaml up -d --build
No license has been chosen yet (all rights reserved).
56 commits
HTML
55.9%
Python
41.1%
JavaScript
2.4%
A modern web-based file management application built with Flask. This application provides a user-friendly interface for browsing, searching, and managing files in a specified directory, with support for metadata tracking, filtering, and sorting.
File Management
AI Image Processing (OCR + Description)
/processing dashboard: queue counts by task/status, paginated and
sortable jobs table with per-job filters (task, status, model,
filename), host/model breakdown, and bulk actions.Advanced Filtering & Sorting
finished_at of the
file's current OCR or VLM run)User Interface
File Operations
data/ directory; WAL mode, FTS5)/
├── app/
│ ├── __init__.py # App initialization, schema migration, worker startup
│ ├── models.py # Database models (File, Tag, ProcessingRun, Setting)
│ ├── routes.py # Route handlers (incl. /processing endpoints)
│ ├── settings.py # Configuration (incl. Ollama / worker settings)
│ ├── worker.py # Background OCR/VLM worker (calls Ollama over LAN)
│ ├── fts.py # SQLite FTS5 full-text search index helpers
│ ├── static/js/ # Frontend JS
│ └── templates/
│ ├── base.html # Layout + nav
│ ├── index.html # File browser
│ ├── processing.html # Processing dashboard
│ └── api.html # API documentation
├── storage/ # File storage directory (thumbnails/ lives inside it)
├── data/ # SQLite database (bind-mounted)
├── requirements.txt # Python dependencies
├── test_smoke.py # End-to-end smoke test (run with: python test_smoke.py)
├── .env.example # Sample environment configuration
├── docker-compose.mbp.yaml # Local (Mac) Docker Compose
├── docker-compose.synology.yaml # Synology NAS Docker Compose
└── README.md # This file
id: Integer (Primary Key)filename: String (Secure filename)original_filename: Stringfile_type: String (image, video, other)mime_type: Stringsize: Integer (bytes)created_at: DateTime (File creation time)last_modified_at: DateTime (File modification time)added_at: DateTime (When added to DB)updated_at: DateTime (DB record update time)notes: Textcurrent_ocr_run_id: FK to most recent successful OCR run (nullable)current_vlm_run_id: FK to most recent successful VLM run (nullable)tags: Many-to-many relationship with Tagid: Integer (Primary Key)name: String (Unique)created_at: DateTimeTiny key/value store for runtime app settings (e.g. the worker pause flag).
key: String (Primary Key) — e.g. worker_pausedvalue: Textupdated_at: DateTimeTracks each OCR or VLM processing attempt for a file. History is preserved across re-runs so you can compare outputs from different models.
id: Integer (Primary Key)file_id: FK to Filetask: String (ocr | vlm)status: String (pending | running | done | failed | cancelled)model: String (e.g. qwen3.6:35b)host: String (which machine ran the job)prompt: Text (prompt used for this run, captured for auditability)output: Text (model output)error: Text (error message if failed)attempt: Integerstarted_at, finished_at, duration_ms, created_atThe (status, task) and (file_id, task) indexes keep the worker poll and
per-file history lookups fast.
Clone the repository:
git clone https://github.com/mcotton/listing.git
cd listing
Create the database directory (file storage is bind-mounted from your existing screenshots/recordings directory — see the compose file):
mkdir -p data
Set up environment variables:
cp .env.example .env
# Edit .env with your configuration
Start the application:
docker compose -f docker-compose.mbp.yaml up -d
Access the application at http://localhost:5005 (Synology: port 9008)
Two compose files are provided:
docker-compose.mbp.yaml — local development on a Mac. Uses
host.docker.internal for Ollama by default.docker-compose.synology.yaml — Synology NAS deployment. Set OLLAMA_URL
to the LAN address of your Mac/CUDA workstation running Ollama.(An untracked docker-compose.yaml can be used as a local override.)
The container runs gunicorn with a single worker process — the background worker's run-claim is not multi-process safe. Threads handle request concurrency.
See the file contents for the full set of environment variables passed to the container.
Copy .env.example to .env and edit as needed.
Core:
FLASK_APP: Application entry point (default: app)FLASK_ENV: Environment (development/production)STORAGE_PATH: Path to file storage directoryTHUMBNAILS_PATH: Path to thumbnail cacheDATABASE_URL: SQLAlchemy DB URL (code default sqlite:///files.db; the
compose files set sqlite:////app/data/files.db — note the four slashes
for an absolute path)SECRET_KEY: Flask secret key for session managementDOCKER_DEFAULT_PLATFORM: Docker platform (e.g., linux/amd64, linux/arm64)AI processing (Ollama):
OLLAMA_URL: URL of the machine running ollama serve on the LAN, e.g.
http://192.168.1.50:11434. From the dev compose on a Mac, use
http://host.docker.internal:11434. The Ollama server must bind to
0.0.0.0 (set OLLAMA_HOST=0.0.0.0:11434 before ollama serve) for
containers on other hosts to reach it.OLLAMA_VLM_MODEL: Vision model tag, e.g. qwen3.6:35b. Must be pulled
on the Ollama host (ollama pull <tag>).OLLAMA_OCR_MODEL: Optional separate model for OCR runs. Leave unset to
reuse OLLAMA_VLM_MODEL. Can also be set per profile in the UI.OLLAMA_NUM_CTX: Ollama context window (default 8192; Ollama's own
default of 4096 overflows on image + prompt).OCR_PROMPT_DEFAULT / VLM_PROMPT_DEFAULT: Prompts used for each task.
The prompt is recorded on each run so changes are auditable.PROCESSING_HOST: Identifier recorded on each run (default: hostname).WORKER_ENABLED: Toggle the background worker (default true).WORKER_POLL_INTERVAL_SEC, WORKER_TIMEOUT_SEC, WORKER_MAX_ATTEMPTS:
Worker tuning knobs.<screenrecordings dir>:/app/storage: File storage directory./data:/app/data: SQLite database (/app/data/files.db)The Mac dev compose file (docker-compose.mbp.yaml) includes a health
check that:
Create a virtual environment:
python -m venv venv
source venv/bin/activate # Linux/Mac
# or
.\venv\Scripts\activate # Windows
Install dependencies:
pip install -r requirements.txt
Run the development server:
flask run
Run the smoke test (uses a throwaway temp DB/storage, no setup needed):
python test_smoke.py
To build for a specific platform (e.g., ARM64 for Synology):
DOCKER_DEFAULT_PLATFORM=linux/arm64 docker compose -f docker-compose.synology.yaml up -d
There is no authentication — every endpoint, including destructive ones
(POST /clear-database, DELETE /files/<id>), is open. Run this on a
trusted LAN only. If it must be reachable from outside, put it behind a
reverse proxy with basic auth and HTTPS.
/scan and queued (one pending
ProcessingRun per task: OCR + VLM). Existing images that are missing a
completed run for either task are also re-queued automatically on each
scan, so a scan doubles as a backfill.pending runs, claims one, base64-encodes the
image, and POSTs to OLLAMA_URL/api/generate with images:[…].done, the output is stored, and the
File.current_ocr_run_id / current_vlm_run_id pointer is updated so
list queries stay one query.pending
without burning attempts and retry on the next poll.WORKER_MAX_ATTEMPTS then mark the run
failed (visible on the dashboard, can be retried with one click).Concurrency is single-worker by design — Ollama serializes per model, and
SQLite doesn't support SKIP LOCKED. If you ever want multiple workers
across hosts, switch the queue to Postgres.
On the machine that will run inference (Mac or CUDA workstation):
# Allow LAN connections
OLLAMA_HOST=0.0.0.0:11434 ollama serve
# Pull the model
ollama pull qwen3.6:35b
Verify reachability from the Flask container:
docker compose exec web python -c "import requests; print(requests.get('http://<ollama-host>:11434/api/tags').text)"
Dashboard:
GET /processing — dashboard pageGET /processing/status — JSON: counts by task/status, host/model breakdown, config (incl. paused state)Job listing (paginated, filterable, sortable):
GET /processing/runs?page=&per_page=&task=&status=&model=&file_search=&sort_by=&sort_order=
sort_by accepts: id, task, status, model, duration_ms, attempt, created_at, started_at, finished_atWorker / queue control:
POST /processing/pause — pause worker (state persisted)POST /processing/resume — resume workerPOST /processing/retry-failed — reset all failed runs to pendingPOST /processing/cancel-pending — cancel every pending run (running runs untouched)POST /processing/clear — body { status?, task? } — delete runs (excludes running); omit fields to clear everything non-runningPOST /processing/reprocess-all — enqueue OCR+VLM for every image lacking pending/running runsPer-run actions:
POST /processing/runs/<id>/cancel — cancel a pending runPOST /processing/runs/<id>/retry — reset a failed/cancelled run to pendingPer-file:
GET /files/<id>/runs — full run history for a filePOST /files/<id>/reprocess — body { "tasks": ["ocr","vlm"] }GET /files/<id>/metadata — now also returns ocr and vlm run snapshotsFile listing filters/sort:
GET /files?has_ocr=1&has_vlm=0 — filter by AI processing stateGET /files?sort_by=ocr_updated|vlm_updated — sort by AI run finished_atGET /files?search=… — FTS5 full-text search (see Search section below)The file listing's search parameter is backed by a SQLite FTS5 virtual
table (file_fts) that indexes:
filename (the original filename)notesocr_text (output of the file's current OCR run, if any)vlm_text (output of the file's current VLM/description run, if any)The index is keyed by rowid = file.id and is kept in sync at well-defined
points in app/fts.py:
/scan) — insert or updatedone run — update with the new OCR or description text/clear-database — full rebuildIf the file_fts table is empty on startup but file has rows (e.g. you
deployed this version on top of an existing DB), the index is automatically
backfilled.
Query handling: each whitespace-separated token in the user input is
escaped, wrapped in double quotes, and given an FTS5 prefix wildcard, then
the tokens are ANDed (FTS5's default). For example, screen 2025 becomes
"screen"* "2025"*. If the SQLite build lacks FTS5 (rare), search
gracefully falls back to the legacy ILIKE match on filename only.
The SQLite database lives in a dedicated bind-mounted data/ directory
so it can be backed up independently of the source tree and the storage
volume.
| compose file | host path |
|---|---|
docker-compose.mbp.yaml (Mac dev) | ./data/files.db |
docker-compose.synology.yaml | /volume2/docker/listing/data/files.db |
Inside the container the path is always /app/data/files.db
(DATABASE_URL=sqlite:////app/data/files.db).
# Backup (use sqlite3 .backup for a consistent copy while running)
sqlite3 ./data/files.db ".backup './data/files.db.bak'"
# Or stop the container first, then a plain cp is safe:
docker compose stop && cp ./data/files.db ./data/files.db.bak && docker compose start
# Restore
docker compose stop && cp ./data/files.db.bak ./data/files.db && docker compose start
Migrating from a previous deployment (DB at instance/files.db):
docker compose down
mkdir -p data
mv instance/files.db data/files.db # path may differ if you customised earlier
docker compose up -d --build
View container logs:
docker compose -f docker-compose.mbp.yaml logs -f
To update the application:
Pull latest changes:
git pull
Rebuild and restart:
docker compose -f docker-compose.mbp.yaml down
docker compose -f docker-compose.mbp.yaml up -d --build
No license has been chosen yet (all rights reserved).
56 commits
HTML
55.9%
Python
41.1%
JavaScript
2.4%