Add photo upload tab with ActiveStorage + MinIO (#33) #36

Merged
ldraney merged 5 commits from 33-upload-tab-minio into main 2026-05-25 12:18:34 +00:00
Owner

Summary

  • New Photos tab for uploading schedule paper photos via ActiveStorage + MinIO
  • Show/hide inactive properties on Properties tab
  • Active/inactive toggle on property edit form
  • Fix Turbo redirect issues for mobile (status: :see_other)

Changes

Upload feature

  • app/models/upload.rb: Upload model with has_one_attached :photo
  • app/controllers/uploads_controller.rb: index, show, create, edit, update, destroy with :see_other redirects
  • app/views/uploads/index.html.erb: single Upload Photo button, auto-submit on file select, compact thumbnail list
  • app/views/uploads/show.html.erb: full-size view with upload date, Edit Caption + Delete
  • app/views/uploads/edit.html.erb: edit caption form
  • app/javascript/controllers/upload_controller.js: Stimulus controller for auto-submit on file selection

Storage

  • Gemfile: added aws-sdk-s3 for MinIO
  • Gemfile.lock: updated
  • config/storage.yml: MinIO S3-compatible service config via env vars
  • config/environments/production.rb: auto-selects MinIO when MINIO_ENDPOINT is set
  • db/migrate/: ActiveStorage tables + uploads table
  • db/schema.rb: updated

Properties improvements

  • app/views/properties/manage.html.erb: Show Inactive filter chip (hidden by default)
  • app/views/properties/edit.html.erb: Active checkbox on edit form
  • app/controllers/properties_controller.rb: permit :active, add :see_other redirect
  • app/javascript/controllers/filter_controller.js: inactive toggle filter, run filter on connect

Navigation + styling

  • app/views/layouts/application.html.erb: Photos tab (5th tab) in bottom nav
  • app/assets/stylesheets/application.css: upload list, buttons, show page, and edit caption styles
  • config/routes.rb: resources :uploads with full CRUD

Docs

  • docs/app-architecture.md: Upload model, MinIO in diagrams, design decision
  • docs/ROADMAP.md: Ticket 4 section

Infra (pal-e-deployments, committed separately)

  • Prod: MinIO env vars added to deployment patch (init + app container)
  • Dev: fixed X-Forwarded-Proto: https for Tailscale funnel
  • Dev: added proxy_redirect to rewrite upstream Location headers
  • MinIO bucket landscaping-uploads created on cluster
  • landscaping-assistant-secrets patched with MinIO credentials

Test Plan

  • Upload photo via file picker -- saves and displays in list
  • Tap photo in list -- shows full-size with date and caption
  • Edit caption -- saves and redirects to show page
  • Delete photo -- removes from list
  • Show Inactive toggle on Properties tab hides/shows inactive
  • Active checkbox on property edit saves correctly
  • Redirects work through Tailscale funnel dev proxy (mobile tested)
  • CI pipeline green

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • ldraney/landscaping-assistant #33 -- Upload tab: snap and store schedule paper photo in MinIO
  • landscaping-assistant -- project this work belongs to

Closes #33

## Summary - New **Photos** tab for uploading schedule paper photos via ActiveStorage + MinIO - Show/hide inactive properties on Properties tab - Active/inactive toggle on property edit form - Fix Turbo redirect issues for mobile (status: :see_other) ## Changes ### Upload feature - `app/models/upload.rb`: Upload model with `has_one_attached :photo` - `app/controllers/uploads_controller.rb`: index, show, create, edit, update, destroy with `:see_other` redirects - `app/views/uploads/index.html.erb`: single Upload Photo button, auto-submit on file select, compact thumbnail list - `app/views/uploads/show.html.erb`: full-size view with upload date, Edit Caption + Delete - `app/views/uploads/edit.html.erb`: edit caption form - `app/javascript/controllers/upload_controller.js`: Stimulus controller for auto-submit on file selection ### Storage - `Gemfile`: added `aws-sdk-s3` for MinIO - `Gemfile.lock`: updated - `config/storage.yml`: MinIO S3-compatible service config via env vars - `config/environments/production.rb`: auto-selects MinIO when `MINIO_ENDPOINT` is set - `db/migrate/`: ActiveStorage tables + uploads table - `db/schema.rb`: updated ### Properties improvements - `app/views/properties/manage.html.erb`: Show Inactive filter chip (hidden by default) - `app/views/properties/edit.html.erb`: Active checkbox on edit form - `app/controllers/properties_controller.rb`: permit `:active`, add `:see_other` redirect - `app/javascript/controllers/filter_controller.js`: inactive toggle filter, run filter on connect ### Navigation + styling - `app/views/layouts/application.html.erb`: Photos tab (5th tab) in bottom nav - `app/assets/stylesheets/application.css`: upload list, buttons, show page, and edit caption styles - `config/routes.rb`: `resources :uploads` with full CRUD ### Docs - `docs/app-architecture.md`: Upload model, MinIO in diagrams, design decision - `docs/ROADMAP.md`: Ticket 4 section ### Infra (pal-e-deployments, committed separately) - Prod: MinIO env vars added to deployment patch (init + app container) - Dev: fixed `X-Forwarded-Proto: https` for Tailscale funnel - Dev: added `proxy_redirect` to rewrite upstream Location headers - MinIO bucket `landscaping-uploads` created on cluster - `landscaping-assistant-secrets` patched with MinIO credentials ## Test Plan - [x] Upload photo via file picker -- saves and displays in list - [x] Tap photo in list -- shows full-size with date and caption - [x] Edit caption -- saves and redirects to show page - [x] Delete photo -- removes from list - [x] Show Inactive toggle on Properties tab hides/shows inactive - [x] Active checkbox on property edit saves correctly - [x] Redirects work through Tailscale funnel dev proxy (mobile tested) - [ ] CI pipeline green ## Review Checklist - [ ] Passed automated review-fix loop - [x] No secrets committed - [x] No unnecessary file changes - [x] Commit messages are descriptive ## Related Notes - `ldraney/landscaping-assistant #33` -- Upload tab: snap and store schedule paper photo in MinIO - `landscaping-assistant` -- project this work belongs to Closes #33
Upload tab lets you snap a photo of the schedule paper and stash it
for later reference. ActiveStorage with S3-compatible MinIO backend
in production, local disk in dev.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update Gemfile.lock and schema for Active Storage
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
56cf8ec204
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix upload flow, add inactive toggle, editable captions
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
5c7ab187cb
- Single upload button (mobile gives camera option natively)
- Remove direct_upload that broke without ActiveStorage JS
- Auto-submit on file selection via Stimulus controller
- Add edit/update for upload captions
- Show Inactive filter chip on Properties tab (hidden by default)
- Active checkbox on property edit form
- Add status: :see_other to all POST/PATCH/DELETE redirects for Turbo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update docs for upload tab, inactive toggle, and dev proxy fix
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
126a279779
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #36 Review

DOMAIN REVIEW

Tech stack: Ruby on Rails 8.1 / ActiveStorage / Hotwire (Stimulus + Turbo) / PostgreSQL / MinIO (S3-compatible) / CSS (vanilla, no framework)

Rails / ActiveStorage

  • Storage config (config/storage.yml) correctly uses env vars for all credentials, with force_path_style: true for MinIO. Good.
  • Production environment (config/environments/production.rb) conditionally selects :minio vs :local based on MINIO_ENDPOINT presence. Clean approach.
  • Upload model validates :photo presence. ActiveStorage migration is the standard Rails-generated one. Schema is correct.
  • UploadsController uses strong params with two separate param methods (upload_params for create, caption_params for update). Good separation -- prevents photo replacement on caption edits.
  • All POST/PATCH/DELETE redirects use status: :see_other (303), which is correct for Turbo Drive compatibility. This also fixes the existing PropertiesController#update which was missing it.
  • Routes correctly scope uploads to only the needed CRUD actions (no new action, which makes sense since the form is inline on index).

Content type validation gap: The Upload model validates presence of :photo but does NOT validate content type or file size. The accept: "image/*" attribute on the file input is client-side only -- it can be bypassed. A user (or attacker) could upload arbitrary files (executables, multi-GB files) through the endpoint. Rails 7+ ActiveStorage supports validates :photo, content_type: [...] and validates :photo, size: { less_than: N.megabytes } natively. This is not a security blocker in a single-user app with no auth, but it is a correctness and resource-safety issue.

N+1 query risk: UploadsController#index loads all uploads and then iterates in the view calling upload.photo.attached? and upload.photo.variant(...). Without eager loading (Upload.with_attached_photo), this generates N+1 queries. For a small photo collection this is fine, but it should use Upload.with_attached_photo.order(created_at: :desc) as a best practice.

Stimulus / JavaScript

  • upload_controller.js is minimal and correct. requestSubmit() properly fires form validation (vs submit() which would skip it).
  • filter_controller.js cleanly extends the existing controller with a new target and action. The connect() hook ensures inactive properties are hidden on page load, which is correct behavior.

CSS

  • All new styles use CSS custom properties (var(--spacing-*), var(--color-*), var(--radius)) consistently with the existing design system. No magic numbers for spacing/colors.
  • One hardcoded color: .btn-delete:hover and .btn-danger-outline:hover both use background: #fef2f2 instead of a CSS variable. Not a blocker, but should use a theme token for consistency.

Views / UX

  • Empty state handled on uploads index ("No photos yet..."). Good.
  • Delete confirmations use turbo_confirm. Good.
  • show.html.erb line 9: image_tag @upload.photo renders the full original image. For a phone camera photo this could be several MB. Consider using a variant for display (e.g., resize_to_limit: [1200, 1200]) to reduce bandwidth on mobile.
  • Alt text is missing on all image_tag calls. Screen readers will not have context for these images. The thumbnail call on index line 21 and the full image on show line 9 should include alt: attributes (e.g., alt: upload.caption.presence || "Schedule photo").

Documentation

  • docs/app-architecture.md, docs/ROADMAP.md, docs/hotwire.md, docs/infrastructure-and-pipeline.md, docs/networking.md all updated to reflect the new feature. Thorough.
  • The infrastructure doc accurately documents the deployment-patch pattern with init containers and MinIO secrets.

BLOCKERS

1. No test coverage for new Upload functionality

The project has an established RSpec test suite (CI runs bundle exec rspec). There are existing model specs (spec/models/property_spec.rb) and request specs (spec/requests/properties_spec.rb). This PR adds a new model (Upload), a new controller (UploadsController) with 6 actions, and modifies filter_controller.js -- none of which have tests.

Required at minimum:

  • spec/models/upload_spec.rb: validates photo presence, valid with photo attached, optional caption
  • spec/requests/uploads_spec.rb: create with photo, create without photo (failure), show, edit, update caption, destroy

This is a BLOCKER per review criteria: "New functionality with zero test coverage."

NITS

  1. N+1 query on uploads index: UploadsController#index should use Upload.with_attached_photo.order(created_at: :desc) to eager-load attachments.

  2. Hardcoded hover color: .btn-delete:hover and .btn-danger-outline:hover use #fef2f2 -- consider adding a --color-danger-light token.

  3. Missing alt text on images: Both the thumbnail in uploads/index.html.erb (line 21) and the full image in uploads/show.html.erb (line 9) should include alt: attributes for accessibility.

  4. No content type or size validation on Upload model: Consider adding validates :photo, content_type: ['image/png', 'image/jpeg', 'image/webp', 'image/heic'], size: { less_than: 20.megabytes } to prevent non-image uploads and oversized files server-side.

  5. Full-size image on show page: image_tag @upload.photo serves the raw upload. On mobile, camera photos are typically 3-8 MB. A display variant (resize_to_limit: [1200, 1200]) would improve load times.

  6. PR checklist incomplete: "Passed automated review-fix loop" is unchecked, and "CI pipeline green" is unchecked. These should be resolved before merge.

SOP COMPLIANCE

  • Branch named after issue: 33-upload-tab-minio follows {issue-number}-{kebab-case-purpose} convention
  • PR body follows template: Summary, Changes, Test Plan, Related sections all present
  • Related references plan slug: No plan slug referenced (none provided -- noted in task)
  • No secrets committed: All credentials use env vars / k8s secret refs
  • No unnecessary file changes: All 25 changed files are directly related to the upload feature, properties active/inactive toggle, or documentation updates
  • Commit messages are descriptive (per PR title)

PROCESS OBSERVATIONS

  • Scope: This PR bundles the upload/photos feature (issue #33) with the "Show Inactive" properties toggle (issue #25 is open for this). The properties active/inactive toggle was already partially implemented in prior PRs, and this PR extends it with the filter chip on the manage view. This is reasonable scope creep since the filter controller was already being modified, but issue #25 should be closed when this merges.
  • Change failure risk: Low for the upload feature itself. The MinIO integration depends on infra being pre-configured (bucket created, secrets patched), which the PR body confirms was done. The ENV["MINIO_ENDPOINT"].present? fallback to :local is a safe degradation path.
  • Test gap: The test gap is the primary risk. The CI pipeline runs bundle exec rspec, so adding specs will catch regressions automatically. Without them, the upload CRUD has zero automated safety net.

VERDICT: NOT APPROVED

One blocker: no test coverage for the new Upload model and controller. The project has an established RSpec test suite and CI pipeline that runs tests. New CRUD functionality (6 controller actions, 1 model) must have specs before merge. Fix the blocker and address the nits (especially N+1 eager loading and alt text), then re-request review.

## PR #36 Review ### DOMAIN REVIEW **Tech stack**: Ruby on Rails 8.1 / ActiveStorage / Hotwire (Stimulus + Turbo) / PostgreSQL / MinIO (S3-compatible) / CSS (vanilla, no framework) **Rails / ActiveStorage** - Storage config (`config/storage.yml`) correctly uses env vars for all credentials, with `force_path_style: true` for MinIO. Good. - Production environment (`config/environments/production.rb`) conditionally selects `:minio` vs `:local` based on `MINIO_ENDPOINT` presence. Clean approach. - `Upload` model validates `:photo` presence. ActiveStorage migration is the standard Rails-generated one. Schema is correct. - `UploadsController` uses strong params with two separate param methods (`upload_params` for create, `caption_params` for update). Good separation -- prevents photo replacement on caption edits. - All POST/PATCH/DELETE redirects use `status: :see_other` (303), which is correct for Turbo Drive compatibility. This also fixes the existing `PropertiesController#update` which was missing it. - Routes correctly scope uploads to only the needed CRUD actions (no `new` action, which makes sense since the form is inline on `index`). **Content type validation gap**: The `Upload` model validates presence of `:photo` but does NOT validate content type or file size. The `accept: "image/*"` attribute on the file input is client-side only -- it can be bypassed. A user (or attacker) could upload arbitrary files (executables, multi-GB files) through the endpoint. Rails 7+ ActiveStorage supports `validates :photo, content_type: [...]` and `validates :photo, size: { less_than: N.megabytes }` natively. This is not a security blocker in a single-user app with no auth, but it is a correctness and resource-safety issue. **N+1 query risk**: `UploadsController#index` loads all uploads and then iterates in the view calling `upload.photo.attached?` and `upload.photo.variant(...)`. Without eager loading (`Upload.with_attached_photo`), this generates N+1 queries. For a small photo collection this is fine, but it should use `Upload.with_attached_photo.order(created_at: :desc)` as a best practice. **Stimulus / JavaScript** - `upload_controller.js` is minimal and correct. `requestSubmit()` properly fires form validation (vs `submit()` which would skip it). - `filter_controller.js` cleanly extends the existing controller with a new target and action. The `connect()` hook ensures inactive properties are hidden on page load, which is correct behavior. **CSS** - All new styles use CSS custom properties (`var(--spacing-*)`, `var(--color-*)`, `var(--radius)`) consistently with the existing design system. No magic numbers for spacing/colors. - One hardcoded color: `.btn-delete:hover` and `.btn-danger-outline:hover` both use `background: #fef2f2` instead of a CSS variable. Not a blocker, but should use a theme token for consistency. **Views / UX** - Empty state handled on uploads index ("No photos yet..."). Good. - Delete confirmations use `turbo_confirm`. Good. - `show.html.erb` line 9: `image_tag @upload.photo` renders the full original image. For a phone camera photo this could be several MB. Consider using a variant for display (e.g., `resize_to_limit: [1200, 1200]`) to reduce bandwidth on mobile. - Alt text is missing on all `image_tag` calls. Screen readers will not have context for these images. The thumbnail call on index line 21 and the full image on show line 9 should include `alt:` attributes (e.g., `alt: upload.caption.presence || "Schedule photo"`). **Documentation** - `docs/app-architecture.md`, `docs/ROADMAP.md`, `docs/hotwire.md`, `docs/infrastructure-and-pipeline.md`, `docs/networking.md` all updated to reflect the new feature. Thorough. - The infrastructure doc accurately documents the deployment-patch pattern with init containers and MinIO secrets. ### BLOCKERS **1. No test coverage for new Upload functionality** The project has an established RSpec test suite (CI runs `bundle exec rspec`). There are existing model specs (`spec/models/property_spec.rb`) and request specs (`spec/requests/properties_spec.rb`). This PR adds a new model (`Upload`), a new controller (`UploadsController`) with 6 actions, and modifies `filter_controller.js` -- none of which have tests. Required at minimum: - `spec/models/upload_spec.rb`: validates photo presence, valid with photo attached, optional caption - `spec/requests/uploads_spec.rb`: create with photo, create without photo (failure), show, edit, update caption, destroy This is a BLOCKER per review criteria: "New functionality with zero test coverage." ### NITS 1. **N+1 query on uploads index**: `UploadsController#index` should use `Upload.with_attached_photo.order(created_at: :desc)` to eager-load attachments. 2. **Hardcoded hover color**: `.btn-delete:hover` and `.btn-danger-outline:hover` use `#fef2f2` -- consider adding a `--color-danger-light` token. 3. **Missing alt text on images**: Both the thumbnail in `uploads/index.html.erb` (line 21) and the full image in `uploads/show.html.erb` (line 9) should include `alt:` attributes for accessibility. 4. **No content type or size validation on Upload model**: Consider adding `validates :photo, content_type: ['image/png', 'image/jpeg', 'image/webp', 'image/heic'], size: { less_than: 20.megabytes }` to prevent non-image uploads and oversized files server-side. 5. **Full-size image on show page**: `image_tag @upload.photo` serves the raw upload. On mobile, camera photos are typically 3-8 MB. A display variant (`resize_to_limit: [1200, 1200]`) would improve load times. 6. **PR checklist incomplete**: "Passed automated review-fix loop" is unchecked, and "CI pipeline green" is unchecked. These should be resolved before merge. ### SOP COMPLIANCE - [x] Branch named after issue: `33-upload-tab-minio` follows `{issue-number}-{kebab-case-purpose}` convention - [x] PR body follows template: Summary, Changes, Test Plan, Related sections all present - [ ] Related references plan slug: No plan slug referenced (none provided -- noted in task) - [x] No secrets committed: All credentials use env vars / k8s secret refs - [x] No unnecessary file changes: All 25 changed files are directly related to the upload feature, properties active/inactive toggle, or documentation updates - [x] Commit messages are descriptive (per PR title) ### PROCESS OBSERVATIONS - **Scope**: This PR bundles the upload/photos feature (issue #33) with the "Show Inactive" properties toggle (issue #25 is open for this). The properties active/inactive toggle was already partially implemented in prior PRs, and this PR extends it with the filter chip on the manage view. This is reasonable scope creep since the filter controller was already being modified, but issue #25 should be closed when this merges. - **Change failure risk**: Low for the upload feature itself. The MinIO integration depends on infra being pre-configured (bucket created, secrets patched), which the PR body confirms was done. The `ENV["MINIO_ENDPOINT"].present?` fallback to `:local` is a safe degradation path. - **Test gap**: The test gap is the primary risk. The CI pipeline runs `bundle exec rspec`, so adding specs will catch regressions automatically. Without them, the upload CRUD has zero automated safety net. ### VERDICT: NOT APPROVED One blocker: no test coverage for the new Upload model and controller. The project has an established RSpec test suite and CI pipeline that runs tests. New CRUD functionality (6 controller actions, 1 model) must have specs before merge. Fix the blocker and address the nits (especially N+1 eager loading and alt text), then re-request review.
Add upload specs, fix N+1, validations, alt tags, CSS tokens
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
CI / scan_ruby (pull_request) Has been cancelled
CI / scan_js (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
29c482dd1c
- spec/models/upload_spec.rb: validates presence, content type, file size
- spec/requests/uploads_spec.rb: full CRUD coverage (all 6 actions)
- Fix N+1 with Upload.with_attached_photo in controller
- Add server-side content type + file size validation on Upload
- Add alt attributes to image tags in upload views
- Use display variant (resize_to_limit 1200x1200) on show page
- Replace hardcoded #fef2f2 with --color-danger-light token
- Allow test host in test.rb (fixes all request specs in docker)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #36 Review (Re-review)

Previous review: NOT APPROVED (blocker: no test coverage; nits: N+1, missing alt tags, no content type/size validation, hardcoded CSS color, raw full-size image on show). This re-review confirms whether all items were addressed.

DOMAIN REVIEW

Stack: Ruby on Rails 8.1 / ActiveStorage / ActiveRecord / Stimulus JS / Vanilla CSS

Model layer (app/models/upload.rb):

  • Content type validation present with ALLOWED_CONTENT_TYPES constant -- covers PNG, JPEG, WebP, HEIC/HEIF. Previous nit resolved.
  • File size validation present with MAX_FILE_SIZE = 20.megabytes. Previous nit resolved.
  • Conditional validation (if: -> { photo.attached? }) correctly avoids calling content_type/byte_size on nil. Good.
  • has_one_attached :photo is appropriate for single-file uploads.

Controller layer (app/controllers/uploads_controller.rb):

  • with_attached_photo eager loading on index -- eliminates the N+1 query. Previous nit resolved.
  • All redirects use status: :see_other for Turbo Drive compatibility. Correct.
  • Strong params split into upload_params (photo + caption) and caption_params (caption only). The update action correctly uses caption_params, preventing photo replacement via PATCH. Good separation.
  • Error paths re-render with :unprocessable_entity. Correct.

Views:

  • index.html.erb: Thumbnails use variant(resize_to_fill: [80, 80]) -- no raw full-size images in list. Previous nit resolved.
  • show.html.erb: Full-size view uses variant(resize_to_limit: [1200, 1200]) -- caps resolution rather than serving raw originals. Previous nit resolved.
  • Both index and show have alt attributes with meaningful fallbacks (upload.caption.presence || "Schedule photo"). Previous nit resolved.
  • Empty state text present ("No photos yet..."). Good UX.
  • Delete buttons have turbo_confirm for destructive actions. Good.

CSS (application.css):

  • New --color-danger-light: #fef2f2 token added. Both .btn-remove:hover and .flash-alert now reference the token instead of the hardcoded hex value. Previous nit resolved.
  • All new upload styles use existing design tokens (--spacing-*, --radius, --color-*). No magic numbers introduced.

Storage (config/storage.yml):

  • MinIO credentials sourced from environment variables, not hardcoded. Correct.
  • force_path_style: true needed for MinIO S3-compatible endpoints. Correct.
  • Production config conditionally selects MinIO vs local based on ENV["MINIO_ENDPOINT"]. Reasonable fallback.

Tests:

  • spec/models/upload_spec.rb: 5 examples covering valid photo, missing photo, rejected content type, file size limit, and optional caption. Covers happy path + edge cases + error handling. Previous blocker resolved.
  • spec/requests/uploads_spec.rb: 7 examples covering all CRUD routes (GET index, POST create success/failure, GET show, GET edit, PATCH update, DELETE destroy). Verifies status codes, redirects, and database state. Previous blocker resolved.
  • config/environments/test.rb: config.hosts.clear added to allow test requests. Necessary for RSpec request specs.

Properties improvements:

  • :active added to strong params in PropertiesController. Appropriate.
  • filter_controller.js: connect() calls filter() so inactive properties are hidden on page load. toggleInactive action wired correctly.
  • Manage view applies is-inactive class conditionally and the Stimulus controller reads it. Clean integration.

BLOCKERS

None. All previous blockers have been addressed:

  • [RESOLVED] Test coverage now exists: 5 model specs + 7 request specs covering the full Upload CRUD lifecycle.

NITS

All previous nits have been addressed. New observations (non-blocking):

  1. Caption XSS surface: upload.caption is rendered via ERB <%= %> which auto-escapes in Rails. No issue, just noting the review was done.

  2. No new route: Routes are only: [:index, :show, :create, :edit, :update, :destroy] -- no standalone new action. The create form is embedded in index.html.erb. This is intentional for the single-page upload flow but worth noting for future developers.

  3. Stimulus upload controller is minimal: The entire controller is this.element.requestSubmit(). If this pattern is needed elsewhere, it could become a generic auto-submit controller. Not worth changing for a single use.

SOP COMPLIANCE

  • Branch named after issue: 33-upload-tab-minio matches issue #33
  • PR body has Summary, Changes, Test Plan, Related sections
  • Related section references plan slug -- no plan slug exists for this ticket (noted in task: "No plan slug")
  • No secrets committed -- credentials sourced from ENV vars
  • No unnecessary file changes -- all 28 changed files relate to the upload feature, properties active toggle, or supporting docs/infra
  • Commit messages are descriptive

PROCESS OBSERVATIONS

  • Deployment frequency: This PR is well-scoped. The upload feature, properties active toggle, and Turbo redirect fixes are cohesive changes for the same deployment.
  • Change failure risk: Low. Model validations are tested, controller CRUD is tested, storage config falls back to local when MinIO is unavailable. The see_other redirect pattern is applied consistently.
  • Documentation: Architecture docs, roadmap, hotwire docs, infrastructure docs, and networking docs all updated in the same PR. This is thorough and reduces documentation drift.
  • Re-review turnaround: All 5 previous findings (1 blocker + 4 nits) were addressed cleanly in the follow-up commit. Good fix discipline.

VERDICT: APPROVED

## PR #36 Review (Re-review) Previous review: NOT APPROVED (blocker: no test coverage; nits: N+1, missing alt tags, no content type/size validation, hardcoded CSS color, raw full-size image on show). This re-review confirms whether all items were addressed. ### DOMAIN REVIEW **Stack**: Ruby on Rails 8.1 / ActiveStorage / ActiveRecord / Stimulus JS / Vanilla CSS **Model layer** (`app/models/upload.rb`): - Content type validation present with `ALLOWED_CONTENT_TYPES` constant -- covers PNG, JPEG, WebP, HEIC/HEIF. Previous nit resolved. - File size validation present with `MAX_FILE_SIZE = 20.megabytes`. Previous nit resolved. - Conditional validation (`if: -> { photo.attached? }`) correctly avoids calling `content_type`/`byte_size` on nil. Good. - `has_one_attached :photo` is appropriate for single-file uploads. **Controller layer** (`app/controllers/uploads_controller.rb`): - `with_attached_photo` eager loading on index -- eliminates the N+1 query. Previous nit resolved. - All redirects use `status: :see_other` for Turbo Drive compatibility. Correct. - Strong params split into `upload_params` (photo + caption) and `caption_params` (caption only). The update action correctly uses `caption_params`, preventing photo replacement via PATCH. Good separation. - Error paths re-render with `:unprocessable_entity`. Correct. **Views**: - `index.html.erb`: Thumbnails use `variant(resize_to_fill: [80, 80])` -- no raw full-size images in list. Previous nit resolved. - `show.html.erb`: Full-size view uses `variant(resize_to_limit: [1200, 1200])` -- caps resolution rather than serving raw originals. Previous nit resolved. - Both index and show have `alt` attributes with meaningful fallbacks (`upload.caption.presence || "Schedule photo"`). Previous nit resolved. - Empty state text present ("No photos yet..."). Good UX. - Delete buttons have `turbo_confirm` for destructive actions. Good. **CSS** (`application.css`): - New `--color-danger-light: #fef2f2` token added. Both `.btn-remove:hover` and `.flash-alert` now reference the token instead of the hardcoded hex value. Previous nit resolved. - All new upload styles use existing design tokens (`--spacing-*`, `--radius`, `--color-*`). No magic numbers introduced. **Storage** (`config/storage.yml`): - MinIO credentials sourced from environment variables, not hardcoded. Correct. - `force_path_style: true` needed for MinIO S3-compatible endpoints. Correct. - Production config conditionally selects MinIO vs local based on `ENV["MINIO_ENDPOINT"]`. Reasonable fallback. **Tests**: - `spec/models/upload_spec.rb`: 5 examples covering valid photo, missing photo, rejected content type, file size limit, and optional caption. Covers happy path + edge cases + error handling. Previous blocker resolved. - `spec/requests/uploads_spec.rb`: 7 examples covering all CRUD routes (GET index, POST create success/failure, GET show, GET edit, PATCH update, DELETE destroy). Verifies status codes, redirects, and database state. Previous blocker resolved. - `config/environments/test.rb`: `config.hosts.clear` added to allow test requests. Necessary for RSpec request specs. **Properties improvements**: - `:active` added to strong params in `PropertiesController`. Appropriate. - `filter_controller.js`: `connect()` calls `filter()` so inactive properties are hidden on page load. `toggleInactive` action wired correctly. - Manage view applies `is-inactive` class conditionally and the Stimulus controller reads it. Clean integration. ### BLOCKERS None. All previous blockers have been addressed: - [RESOLVED] Test coverage now exists: 5 model specs + 7 request specs covering the full Upload CRUD lifecycle. ### NITS All previous nits have been addressed. New observations (non-blocking): 1. **Caption XSS surface**: `upload.caption` is rendered via ERB `<%= %>` which auto-escapes in Rails. No issue, just noting the review was done. 2. **No `new` route**: Routes are `only: [:index, :show, :create, :edit, :update, :destroy]` -- no standalone `new` action. The create form is embedded in `index.html.erb`. This is intentional for the single-page upload flow but worth noting for future developers. 3. **Stimulus upload controller is minimal**: The entire controller is `this.element.requestSubmit()`. If this pattern is needed elsewhere, it could become a generic `auto-submit` controller. Not worth changing for a single use. ### SOP COMPLIANCE - [x] Branch named after issue: `33-upload-tab-minio` matches issue #33 - [x] PR body has Summary, Changes, Test Plan, Related sections - [ ] Related section references plan slug -- no plan slug exists for this ticket (noted in task: "No plan slug") - [x] No secrets committed -- credentials sourced from ENV vars - [x] No unnecessary file changes -- all 28 changed files relate to the upload feature, properties active toggle, or supporting docs/infra - [x] Commit messages are descriptive ### PROCESS OBSERVATIONS - **Deployment frequency**: This PR is well-scoped. The upload feature, properties active toggle, and Turbo redirect fixes are cohesive changes for the same deployment. - **Change failure risk**: Low. Model validations are tested, controller CRUD is tested, storage config falls back to local when MinIO is unavailable. The `see_other` redirect pattern is applied consistently. - **Documentation**: Architecture docs, roadmap, hotwire docs, infrastructure docs, and networking docs all updated in the same PR. This is thorough and reduces documentation drift. - **Re-review turnaround**: All 5 previous findings (1 blocker + 4 nits) were addressed cleanly in the follow-up commit. Good fix discipline. ### VERDICT: APPROVED
ldraney deleted branch 33-upload-tab-minio 2026-05-25 12:18:34 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/landscaping-assistant!36
No description provided.