From d91dca635cb5bb0d68c3c1c085a4eb56a44264d9 Mon Sep 17 00:00:00 2001 From: Denis FL Date: Sat, 13 Jun 2026 14:21:03 +0200 Subject: [PATCH 1/4] Add development environment setup and Docker configuration for inbox-web --- .claude/skills/dev-env/SKILL.md | 49 +++++++ CLAUDE.md | 134 ++++++++++++++++++ Dockerfile.dev | 51 +++++++ compose.yaml | 42 ++++++ .../service/unified_storage_service.rb | 19 ++- spec/lib/unified_storage_service_spec.rb | 85 +++++++++++ 6 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 .claude/skills/dev-env/SKILL.md create mode 100644 CLAUDE.md create mode 100644 Dockerfile.dev create mode 100644 compose.yaml create mode 100644 spec/lib/unified_storage_service_spec.rb diff --git a/.claude/skills/dev-env/SKILL.md b/.claude/skills/dev-env/SKILL.md new file mode 100644 index 0000000..bf01122 --- /dev/null +++ b/.claude/skills/dev-env/SKILL.md @@ -0,0 +1,49 @@ +--- +name: dev-env +description: Boot, test, or run Rails commands for inbox-web through Docker. Use whenever you need to run rspec, rubocop, rails console/runner, db tasks, or the web server for this project — the native Ruby toolchain on this machine is broken, so everything must go through the dev container. +--- + +# inbox-web dev environment (Docker) + +The native `bundle` install on this machine does not match `Gemfile.lock`, so `bin/rails` +and `bundle exec` fail directly on the host. Run everything through the dev container +defined by `Dockerfile.dev` + `compose.yaml`. + +## First time / after Gemfile changes + +```bash +docker compose build web # or: docker build -f Dockerfile.dev -t inbox-web-dev . +``` + +## Common commands + +```bash +docker compose up # web server → http://localhost:3000 +docker compose run --rm web bundle exec rspec # full suite +docker compose run --rm web bundle exec rspec spec/lib/unified_storage_service_spec.rb +docker compose run --rm web bin/rubocop +docker compose run --rm web bin/rails console +docker compose run --rm web bin/rails db:prepare +``` + +## One-off scripts / reproductions + +To exercise app code (e.g. reproduce a storage bug) without compose, mount the script and run: + +```bash +docker run --rm -v "$PWD":/rails -v /tmp/script.rb:/tmp/script.rb \ + -e RAILS_ENV=development \ + -e ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=devkeydevkeydevkeydevkeydevkey01 \ + -e ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=devkeydevkeydevkeydevkeydevkey02 \ + -e ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=devkeydevkeydevkeydevkeydevkey03 \ + inbox-web-dev bash -lc "bin/rails db:prepare >/dev/null 2>&1; bin/rails runner /tmp/script.rb" +``` + +## Notes + +- The dev SQLite db lives at `storage/development.sqlite3` (a Docker volume, not the host + tree) — an empty host `storage/` is normal, not a sign of lost data. +- `StorageSetting` uses an encrypted column, so `ACTIVE_RECORD_ENCRYPTION_*` keys must be + set in the environment (compose.yaml provides dev defaults). +- Test env uses the `:test` Disk ActiveStorage service; dev/prod use `:unified`. See + `CLAUDE.md` → "File storage" before changing storage code. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..18302f8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes, followed by project-specific +instructions for **inbox-web**. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: + +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. + +--- + +# Project: inbox-web + +Privacy-first single-user note system. Rails 8.1 + SQLite, Stimulus/Tailwind front end, +Telegram bot capture, local voice transcription, optional cloud file storage. Runs on a +Raspberry Pi — no external services required. + +## Running things + +Native `bundle` on this machine is currently broken (Gemfile.lock pins gems that aren't +installed for the active Ruby). **Use Docker for anything that boots Rails:** + +```bash +docker compose up # web → http://localhost:3000 +docker compose run --rm web bundle exec rspec # full test suite +docker compose run --rm web bin/rubocop # style +docker compose run --rm web bin/rails console +``` + +`compose.yaml` bind-mounts the source (edits are live) and keeps gems + `storage/` +(SQLite db + uploads) in named volumes. See `Dockerfile.dev`. + +If the native toolchain is fixed, the same commands work without the `docker compose run` +prefix: `bin/dev`, `bundle exec rspec`, `bin/rubocop`, `bin/brakeman --no-pager`. + +## Layout + +- `app/models`, `app/controllers` — standard Rails. Notes are `Document` + `Block`; the + editor is Lexxy (ActionText rich text). The `Api::Uploads` block routes are **deprecated** + (their specs are skipped) — uploads now flow through ActionText/ActiveStorage. +- `app/services/storage_adapter/` — pluggable cloud backends (S3, Dropbox, GoogleDrive, + OneDrive, Local). `StorageAdapter.build(provider, config)` instantiates one. +- `app/services/oauth_manager.rb` — OAuth dance + token refresh for Dropbox/Drive/OneDrive. +- `lib/active_storage/service/unified_storage_service.rb` — the ActiveStorage service. +- `whisper_service/` — separate Python transcription service (Parakeet v3). + +## File storage — read before touching + +`config.active_storage.service = :unified` in **both dev and prod**. Every upload/download +goes through `ActiveStorage::Service::UnifiedStorageService`, which at runtime: + +1. Reads the active `StorageSetting` (one row, `active: true`). +2. If the provider is a cloud one, builds a `StorageAdapter` and delegates to it. +3. Otherwise (provider `local`, no setting, or a DB error) falls back to a local + `DiskService` rooted at `storage/`. + +Non-obvious gotchas that have bitten us: + +- **The test suite does not exercise this service.** Test env uses `config.active_storage.service = :test` + (a plain Disk service), so unified-only code paths (cloud delegation, the disk fallback, + lazy class loading) are invisible to most specs. `spec/lib/unified_storage_service_spec.rb` + is the one place that does — extend it when you change the service. +- ActiveStorage **lazily loads** `Service` subclasses; only services named in `storage.yml` + get required. Since dev/prod configure only `:unified`, `DiskService` is **not** auto-loaded + there — the file `require`s it explicitly. Don't remove that require. +- The disk fallback serves files via `ActiveStorage::DiskController`, which calls + `path_for` on the service named in the signed key (`"unified"`). The unified service must + define `path_for` (delegates to the disk service) or local downloads 500. +- Cloud blobs are served by redirecting to the adapter's temporary URL (`private_url`/ + `public_url` return `adapter.url`), not through the disk controller. + +## Conventions + +- Ruby/Rails app uses RSpec + FactoryBot (`spec/`). There is also a `test/` dir — RSpec is canonical. +- `StorageSetting#config_data` is JSON stored in an encrypted column; cloud tokens live there. +- When adding a storage provider: add an adapter under `app/services/storage_adapter/`, + register it in `StorageAdapter::ADAPTER_CLASSES`, and (if OAuth) in `OAuthManager::PROVIDERS`. diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..9d4d996 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,51 @@ +# Development-only image. +# Production deploy uses ./Dockerfile (consumed by Dokku). +# +# Run via: docker compose up +# See: compose.yaml +# +# syntax = docker/dockerfile:1 + +ARG RUBY_VERSION=3.4.5 +FROM docker.io/library/ruby:${RUBY_VERSION}-slim + +WORKDIR /rails + +# System packages needed to build native gems + dev convenience tools. +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y \ + build-essential \ + git \ + curl \ + pkg-config \ + libpq-dev \ + libyaml-dev \ + libjemalloc2 \ + libvips \ + imagemagick \ + libmagickwand-dev \ + sqlite3 \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* + +# Match Bundler version from Gemfile.lock so resolution is stable. +RUN gem install bundler + +# Pre-install gems for faster cold starts. The source tree is bind-mounted +# at runtime (see compose.yaml), and /usr/local/bundle is a named volume, +# so gems persist across rebuilds and survive bare `compose down`. +COPY Gemfile Gemfile.lock ./ +RUN bundle config set --local without "" && \ + bundle install + +# Source code lives on the host and is bind-mounted into /rails at runtime, +# so no COPY here. Anything you do on the host (edit a file) is immediately +# visible inside the container. + +ENV RAILS_ENV=development \ + BINDING=0.0.0.0 + +EXPOSE 3000 + +# Default command — overridden by compose.yaml's `command:` to chain db:prepare. +CMD ["bin/rails", "server", "-b", "0.0.0.0"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a9b9834 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,42 @@ +# Local development stack. Production deploy uses Dokku via ./Dockerfile. +# +# docker compose up # web on http://localhost:3000 +# docker compose run --rm web bin/rails console +# docker compose run --rm web bundle exec rspec +# +# The source tree is bind-mounted, so host edits are live. Gems live in a +# named volume so they survive `compose down` and aren't reinstalled on every +# boot. App data (SQLite db + uploaded files) lives under storage/ — also a +# named volume so it persists across rebuilds. + +services: + web: + build: + context: . + dockerfile: Dockerfile.dev + # db:prepare is idempotent — creates/migrates the SQLite db on first boot. + command: bash -c "bin/rails db:prepare && bin/rails server -b 0.0.0.0" + ports: + - "3000:3000" + volumes: + - .:/rails + - bundle:/usr/local/bundle + - storage:/rails/storage + environment: + RAILS_ENV: development + # ActiveRecord encryption keys — StorageSetting#config_encrypted is encrypted. + # Override with real values (bin/rails db:encryption:init) for persistent data. + ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: ${ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY:-devkeydevkeydevkeydevkeydevkey01} + ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: ${ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY:-devkeydevkeydevkeydevkeydevkey02} + ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: ${ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT:-devkeydevkeydevkeydevkeydevkey03} + # Cloud storage OAuth (optional — configure via Settings > Storage). + DROPBOX_CLIENT_ID: ${DROPBOX_CLIENT_ID:-} + DROPBOX_CLIENT_SECRET: ${DROPBOX_CLIENT_SECRET:-} + GOOGLE_DRIVE_CLIENT_ID: ${GOOGLE_DRIVE_CLIENT_ID:-} + GOOGLE_DRIVE_CLIENT_SECRET: ${GOOGLE_DRIVE_CLIENT_SECRET:-} + ONEDRIVE_CLIENT_ID: ${ONEDRIVE_CLIENT_ID:-} + ONEDRIVE_CLIENT_SECRET: ${ONEDRIVE_CLIENT_SECRET:-} + +volumes: + bundle: + storage: diff --git a/lib/active_storage/service/unified_storage_service.rb b/lib/active_storage/service/unified_storage_service.rb index 5c4b789..772e7f6 100644 --- a/lib/active_storage/service/unified_storage_service.rb +++ b/lib/active_storage/service/unified_storage_service.rb @@ -1,4 +1,5 @@ require "active_storage/service" +require "active_storage/service/disk_service" module ActiveStorage class Service @@ -112,14 +113,28 @@ def headers_for_direct_upload(key, content_type:, checksum:, **) { "Content-Type" => content_type, "Content-MD5" => checksum } end + # Resolve the on-disk path for a key. Called by ActiveStorage::DiskController + # when serving disk-backed blobs (local provider) via rails_disk_service_url. + def path_for(key) + disk_service.path_for(key) + end + private def private_url(key, expires_in:, filename:, content_type:, disposition:, **) - generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition) + if (adapter = cloud_adapter_if_active) + adapter.url(key, namespace: @namespace, expires_in: expires_in || 5.minutes) + else + generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition) + end end def public_url(key, filename:, content_type: nil, disposition: :attachment, **) - generate_url(key, expires_in: nil, filename: filename, content_type: content_type, disposition: disposition) + if (adapter = cloud_adapter_if_active) + adapter.url(key, namespace: @namespace) + else + generate_url(key, expires_in: nil, filename: filename, content_type: content_type, disposition: disposition) + end end def generate_url(key, expires_in:, filename:, content_type:, disposition:) diff --git a/spec/lib/unified_storage_service_spec.rb b/spec/lib/unified_storage_service_spec.rb new file mode 100644 index 0000000..13c5860 --- /dev/null +++ b/spec/lib/unified_storage_service_spec.rb @@ -0,0 +1,85 @@ +require "rails_helper" + +# Exercises the ActiveStorage service that backs both development and +# production (config.active_storage.service = :unified). It delegates to a +# cloud StorageAdapter when one is active, and falls back to local disk +# otherwise. There was no coverage here previously, which let a regression +# (uninitialized constant ActiveStorage::Service::DiskService) ship: the disk +# service class is only autoloaded when a Disk service is configured, which +# never happens in the :unified-only dev/prod setup. +RSpec.describe ActiveStorage::Service::UnifiedStorageService do + let(:disk_root) { Rails.root.join("tmp", "unified_spec_#{SecureRandom.hex(4)}") } + let(:disk) { ActiveStorage::Service::DiskService.new(root: disk_root.to_s) } + let(:service) do + described_class.new(namespace: :test_files).tap { |s| s.name = "unified" } + end + + before { allow(service).to receive(:disk_service).and_return(disk) } + after { FileUtils.rm_rf(disk_root) } + + def upload(key, contents) + service.upload(key, StringIO.new(contents), checksum: nil) + end + + context "with no active cloud provider (local disk)" do + before { allow(service).to receive(:cloud_adapter_if_active).and_return(nil) } + + it "round-trips upload, exist?, download and delete via disk" do + upload("abc123", "hello local") + + expect(service.exist?("abc123")).to be true + expect(service.download("abc123")).to eq("hello local") + + service.delete("abc123") + expect(service.exist?("abc123")).to be false + end + + it "exposes path_for so ActiveStorage::DiskController can serve the file" do + upload("abc123", "served bytes") + + expect(service.path_for("abc123")).to eq(disk.path_for("abc123")) + expect(File.read(service.path_for("abc123"))).to eq("served bytes") + end + + it "builds a disk-service URL for serving" do + ActiveStorage::Current.url_options = { host: "localhost", protocol: "http" } + url = service.url("abc123", expires_in: 5.minutes, filename: ActiveStorage::Filename.new("a.txt"), + content_type: "text/plain", disposition: :inline) + + expect(url).to include("/rails/active_storage/disk/") + ensure + ActiveStorage::Current.url_options = nil + end + end + + context "with an active cloud provider" do + let(:adapter) { instance_double("StorageAdapter::Dropbox") } + + before { allow(service).to receive(:cloud_adapter_if_active).and_return(adapter) } + + it "uploads through the cloud adapter, not the disk" do + expect(adapter).to receive(:upload).with(kind_of(String), "cloudkey", namespace: :test_files) + + upload("cloudkey", "to the cloud") + + expect(disk.exist?("cloudkey")).to be false + end + + it "redirects to the adapter's temporary URL instead of a disk URL" do + allow(adapter).to receive(:url).with("cloudkey", namespace: :test_files, expires_in: 5.minutes) + .and_return("https://dl.example.com/cloudkey") + + url = service.url("cloudkey", expires_in: 5.minutes, filename: ActiveStorage::Filename.new("a.txt"), + content_type: "text/plain", disposition: :inline) + + expect(url).to eq("https://dl.example.com/cloudkey") + end + + it "falls back to disk on cloud download errors for not-yet-migrated files" do + disk.upload("legacy", StringIO.new("on disk"), checksum: nil) + allow(adapter).to receive(:download).and_raise(StandardError, "404 from cloud") + + expect(service.download("legacy")).to eq("on disk") + end + end +end From 37b5d5aa0b3443e0f6039cfc7ae346c1cca79c2b Mon Sep 17 00:00:00 2001 From: Denis FL Date: Sat, 13 Jun 2026 14:30:37 +0200 Subject: [PATCH 2/4] Update brakeman to version 8.0.5 and add pnpm configuration to package.json --- Gemfile.lock | 2 +- package.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3e76395..c2210eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,7 +105,7 @@ GEM bindex (0.8.1) bootsnap (1.23.0) msgpack (~> 1.2) - brakeman (8.0.4) + brakeman (8.0.5) racc builder (3.3.0) bundler-audit (0.9.3) diff --git a/package.json b/package.json index 79db4be..0bfd552 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,10 @@ "devDependencies": { "esbuild": "^0.27.3", "tailwindcss": "^3.4.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] } } From 867c83b9e8eaa25415209c1bba34e6725fd41b56 Mon Sep 17 00:00:00 2001 From: Denis FL Date: Sat, 13 Jun 2026 14:52:30 +0200 Subject: [PATCH 3/4] Update dependencies in Gemfile.lock, modify OAuth environment variable handling in compose.yaml, and add pnpm workspace configuration --- Gemfile.lock | 204 ++++++++++++++++++++++---------------------- compose.yaml | 15 ++-- package.json | 5 -- pnpm-workspace.yaml | 7 ++ 4 files changed, 118 insertions(+), 113 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/Gemfile.lock b/Gemfile.lock index c2210eb..52eb376 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,31 @@ GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.17) + action_text-trix (2.1.19) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,36 +33,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.3) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -75,12 +75,12 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.9) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1229.0) - aws-sdk-core (3.244.0) + aws-partitions (1.1260.0) + aws-sdk-core (3.252.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -88,11 +88,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.123.0) - aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-kms (1.129.0) + aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.217.0) - aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-s3 (1.225.1) + aws-sdk-core (~> 3, >= 3.248.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -101,9 +101,9 @@ GEM bcrypt_pbkdf (1.1.2) bcrypt_pbkdf (1.1.2-arm64-darwin) bcrypt_pbkdf (1.1.2-x86_64-darwin) - bigdecimal (4.0.1) + bigdecimal (4.1.2) bindex (0.8.1) - bootsnap (1.23.0) + bootsnap (1.24.6) msgpack (~> 1.2) brakeman (8.0.5) racc @@ -165,7 +165,7 @@ GEM dry-logic (~> 1.4) zeitwerk (~> 2.6) ed25519 (1.4.0) - erb (6.0.2) + erb (6.0.4) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -174,9 +174,9 @@ GEM factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) - faker (3.6.1) + faker (3.8.0) i18n (>= 1.8.11, < 2) - faraday (2.14.1) + faraday (2.14.2) faraday-net_http (>= 2.0, < 3.5) json logger @@ -184,7 +184,7 @@ GEM faraday (>= 1, < 3) faraday-multipart (1.2.0) multipart-post (~> 2.0) - faraday-net_http (3.4.2) + faraday-net_http (3.4.4) net-http (~> 0.5) ffi (1.17.3-aarch64-linux-gnu) ffi (1.17.3-aarch64-linux-musl) @@ -222,14 +222,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) hashdiff (1.2.1) - http (6.0.2) + http (6.0.3) http-cookie (~> 1.0) llhttp (~> 0.6.1) - http-cookie (1.1.0) + http-cookie (1.1.6) domain_name (~> 0.5) i18n (1.14.8) concurrent-ruby (~> 1.0) - icalendar (2.12.1) + icalendar (2.12.3) base64 ice_cube (~> 0.16) logger @@ -240,22 +240,22 @@ GEM mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) io-console (0.8.2) - irb (1.17.0) + irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - jbuilder (2.14.1) + jbuilder (2.15.1) actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.19.2) - json-schema (6.1.0) + json (2.19.9) + json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) - jwt (2.10.2) + jwt (3.2.0) base64 kamal (2.11.0) activesupport (>= 7.0) @@ -281,7 +281,7 @@ GEM kaminari-core (= 1.2.2) kaminari-core (1.2.2) language_server-protocol (3.17.0.5) - lexxy (0.9.0.beta) + lexxy (0.9.18) rails (>= 8.0.2) lint_roller (1.1.0) llhttp (0.6.1) @@ -295,22 +295,22 @@ GEM net-imap net-pop net-smtp - marcel (1.1.0) + marcel (1.2.1) matrix (0.4.3) - mcp (0.7.1) + mcp (0.19.0) json-schema (>= 4.1) mini_magick (5.3.1) logger mini_mime (1.1.5) - minitest (6.0.2) + minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) - msgpack (1.8.0) + msgpack (1.8.3) multi_json (1.19.1) multipart-post (2.4.1) net-http (0.9.1) uri (>= 0.11.1) - net-imap (0.6.3) + net-imap (0.6.4.1) date net-protocol net-pop (0.1.2) @@ -325,21 +325,21 @@ GEM net-protocol net-ssh (7.3.1) nio4r (2.7.5) - nokogiri (1.19.2-aarch64-linux-gnu) + nokogiri (1.19.3-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.2-aarch64-linux-musl) + nokogiri (1.19.3-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.2-arm-linux-gnu) + nokogiri (1.19.3-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.2-arm-linux-musl) + nokogiri (1.19.3-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.2-arm64-darwin) + nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.2-x86_64-darwin) + nokogiri (1.19.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-gnu) + nokogiri (1.19.3-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.2-x86_64-linux-musl) + nokogiri (1.19.3-x86_64-linux-musl) racc (~> 1.4) os (1.1.4) ostruct (0.6.3) @@ -355,40 +355,40 @@ GEM prettyprint prettyprint (0.2.0) prism (1.9.0) - propshaft (1.3.1) + propshaft (1.3.2) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack - psych (5.3.1) + psych (5.4.0) date stringio public_suffix (7.0.5) - puma (7.2.0) + puma (8.0.2) nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.5) - rack-session (2.1.1) + rack (3.2.6) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -396,9 +396,9 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -406,7 +406,7 @@ GEM tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) + rake (13.4.2) rdoc (7.2.0) erb psych (>= 4.0.0) @@ -414,7 +414,7 @@ GEM redcarpet (3.6.1) redis (5.4.1) redis-client (>= 0.22.0) - redis-client (0.26.4) + redis-client (0.30.0) connection_pool regexp_parser (2.11.3) reline (0.6.3) @@ -475,9 +475,9 @@ GEM ruby-vips (2.3.0) ffi (~> 1.12) logger - rubyzip (3.2.2) + rubyzip (3.3.1) securerandom (0.4.1) - selenium-webdriver (4.41.0) + selenium-webdriver (4.44.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -485,12 +485,12 @@ GEM websocket (~> 1.0) shoulda-matchers (7.0.1) activesupport (>= 7.1) - sidekiq (8.1.1) + sidekiq (8.1.6) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) rack (>= 3.2.0) - redis-client (>= 0.26.0) + redis-client (>= 0.29.0) signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -570,14 +570,14 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) - websocket-driver (0.8.0) + websocket-driver (0.8.1) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) yaml (0.4.0) - zeitwerk (2.7.5) + zeitwerk (2.8.2) PLATFORMS aarch64-linux diff --git a/compose.yaml b/compose.yaml index a9b9834..c51665f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -30,12 +30,15 @@ services: ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: ${ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY:-devkeydevkeydevkeydevkeydevkey02} ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: ${ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT:-devkeydevkeydevkeydevkeydevkey03} # Cloud storage OAuth (optional — configure via Settings > Storage). - DROPBOX_CLIENT_ID: ${DROPBOX_CLIENT_ID:-} - DROPBOX_CLIENT_SECRET: ${DROPBOX_CLIENT_SECRET:-} - GOOGLE_DRIVE_CLIENT_ID: ${GOOGLE_DRIVE_CLIENT_ID:-} - GOOGLE_DRIVE_CLIENT_SECRET: ${GOOGLE_DRIVE_CLIENT_SECRET:-} - ONEDRIVE_CLIENT_ID: ${ONEDRIVE_CLIENT_ID:-} - ONEDRIVE_CLIENT_SECRET: ${ONEDRIVE_CLIENT_SECRET:-} + # Null values pass the host env through and stay UNSET when not provided — + # don't use ${VAR:-} here, an empty string makes OAuthManager#env_credential + # treat the var as configured. + DROPBOX_CLIENT_ID: + DROPBOX_CLIENT_SECRET: + GOOGLE_DRIVE_CLIENT_ID: + GOOGLE_DRIVE_CLIENT_SECRET: + ONEDRIVE_CLIENT_ID: + ONEDRIVE_CLIENT_SECRET: volumes: bundle: diff --git a/package.json b/package.json index 0bfd552..79db4be 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,5 @@ "devDependencies": { "esbuild": "^0.27.3", "tailwindcss": "^3.4.0" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "esbuild" - ] } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..296dc0c --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +# pnpm reads settings from here (the package.json "pnpm" field is no longer read in pnpm 11). +# esbuild needs its postinstall to fetch the platform binary; allow it to run. +# `allowBuilds` is the pnpm 11 setting; `onlyBuiltDependencies` keeps pnpm 9/10 working. +allowBuilds: + esbuild: true +onlyBuiltDependencies: + - esbuild From 9a261e6abf6db55388ba65744104116c40afb594 Mon Sep 17 00:00:00 2001 From: Denis FL Date: Sun, 14 Jun 2026 11:44:05 +0200 Subject: [PATCH 4/4] chore: update development environment setup with new compose.dev.yaml and Dockerfile.dev adjustments --- .claude/skills/dev-env/SKILL.md | 19 ++++++++++--------- CLAUDE.md | 14 +++++++++----- Dockerfile.dev | 8 ++++---- compose.yaml => compose.dev.yaml | 0 4 files changed, 23 insertions(+), 18 deletions(-) rename compose.yaml => compose.dev.yaml (100%) diff --git a/.claude/skills/dev-env/SKILL.md b/.claude/skills/dev-env/SKILL.md index bf01122..7b6af22 100644 --- a/.claude/skills/dev-env/SKILL.md +++ b/.claude/skills/dev-env/SKILL.md @@ -7,23 +7,24 @@ description: Boot, test, or run Rails commands for inbox-web through Docker. Use The native `bundle` install on this machine does not match `Gemfile.lock`, so `bin/rails` and `bundle exec` fail directly on the host. Run everything through the dev container -defined by `Dockerfile.dev` + `compose.yaml`. +defined by `Dockerfile.dev` + `compose.dev.yaml`. That file is intentionally NOT a default +Compose name (production's `docker-compose.yml` keeps that slot), so always pass `-f compose.dev.yaml`. ## First time / after Gemfile changes ```bash -docker compose build web # or: docker build -f Dockerfile.dev -t inbox-web-dev . +docker compose -f compose.dev.yaml build web # or: docker build -f Dockerfile.dev -t inbox-web-dev . ``` ## Common commands ```bash -docker compose up # web server → http://localhost:3000 -docker compose run --rm web bundle exec rspec # full suite -docker compose run --rm web bundle exec rspec spec/lib/unified_storage_service_spec.rb -docker compose run --rm web bin/rubocop -docker compose run --rm web bin/rails console -docker compose run --rm web bin/rails db:prepare +docker compose -f compose.dev.yaml up # web server → http://localhost:3000 +docker compose -f compose.dev.yaml run --rm web bundle exec rspec # full suite +docker compose -f compose.dev.yaml run --rm web bundle exec rspec spec/lib/unified_storage_service_spec.rb +docker compose -f compose.dev.yaml run --rm web bin/rubocop +docker compose -f compose.dev.yaml run --rm web bin/rails console +docker compose -f compose.dev.yaml run --rm web bin/rails db:prepare ``` ## One-off scripts / reproductions @@ -44,6 +45,6 @@ docker run --rm -v "$PWD":/rails -v /tmp/script.rb:/tmp/script.rb \ - The dev SQLite db lives at `storage/development.sqlite3` (a Docker volume, not the host tree) — an empty host `storage/` is normal, not a sign of lost data. - `StorageSetting` uses an encrypted column, so `ACTIVE_RECORD_ENCRYPTION_*` keys must be - set in the environment (compose.yaml provides dev defaults). + set in the environment (compose.dev.yaml provides dev defaults). - Test env uses the `:test` Disk ActiveStorage service; dev/prod use `:unified`. See `CLAUDE.md` → "File storage" before changing storage code. diff --git a/CLAUDE.md b/CLAUDE.md index 18302f8..f6fcb02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,14 +77,18 @@ Raspberry Pi — no external services required. Native `bundle` on this machine is currently broken (Gemfile.lock pins gems that aren't installed for the active Ruby). **Use Docker for anything that boots Rails:** +The dev stack lives in `compose.dev.yaml`. It is **not** a default Compose filename +(that's deliberate — `docker-compose.yml` is the production stack and must stay the +default), so always pass it with `-f`: + ```bash -docker compose up # web → http://localhost:3000 -docker compose run --rm web bundle exec rspec # full test suite -docker compose run --rm web bin/rubocop # style -docker compose run --rm web bin/rails console +docker compose -f compose.dev.yaml up # web → http://localhost:3000 +docker compose -f compose.dev.yaml run --rm web bundle exec rspec # full test suite +docker compose -f compose.dev.yaml run --rm web bin/rubocop # style +docker compose -f compose.dev.yaml run --rm web bin/rails console ``` -`compose.yaml` bind-mounts the source (edits are live) and keeps gems + `storage/` +`compose.dev.yaml` bind-mounts the source (edits are live) and keeps gems + `storage/` (SQLite db + uploads) in named volumes. See `Dockerfile.dev`. If the native toolchain is fixed, the same commands work without the `docker compose run` diff --git a/Dockerfile.dev b/Dockerfile.dev index 9d4d996..2e21f0d 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,8 +1,8 @@ # Development-only image. # Production deploy uses ./Dockerfile (consumed by Dokku). # -# Run via: docker compose up -# See: compose.yaml +# Run via: docker compose -f compose.dev.yaml up +# See: compose.dev.yaml # # syntax = docker/dockerfile:1 @@ -32,7 +32,7 @@ RUN apt-get update -qq && \ RUN gem install bundler # Pre-install gems for faster cold starts. The source tree is bind-mounted -# at runtime (see compose.yaml), and /usr/local/bundle is a named volume, +# at runtime (see compose.dev.yaml), and /usr/local/bundle is a named volume, # so gems persist across rebuilds and survive bare `compose down`. COPY Gemfile Gemfile.lock ./ RUN bundle config set --local without "" && \ @@ -47,5 +47,5 @@ ENV RAILS_ENV=development \ EXPOSE 3000 -# Default command — overridden by compose.yaml's `command:` to chain db:prepare. +# Default command — overridden by compose.dev.yaml's `command:` to chain db:prepare. CMD ["bin/rails", "server", "-b", "0.0.0.0"] diff --git a/compose.yaml b/compose.dev.yaml similarity index 100% rename from compose.yaml rename to compose.dev.yaml