Local-first Imgflip clone designed for robust polling, change-only history tracking, and a browse-first web UI.
This project follows the design in:
docs/plans/2026-03-03-imgflop-design.mddocs/plans/2026-03-03-imgflop-implementation-plan.md
- Poll Imgflip top memes on a schedule.
- Support
api_top_n = max | <int>. - Store image files on disk and metadata in SQLite.
- Persist history as change events only (no duplicate unchanged snapshots).
- Provide a local web UI for gallery browsing, details, admin operations, and meme creation.
- Gate admin routes with a simple secure single-account login.
- Single Rust binary for poller + scheduler + HTTP server.
- Async runtime:
tokio. - Web/API:
axum. - Database: SQLite (
sqlx, WAL mode).
trigger -> lock -> fetch -> normalize -> assets -> rank -> diff -> commit -> publish status
Key behavior:
- Single-flight locking prevents overlapping polls.
- Manual trigger during active run sets
pending_repoll. - Source failures are captured and surfaced through run/error records.
- Binary image assets: content-addressed files on disk (SHA-256).
- Relational state/history: SQLite tables for runs, errors, memes, source mappings, current top state, change events, and created memes.
Important tables:
poll_runspoll_run_errorsmemessource_recordsimage_assetstop_state_currenttop_state_eventscreated_memescreated_meme_layersauth_events
- Emit only:
entered_topleft_toprank_changedmetadata_changed
- No event emitted when an item is unchanged.
[polling]
schedule_cron = "0 * * * *"
api_top_n = "max" # or integer
history_top_n = 2000
scraper_enabled = true
[storage]
data_dir = "./data"
images_dir = "./data/images"
temp_dir = "./data/tmp"
[web]
bind = "127.0.0.1:8080"
base_url = "http://localhost:8080"
[auth]
admin_user = "admin"
admin_password_hash = "$argon2id$..."
[logging]
level = "info"
format = "json"
dir = "./data/logs"
rotate = "daily"
retention_days = 14
stdout = true- Single configured admin account with Argon2id password hash.
- Session-cookie auth for
/admin/*. - CSRF protection on state-changing admin routes.
- Login throttling and basic auth event auditing.
ADMIN_USER(optional fallback login)ADMIN_PASSWORD_HASH(optional fallback login, requiresADMIN_USER)IMGFLOP_API_TOP_N(maxby default, or integer>= 1)IMGFLOP_HISTORY_TOP_N(100by default, integer>= 1)IMGFLOP_POLL_INTERVAL_SECS(300by default, integer>= 1)IMGFLOP_BIND(127.0.0.1:8080by default)IMGFLOP_DB_URL(sqlite://imgflop.db?mode=rwcby default)IMGFLOP_ASSETS_DIR(data/imagesby default)IMGFLOP_API_ENDPOINT(optional override, defaults to Imgflip public API)
IMGFLOP_API_TOP_N and IMGFLOP_HISTORY_TOP_N are independent:
- API ingest candidate count is controlled by
IMGFLOP_API_TOP_N. - Persisted top-state/events are controlled by
IMGFLOP_HISTORY_TOP_N.
Admin auth behavior:
- If no admin account exists in DB,
/admin/loginshows a first-run setup form. - Setup stores one local admin credential in SQLite using Argon2id hash.
- If
ADMIN_USER+ADMIN_PASSWORD_HASHare provided, they remain usable as an emergency fallback login.
- Rust toolchain (stable)
- Docker Desktop (for
rust.testcontainersscenarios) cargo-llvm-covfor local coverage gating
cargo buildcargo testcargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo llvm-cov --workspace --all-features --summary-only --fail-under-lines 80- Unit tests for pure logic (config parsing, diff behavior, scheduler/locking behavior).
- Integration tests for ingest/store behavior and route behavior.
- End-to-end smoke tests for login, poll trigger, gallery, and create export flow.
- Use
rust.testcontainerswhere tests require external services. - Use in-memory/temp resources where no external service is needed.
src/application modules (auth,assets,config,diff,ingest,ops,sources,store,web,designer)migrations/SQLite schema migrationstests/unit/integration/e2e coverage tests.github/workflows/ci.ymlCI checks and coverage gatedocs/plans/design and implementation plans