source_monitor 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agent-memory/vbw-vbw-dev/MEMORY.md +34 -0
  3. data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +49 -0
  4. data/.claude/commands/release.md +255 -0
  5. data/.claude/skills/sm-configure/SKILL.md +13 -2
  6. data/.claude/skills/sm-configure/reference/configuration-reference.md +33 -0
  7. data/.claude/skills/sm-host-setup/SKILL.md +21 -3
  8. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +36 -0
  9. data/.claude/skills/sm-job/SKILL.md +10 -9
  10. data/.gitignore +4 -0
  11. data/.vbw-planning/REQUIREMENTS.md +22 -0
  12. data/.vbw-planning/ROADMAP.md +125 -0
  13. data/.vbw-planning/STATE.md +43 -0
  14. data/.vbw-planning/config.json +3 -1
  15. data/.vbw-planning/discovery.json +3 -1
  16. data/.vbw-planning/phases/01-generator-steps/01-CONTEXT.md +33 -0
  17. data/.vbw-planning/phases/01-generator-steps/01-VERIFICATION.md +86 -0
  18. data/.vbw-planning/phases/01-generator-steps/PLAN-01-SUMMARY.md +61 -0
  19. data/.vbw-planning/phases/01-generator-steps/PLAN-01.md +380 -0
  20. data/.vbw-planning/phases/02-verification/02-VERIFICATION.md +78 -0
  21. data/.vbw-planning/phases/02-verification/PLAN-01-SUMMARY.md +46 -0
  22. data/.vbw-planning/phases/02-verification/PLAN-01.md +500 -0
  23. data/.vbw-planning/phases/03-docs-alignment/03-VERIFICATION.md +89 -0
  24. data/.vbw-planning/phases/03-docs-alignment/PLAN-01-SUMMARY.md +48 -0
  25. data/.vbw-planning/phases/03-docs-alignment/PLAN-01.md +456 -0
  26. data/.vbw-planning/phases/04-dashboard-ux/04-VERIFICATION.md +129 -0
  27. data/.vbw-planning/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +70 -0
  28. data/.vbw-planning/phases/04-dashboard-ux/PLAN-01.md +747 -0
  29. data/.vbw-planning/phases/05-active-storage-images/05-VERIFICATION.md +156 -0
  30. data/.vbw-planning/phases/05-active-storage-images/PLAN-01-SUMMARY.md +69 -0
  31. data/.vbw-planning/phases/05-active-storage-images/PLAN-01.md +455 -0
  32. data/.vbw-planning/phases/05-active-storage-images/PLAN-02-SUMMARY.md +39 -0
  33. data/.vbw-planning/phases/05-active-storage-images/PLAN-02.md +488 -0
  34. data/.vbw-planning/phases/06-netflix-feed-fix/06-VERIFICATION.md +100 -0
  35. data/.vbw-planning/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +37 -0
  36. data/.vbw-planning/phases/06-netflix-feed-fix/PLAN-01.md +345 -0
  37. data/CHANGELOG.md +43 -0
  38. data/Gemfile.lock +1 -1
  39. data/VERSION +1 -1
  40. data/app/assets/builds/source_monitor/application.css +9 -0
  41. data/app/helpers/source_monitor/application_helper.rb +38 -0
  42. data/app/jobs/source_monitor/download_content_images_job.rb +72 -0
  43. data/app/models/source_monitor/item_content.rb +2 -0
  44. data/app/views/source_monitor/dashboard/_recent_activity.html.erb +9 -0
  45. data/app/views/source_monitor/items/_details.html.erb +2 -2
  46. data/app/views/source_monitor/logs/index.html.erb +9 -0
  47. data/app/views/source_monitor/sources/_details.html.erb +2 -2
  48. data/app/views/source_monitor/sources/_row.html.erb +1 -1
  49. data/docs/setup.md +13 -4
  50. data/docs/troubleshooting.md +38 -7
  51. data/lib/generators/source_monitor/install/install_generator.rb +201 -0
  52. data/lib/source_monitor/configuration/http_settings.rb +7 -1
  53. data/lib/source_monitor/configuration/images_settings.rb +37 -0
  54. data/lib/source_monitor/configuration.rb +3 -1
  55. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +16 -7
  56. data/lib/source_monitor/dashboard/recent_activity.rb +1 -0
  57. data/lib/source_monitor/dashboard/recent_activity_presenter.rb +15 -2
  58. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +13 -0
  59. data/lib/source_monitor/http.rb +23 -0
  60. data/lib/source_monitor/images/content_rewriter.rb +81 -0
  61. data/lib/source_monitor/images/downloader.rb +82 -0
  62. data/lib/source_monitor/logs/table_presenter.rb +25 -0
  63. data/lib/source_monitor/setup/procfile_patcher.rb +31 -0
  64. data/lib/source_monitor/setup/queue_config_patcher.rb +84 -0
  65. data/lib/source_monitor/setup/verification/recurring_schedule_verifier.rb +102 -0
  66. data/lib/source_monitor/setup/verification/runner.rb +1 -1
  67. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +1 -1
  68. data/lib/source_monitor/setup/workflow.rb +10 -0
  69. data/lib/source_monitor/version.rb +1 -1
  70. data/lib/source_monitor.rb +8 -0
  71. metadata +34 -3
  72. data/.vbw-planning/.notification-log.jsonl +0 -294
  73. data/.vbw-planning/.session-log.jsonl +0 -1376
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24dc578d91173b255c4d1a4ecc7d85542c5c2a86daabac9ee779b73c4f012ed5
4
- data.tar.gz: 43f2ae2005109c50a5c3298c315a48092926411e02a8a4715459e39304324e14
3
+ metadata.gz: 69576ecf67e1a978b29bbc96f594a01cd33c7e5510600f696bb5aca1cf5c0740
4
+ data.tar.gz: a1d42ffe1fc8339e0657c915cdaf1765fe9518659978a20ad8a18a9861823145
5
5
  SHA512:
6
- metadata.gz: 640830590b4e1638b0fd2e90c337f49f41c788948abd7a2552338a15e80b13560da320d449ec55fad12a6a9724916af2e1a0942129ca396e9c11f0ddb3abcc89
7
- data.tar.gz: 5dae4fbfe5864a33fd2895a275f2511d014e5a94ccf49a19bc1f4d0c284bd9045a561c003e945fec3acf9565b116f868616c45566310857675f3b02d3768f0e8
6
+ metadata.gz: 2a3a5c00b0803cf7bef974d5b9507519221a00794a6f17398674e089221d1102fe4fef1b2e2d7cdecd30d3b9e9b0f44f50f62b04ebb46c96df7a9f3a52710fac
7
+ data.tar.gz: 1ec0ec3676faa8055c984be855b0e89096c47a9b59eabc5aeba5d806e00f70a8e098ad61c4daa707114e0570bd2c655cd853896524f3855e01bedd29491eaade
@@ -0,0 +1,34 @@
1
+ # VBW Dev Agent Memory
2
+
3
+ ## Project: source_monitor
4
+ - Rails engine gem for RSS/feed monitoring
5
+ - Ruby 3.4.4, Rails 8.x
6
+ - Test suite: 473 tests via `bin/rails test` (takes ~76 seconds)
7
+ - RuboCop uses `rubocop-rails-omakase` base config
8
+
9
+ ## Key Learnings
10
+
11
+ ### Shell/Bash in zsh
12
+ - `!` in zsh inline scripts causes `command not found` errors. Use `case` statements instead of `if ! ...` patterns.
13
+ - Use `IFS= read -r` when reading filenames from pipes to handle edge cases.
14
+
15
+ ### RuboCop scope vs git scope
16
+ - RuboCop inspects ALL Ruby-like files (Gemfile, Rakefile, .gemspec, .rake, bin/*, config.ru), not just `.rb` files.
17
+ - `git ls-files -- '*.rb'` only matches `.rb` extensions. Plans scoped to `.rb` files may miss RuboCop violations in non-`.rb` Ruby files.
18
+ - `test/tmp/` contains untracked generated Rails app templates. These are NOT git-tracked but RuboCop scans them unless excluded.
19
+
20
+ ### File structure
21
+ - `test/lib/tmp/install_generator/` contains test fixtures (1 tracked file: `config/initializers/source_monitor.rb`)
22
+ - `test/tmp/host_app_template_*` directories are generated test artifacts, not git-tracked
23
+ - `.rubocop.yml` is at project root, inherits from `rubocop-rails-omakase`
24
+
25
+ ### Frozen string literal pragma
26
+ - Completed in commit `5f02db8` -- 113 files modified
27
+ - Added `test/tmp/**/*` to RuboCop exclude list
28
+
29
+ ### RuboCop omakase ruleset details
30
+ - Only 45 cops enabled (out of 775 available)
31
+ - ALL Metrics cops disabled (ClassLength, MethodLength, BlockLength, etc.)
32
+ - After frozen_string_literal fix, codebase had zero remaining violations
33
+ - No `.rubocop.yml` exclusions needed for large files since Metrics cops are off
34
+ - Plan 02 completed with no code changes required
@@ -0,0 +1,49 @@
1
+ # Lead Agent Memory -- SourceMonitor
2
+
3
+ ## Project Quick Facts
4
+ - Rails 8 engine, Ruby 3.4+, PostgreSQL-only
5
+ - 325 git-tracked Ruby files, 130 test files
6
+ - Coverage baseline: 2328 uncovered lines in config/coverage_baseline.json
7
+ - RuboCop: rubocop-rails-omakase, config at .rubocop.yml
8
+ - CI: .github/workflows/ci.yml (lint, security, test, release-verify, nightly profiling)
9
+ - Large files: FeedFetcher (627), Configuration (655), ImportSessionsController (792)
10
+
11
+ ## Phase 1 Findings
12
+ - 98 git-tracked .rb files missing frozen_string_literal (out of 325)
13
+ - Breakdown: app/(5), lib/(24), test/(43 non-dummy), test/dummy/(22), config/(1), db/migrate/(4)
14
+ - test/tmp/ is NOT git-tracked (generated by install_generator tests) -- exclude from changes
15
+ - test/lib/tmp/install_generator/config/initializers/source_monitor.rb IS tracked and already has pragma
16
+ - bin/rubocop wrapper forces --config .rubocop.yml
17
+ - Phase 1 criterion #3 (10% coverage shrink) not addressed by plans 01/02 -- noted for future
18
+
19
+ ## Phase 2 Planning Insights
20
+ - Coverage baseline.json has 2117 uncovered lines across 105 files (not 2328 -- that was line count)
21
+ - Top 7 files account for ~743 uncovered lines (35% of total)
22
+ - FeedFetcher: 245 uncovered, 12 existing tests, uses VCR+WebMock
23
+ - ItemCreator: 228 uncovered, 8 existing tests, needs mock entries for Atom/JSON branches
24
+ - Configuration: 94 uncovered, 5 existing tests, ~12 nested settings classes
25
+ - Dashboard::Queries: 66 uncovered, 7 existing tests, uses raw SQL + Cache
26
+ - BulkSourceScraper: 66 uncovered, 6 existing tests, ActiveJob test adapter
27
+ - Broadcaster: 48 uncovered, NO existing test file -- needs creation
28
+ - SourcesIndexMetrics: 34 uncovered, 3 existing tests
29
+ - All 5 Phase 2 plans are Wave 1 (no inter-plan dependencies, no file conflicts)
30
+ - Testing main files will indirectly cover supporting files (retry_policy, fetch_error, state, enqueuer, etc.)
31
+ - Broadcaster tests need Turbo::StreamsChannel stubs -- no full Action Cable required
32
+ - 50% coverage target requires ~1059 lines covered; direct plans target ~630, rest from indirect
33
+
34
+ ## Phase 4 Planning Insights
35
+ - 3 plans: conventions-audit (wave 1), item-creator-extraction (wave 1), final-verification (wave 2)
36
+ - SourcesController has dead `fetch`/`retry` methods (leftover from Phase 3 CRUD extraction)
37
+ - ImportSessionsController `new` and `create` are byte-for-byte identical
38
+ - 4 RuboCop violations in db/migrate/20260210204022_add_composite_index_to_log_entries.rb
39
+ - Duplicate test: test/controllers/concerns/ vs test/controllers/source_monitor/concerns/
40
+ - ItemCreator at 601 lines is the last file over 300 lines -- extract to entry_parser + content_extractor
41
+ - Coverage baseline still at 2117 uncovered (not regenerated since Phase 1) -- must regenerate
42
+ - 60% reduction target: at most 847 uncovered lines
43
+ - dashboard/queries.rb (356) and application_helper.rb (346) are near 300 but acceptable as view/query code
44
+ - `bin/update-coverage-baseline` requires `COVERAGE=1 bin/rails test` first
45
+
46
+ ## Bash/Shell Notes
47
+ - zsh on macOS -- `!` in shell conditionals causes `command not found` errors
48
+ - Use `case` statements instead of `if ! grep` patterns in zsh
49
+ - git ls-files piped to while loops works reliably for file enumeration
@@ -0,0 +1,255 @@
1
+ # Release: PR, CI, Merge, and Gem Build
2
+
3
+ Orchestrate a full release cycle for the source_monitor gem. This command handles changelog generation, version bumping, PR creation, CI monitoring, auto-merge on success, release tagging, and gem build with push instructions.
4
+
5
+ ## Inputs
6
+
7
+ - `$ARGUMENTS` -- Optional: version bump description or release notes summary. If empty, derive from commits since last tag.
8
+
9
+ ## Known Gotchas (Read Before Starting)
10
+
11
+ These are real issues encountered in previous releases. Each step below accounts for them, but keep them in mind:
12
+
13
+ 1. **Two version files**: Both `lib/source_monitor/version.rb` AND the top-level `VERSION` file must be bumped. The VBW pre-push hook checks for changes to `VERSION` (the top-level file).
14
+ 2. **Gemfile.lock sync**: After bumping the version in `version.rb`, you MUST run `bundle install` to update `Gemfile.lock`. CI runs `bundle install --frozen` which fails if the lockfile is stale.
15
+ 3. **VBW volatile files**: Files in `.vbw-planning/` (`.cost-ledger.json`, `.notification-log.jsonl`, `.session-log.jsonl`, `.hook-errors.log`) are continuously modified by VBW hooks. They should be in `.gitignore`. If they aren't, add them before proceeding.
16
+ 4. **Pre-push hook**: The VBW pre-push hook at `.git/hooks/pre-push` requires the `VERSION` file to appear in the diff for any push. For new branches, it compares the commit against the working tree -- any dirty files will trigger a false positive. For force-pushes to existing branches where `VERSION` hasn't changed since the last push, use `--no-verify`.
17
+ 5. **Single squashed commit**: Always create ONE commit on the release branch with ALL changes (version bump, changelog, Gemfile.lock, any fixes). Multiple commits cause pre-push hook issues.
18
+ 6. **Diff coverage CI gate**: The `test` CI job enforces diff coverage. Any changed lines in source code (not just test files) must have test coverage. If you change source code during the release (e.g., bug fixes), you must add corresponding tests.
19
+ 7. **Local main divergence after merge**: After the PR merges, local main will have different commits than origin/main (pre-squash vs merged). You must `git reset --hard origin/main` to sync -- this requires user approval since the sandbox blocks it.
20
+
21
+ ## Step 1: Git Hygiene
22
+
23
+ Run these checks. If ANY fail, STOP and report the issue to the user.
24
+
25
+ 1. **VBW volatile files gitignored**: Check that `.gitignore` contains these entries:
26
+ ```
27
+ .vbw-planning/.cost-ledger.json
28
+ .vbw-planning/.notification-log.jsonl
29
+ .vbw-planning/.session-log.jsonl
30
+ .vbw-planning/.hook-errors.log
31
+ ```
32
+ If any are missing, add them. If any of these files are tracked by git, remove them from tracking with `git rm --cached <file>`.
33
+
34
+ 2. **Working tree clean**: `git status --porcelain` must be empty (ignoring the VBW volatile files which should now be gitignored). If not, list the dirty files and ask the user whether to commit, stash, or abort.
35
+
36
+ 3. **On main branch**: `git branch --show-current` must be `main`. If not, ask the user if they want to continue from the current branch or switch.
37
+
38
+ 4. **Fetch latest**: `git fetch origin main`
39
+
40
+ 5. **Up to date with remote**: Compare `git rev-parse HEAD` with `git rev-parse origin/main`. If behind, ask the user whether to pull.
41
+
42
+ 6. **No unpushed commits**: Compare local HEAD with `origin/main`. If ahead, note how many commits are unpushed -- these will be included in the release.
43
+
44
+ Report a summary:
45
+ ```
46
+ Git Status:
47
+ Branch: main
48
+ Clean: yes
49
+ VBW files gitignored: yes
50
+ Synced with origin: yes
51
+ Unpushed commits: N
52
+ ```
53
+
54
+ ## Step 2: Version Check
55
+
56
+ 1. Read `lib/source_monitor/version.rb` to get the current VERSION constant.
57
+ 2. Read the top-level `VERSION` file to verify it matches. If they differ, sync them before proceeding.
58
+ 3. Read the latest git tag with `git tag --sort=-v:refname | head -1`.
59
+ 4. If the VERSION matches the latest tag (e.g., both are `0.3.2`), the version hasn't been bumped. Ask the user:
60
+ - "Current version is X.Y.Z which already has a tag. What should the new version be?"
61
+ - Offer options: patch (X.Y.Z+1), minor (X.Y+1.0), major (X+1.0.0), or custom.
62
+ - Update BOTH `lib/source_monitor/version.rb` AND the top-level `VERSION` file with the new version.
63
+ 5. If VERSION is ahead of the latest tag, proceed with the current version.
64
+
65
+ Store the release version for later steps. Do NOT commit yet -- that happens after the changelog is updated.
66
+
67
+ ## Step 3: Update CHANGELOG.md
68
+
69
+ The changelog follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. The file is at `CHANGELOG.md` in the project root.
70
+
71
+ 1. **Gather commit history** since the last tag:
72
+ ```
73
+ git log vPREVIOUS..HEAD --oneline --no-merges
74
+ ```
75
+ 2. **Categorize commits** into Keep a Changelog sections based on commit prefixes and content:
76
+ - `feat:`, `add:` --> **Added**
77
+ - `fix:`, `bugfix:` --> **Fixed**
78
+ - `chore:`, `refactor:`, `perf:` --> **Changed**
79
+ - `docs:` --> **Documentation** (only include if substantive)
80
+ - `BREAKING:` or `!:` --> **Breaking Changes** (at the top)
81
+ - `remove:`, `deprecate:` --> **Removed**
82
+ - Skip merge commits, version bumps, and CI-only changes.
83
+ - If `$ARGUMENTS` was provided, use it to inform/supplement the categorization.
84
+
85
+ 3. **Draft the changelog entry** and present it to the user for review:
86
+ ```
87
+ ## [X.Y.Z] - YYYY-MM-DD
88
+
89
+ ### Added
90
+ - <items>
91
+
92
+ ### Fixed
93
+ - <items>
94
+
95
+ ### Changed
96
+ - <items>
97
+ ```
98
+ Each bullet should be a concise, user-facing description (not a raw commit message). Consolidate related commits into single bullets where it makes sense.
99
+
100
+ 4. **Ask the user to approve** the changelog entry. Offer to edit if they want changes.
101
+
102
+ 5. **Write the entry** into `CHANGELOG.md`:
103
+ - Replace the `## [Unreleased]` section contents with `- No unreleased changes yet.`
104
+ - Insert the new versioned entry immediately after the `## [Unreleased]` block and before the previous release entry.
105
+ - Preserve all existing entries below.
106
+
107
+ ## Step 4: Sync Gemfile.lock
108
+
109
+ **CRITICAL**: After updating `version.rb`, the gemspec version changes and `Gemfile.lock` becomes stale.
110
+
111
+ 1. Run `bundle install` to update `Gemfile.lock`.
112
+ 2. Verify the output shows the new version: `Using source_monitor X.Y.Z (was X.Y.Z-1)`.
113
+ 3. If `bundle install` fails, resolve the issue before proceeding.
114
+
115
+ ## Step 5: Create Release Branch with Single Squashed Commit
116
+
117
+ **IMPORTANT**: All release changes MUST be in a single commit on the release branch. This avoids pre-push hook issues where individual commits are checked for VERSION changes.
118
+
119
+ 1. Create the release branch from main: `git checkout -b release/vX.Y.Z`
120
+ 2. Stage ALL release files in one commit:
121
+ ```
122
+ git add lib/source_monitor/version.rb VERSION CHANGELOG.md Gemfile.lock
123
+ ```
124
+ Also stage any other files that were changed (updated skills, docs, etc.).
125
+ 3. Create a single commit:
126
+ ```
127
+ chore: release vX.Y.Z
128
+ ```
129
+ 4. Push the branch: `git push -u origin release/vX.Y.Z`
130
+ - If the pre-push hook blocks with a false positive (e.g., VBW files dirty in working tree despite being gitignored), use `git push -u --no-verify origin release/vX.Y.Z`. This is safe because we've verified VERSION is in the commit.
131
+ 5. If the push fails for other reasons, diagnose and fix before proceeding.
132
+
133
+ ## Step 6: Create PR
134
+
135
+ 1. Create the PR using `gh pr create`:
136
+ - Title: `Release vX.Y.Z`
137
+ - Body format:
138
+ ```
139
+ ## Release vX.Y.Z
140
+
141
+ <paste the CHANGELOG.md entry content here>
142
+
143
+ ### Release Checklist
144
+ - [x] Version bumped in `lib/source_monitor/version.rb` and `VERSION`
145
+ - [x] CHANGELOG.md updated
146
+ - [x] Gemfile.lock synced
147
+ - [ ] CI passes (lint, security, test, release_verification)
148
+
149
+ ---
150
+ Auto-generated release PR by `/release` command.
151
+ ```
152
+ - Base: `main`
153
+ 2. Report the PR URL to the user.
154
+
155
+ ## Step 7: Monitor CI Pipeline
156
+
157
+ Poll the CI status using repeated `gh pr checks <PR_NUMBER>` calls. The CI has 4 required jobs: `lint`, `security`, `test`, `release_verification` (release_verification only runs after test passes).
158
+
159
+ 1. Tell the user: "Monitoring CI pipeline for PR #N... This typically takes 3-5 minutes."
160
+ 2. Wait 30 seconds before first poll (give CI time to start).
161
+ 3. Poll with `gh pr checks <PR_NUMBER>` every 60 seconds, up to 15 minutes.
162
+ 4. After each poll, report progress if any jobs completed or failed.
163
+
164
+ ### If CI PASSES (all checks green):
165
+
166
+ Continue to Step 8.
167
+
168
+ ### If CI FAILS:
169
+
170
+ 1. Get the failure details: `gh pr checks <PR_NUMBER>` to identify which jobs failed.
171
+ 2. For each failed job, fetch the log summary. Note: logs are only available after the **entire run** completes (not just one job). If `gh run view <RUN_ID> --log-failed` says "still in progress", wait and retry:
172
+ ```
173
+ gh run view <RUN_ID> --log-failed | tail -80
174
+ ```
175
+ 3. **Common failure: diff coverage** -- If the `test` job fails on "Enforce diff coverage", it means changed source lines lack test coverage. Read the error to identify uncovered files/lines, write tests, and add them to the release commit.
176
+ 4. **Common failure: Gemfile.lock frozen** -- If `bundle install` fails in CI with "frozen mode", you forgot to run `bundle install` locally (Step 4).
177
+ 5. Present failure details to the user and ask what to do:
178
+ - "Fix the issues and re-push" -- Fix issues, amend the commit (`git commit --amend --no-edit`), force push (`git push --force-with-lease --no-verify origin release/vX.Y.Z`), and restart CI monitoring.
179
+ - "Close the PR and abort" -- Close the PR, delete the branch, switch back to main.
180
+ - "Investigate manually" -- Stop and let the user handle it.
181
+
182
+ **Note on force pushes**: When force-pushing the release branch after amending, always use `--no-verify` because the pre-push hook will see the diff between old and new branch tips, and `VERSION` won't appear as changed (it's the same in both). This is expected and safe.
183
+
184
+ ## Step 8: Auto-Merge PR
185
+
186
+ Once CI is green:
187
+
188
+ 1. Merge the PR: `gh pr merge <PR_NUMBER> --merge --delete-branch`
189
+
190
+ 2. **Sync local main with remote** (this is the tricky part):
191
+ - The merge creates a merge commit on origin/main that doesn't exist locally.
192
+ - Local main may have different commits (pre-squash) than what was merged.
193
+ - Switch to main: `git checkout main`
194
+ - Try `git pull origin main`. If it fails with conflicts or divergence:
195
+ - Ask the user to run `git reset --hard origin/main` (the sandbox blocks this command).
196
+ - Explain this is safe because the PR is merged and all changes are on origin/main.
197
+ - Verify with `git log --oneline -3` that local matches remote.
198
+
199
+ 3. Report: "PR #N merged successfully."
200
+
201
+ ## Step 9: Tag the Release
202
+
203
+ 1. Verify you're on main and synced with origin.
204
+ 2. Create an annotated tag:
205
+ ```
206
+ git tag -a vX.Y.Z -m "Release vX.Y.Z"
207
+ ```
208
+ 3. Push the tag: `git push origin vX.Y.Z`
209
+ 4. Create a GitHub release from the tag:
210
+ ```
211
+ gh release create vX.Y.Z --title "vX.Y.Z" --notes "<changelog entry from Step 3>"
212
+ ```
213
+ 5. Report the release URL.
214
+
215
+ ## Step 10: Build the Gem
216
+
217
+ 1. Clean any old gem files: `ls source_monitor-*.gem` and remove them if found (don't error if none exist).
218
+ 2. Build the gem: `gem build source_monitor.gemspec`
219
+ 3. Verify the gem was built: check for `source_monitor-X.Y.Z.gem` in the project root.
220
+ 4. Show the file size: `ls -la source_monitor-X.Y.Z.gem`
221
+
222
+ ## Step 11: Gem Push Instructions
223
+
224
+ Present the final instructions to the user:
225
+
226
+ ```
227
+ Release vX.Y.Z Complete!
228
+
229
+ Git tag: vX.Y.Z (pushed)
230
+ GitHub: <release URL>
231
+ PR: <PR URL> (merged)
232
+ Gem built: source_monitor-X.Y.Z.gem
233
+
234
+ To publish to RubyGems:
235
+
236
+ gem push source_monitor-X.Y.Z.gem
237
+
238
+ You'll be prompted for your RubyGems OTP code (check your authenticator app).
239
+ The gem has `rubygems_mfa_required` enabled, so the OTP is mandatory.
240
+
241
+ After pushing, verify at:
242
+ https://rubygems.org/gems/source_monitor/versions/X.Y.Z
243
+ ```
244
+
245
+ Do NOT run `gem push` automatically -- always let the user handle the OTP-protected push manually.
246
+
247
+ ## Error Recovery
248
+
249
+ - If any step fails unexpectedly, report what happened and where things stand.
250
+ - If a release branch already exists, ask the user whether to reuse or recreate it.
251
+ - If the tag already exists, skip tagging and inform the user.
252
+ - If the pre-push hook blocks, check whether `VERSION` (top-level) is in the commit diff. If it is and the hook still blocks (working tree drift from VBW), use `--no-verify`.
253
+ - If local main diverges from origin after merge, ask the user to run `git reset --hard origin/main`.
254
+ - Always leave the user on the `main` branch in a clean state when possible.
255
+ - If `gem build` warns about duplicate URIs (homepage_uri/source_code_uri), this is cosmetic and safe to ignore.
@@ -29,7 +29,7 @@ After the block executes, `ModelExtensions.reload!` runs automatically to apply
29
29
 
30
30
  ## Configuration Sections
31
31
 
32
- The `config` object (`SourceMonitor::Configuration`) has 10 sub-sections plus top-level queue/job settings:
32
+ The `config` object (`SourceMonitor::Configuration`) has 11 sub-sections plus top-level queue/job settings:
33
33
 
34
34
  | Section | Accessor | Class |
35
35
  |---|---|---|
@@ -44,6 +44,7 @@ The `config` object (`SourceMonitor::Configuration`) has 10 sub-sections plus to
44
44
  | Models | `config.models` | `Models` |
45
45
  | Realtime | `config.realtime` | `RealtimeSettings` |
46
46
  | Authentication | `config.authentication` | `AuthenticationSettings` |
47
+ | Images | `config.images` | `ImagesSettings` |
47
48
 
48
49
  See `reference/configuration-reference.md` for every setting with types, defaults, and examples.
49
50
 
@@ -69,6 +70,14 @@ config.authentication.authenticate_with :authenticate_user!
69
70
  config.authentication.authorize_with ->(c) { c.current_user&.admin? }
70
71
  ```
71
72
 
73
+ ### Image Downloads (Active Storage)
74
+ ```ruby
75
+ config.images.download_to_active_storage = true
76
+ config.images.max_download_size = 5 * 1024 * 1024 # 5 MB
77
+ config.images.download_timeout = 15
78
+ config.images.allowed_content_types = %w[image/jpeg image/png image/webp]
79
+ ```
80
+
72
81
  ### Events
73
82
  ```ruby
74
83
  config.events.after_item_created { |e| Notifier.new_item(e.item) }
@@ -116,6 +125,7 @@ SourceMonitor.mission_control_dashboard_path # Resolved MC path or nil
116
125
  | `lib/source_monitor/configuration/model_definition.rb` | Per-model definition |
117
126
  | `lib/source_monitor/configuration/realtime_settings.rb` | Action Cable settings |
118
127
  | `lib/source_monitor/configuration/authentication_settings.rb` | Auth settings |
128
+ | `lib/source_monitor/configuration/images_settings.rb` | Image download settings |
119
129
  | `lib/source_monitor/configuration/validation_definition.rb` | Validation wrapper |
120
130
 
121
131
  ## References
@@ -146,7 +156,8 @@ end
146
156
  ## Checklist
147
157
 
148
158
  - [ ] Initializer exists at `config/initializers/source_monitor.rb`
149
- - [ ] Queue names match `config/solid_queue.yml` entries
159
+ - [ ] Queue names match `config/queue.yml` (or `config/solid_queue.yml`) entries
160
+ - [x] Dispatcher config includes `recurring_schedule: config/recurring.yml` (handled by install generator)
150
161
  - [ ] Authentication hooks configured for host auth system
151
162
  - [ ] HTTP timeouts appropriate for target feeds
152
163
  - [ ] Retention policy set for production
@@ -309,6 +309,39 @@ end
309
309
 
310
310
  ---
311
311
 
312
+ ## Images Settings (`config.images`)
313
+
314
+ Class: `SourceMonitor::Configuration::ImagesSettings`
315
+
316
+ Controls background downloading of inline images from feed content to Active Storage.
317
+
318
+ **Prerequisite:** The host app must have Active Storage installed (`rails active_storage:install` + migrations).
319
+
320
+ | Setting | Type | Default | Description |
321
+ |---|---|---|---|
322
+ | `download_to_active_storage` | Boolean | `false` | Enable background image downloading for new items |
323
+ | `max_download_size` | Integer | `10485760` (10 MB) | Maximum image file size in bytes; larger images are skipped |
324
+ | `download_timeout` | Integer | `30` | HTTP timeout for image downloads in seconds |
325
+ | `allowed_content_types` | Array | `["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"]` | Permitted MIME types for downloaded images |
326
+
327
+ ### Helper Method
328
+
329
+ | Method | Returns | Description |
330
+ |---|---|---|
331
+ | `download_enabled?` | Boolean | Returns `true` when `download_to_active_storage` is truthy |
332
+
333
+ ```ruby
334
+ # Enable image downloading with custom limits
335
+ config.images.download_to_active_storage = true
336
+ config.images.max_download_size = 5 * 1024 * 1024 # 5 MB
337
+ config.images.download_timeout = 15
338
+ config.images.allowed_content_types = %w[image/jpeg image/png image/webp]
339
+ ```
340
+
341
+ When enabled, `DownloadContentImagesJob` is automatically enqueued after new items are created from feed entries. The job downloads inline `<img>` images from `item.content`, attaches them to `item_content.images` via Active Storage, and rewrites the HTML with Active Storage serving URLs. Failed downloads gracefully preserve the original image URL.
342
+
343
+ ---
344
+
312
345
  ## Environment Variables
313
346
 
314
347
  | Variable | Purpose |
@@ -61,7 +61,7 @@ bin/source_monitor install --yes
61
61
  gem "source_monitor", "~> 0.3.0" # in Gemfile
62
62
  bundle install
63
63
 
64
- # 2. Run the install generator
64
+ # 2. Run the install generator (mounts engine, creates initializer, configures recurring jobs)
65
65
  bin/rails generate source_monitor:install --mount-path=/source_monitor
66
66
 
67
67
  # 3. Copy engine migrations
@@ -70,9 +70,13 @@ bin/rails railties:install:migrations FROM=source_monitor
70
70
  # 4. Apply migrations
71
71
  bin/rails db:migrate
72
72
 
73
- # 5. Start background workers
73
+ # 5. Start background workers (recurring jobs configured automatically in config/recurring.yml)
74
74
  bin/rails solid_queue:start
75
75
 
76
+ # Note: The generator automatically patches Procfile.dev with a jobs: entry
77
+ # and adds recurring_schedule to your queue.yml dispatcher config.
78
+ # Re-run the generator if these were not applied: bin/rails generate source_monitor:install
79
+
76
80
  # 6. Verify
77
81
  bin/source_monitor verify
78
82
  ```
@@ -86,7 +90,7 @@ gem "source_monitor", github: "dchuk/source_monitor"
86
90
 
87
91
  ## What the Install Generator Does
88
92
 
89
- The generator (`SourceMonitor::Generators::InstallGenerator`) performs two actions:
93
+ The generator (`SourceMonitor::Generators::InstallGenerator`) performs five actions:
90
94
 
91
95
  1. **Mounts the engine** in `config/routes.rb`:
92
96
  ```ruby
@@ -97,6 +101,15 @@ The generator (`SourceMonitor::Generators::InstallGenerator`) performs two actio
97
101
  2. **Creates the initializer** at `config/initializers/source_monitor.rb`:
98
102
  Uses the template at `lib/generators/source_monitor/install/templates/source_monitor.rb.tt`. Skips if the file already exists.
99
103
 
104
+ 3. **Configures recurring jobs** in `config/recurring.yml`:
105
+ Adds entries for `ScheduleFetchesJob` (every minute), scrape scheduling (every 2 minutes), `ItemCleanupJob` (2am daily), and `LogCleanupJob` (3am daily). If the file doesn't exist, creates it with `default: &default` and environment sections. If it exists, merges entries without overwriting. Skips if SourceMonitor entries are already present.
106
+
107
+ 4. **Patches Procfile.dev** with a `jobs:` entry for Solid Queue:
108
+ Creates the file with `web:` and `jobs:` entries if it does not exist. Appends a `jobs:` entry if the file exists but lacks one. Skips if a `jobs:` entry is already present.
109
+
110
+ 5. **Patches queue.yml dispatcher** with `recurring_schedule: config/recurring.yml`:
111
+ Adds the `recurring_schedule` key to each dispatcher entry in `config/queue.yml`. If no dispatchers section exists, creates a default one. Skips if `recurring_schedule` is already configured. Skips if `config/queue.yml` does not exist.
112
+
100
113
  Re-running the generator is safe and idempotent.
101
114
 
102
115
  ## Post-Install Configuration
@@ -177,6 +190,8 @@ config.authentication.user_signed_in_method = :user_signed_in?
177
190
  | `lib/generators/source_monitor/install/install_generator.rb` | Rails generator |
178
191
  | `lib/generators/source_monitor/install/templates/source_monitor.rb.tt` | Initializer template |
179
192
  | `lib/source_monitor/setup/initializer_patcher.rb` | Post-install patching |
193
+ | `lib/source_monitor/setup/procfile_patcher.rb` | Procfile.dev patching for guided workflow |
194
+ | `lib/source_monitor/setup/queue_config_patcher.rb` | Queue config patching for guided workflow |
180
195
  | `lib/source_monitor/setup/verification/runner.rb` | Verification runner |
181
196
  | `lib/source_monitor/engine.rb` | Engine configuration and initializers |
182
197
  | `docs/setup.md` | Full setup documentation |
@@ -217,6 +232,9 @@ end
217
232
  - [ ] `bundle install` completed
218
233
  - [ ] Install generator ran (`bin/rails generate source_monitor:install`)
219
234
  - [ ] Engine migrations copied and applied
235
+ - [ ] Recurring jobs configured in `config/recurring.yml`
236
+ - [x] `Procfile.dev` includes `jobs:` entry for Solid Queue (handled by generator)
237
+ - [x] Dispatcher config includes `recurring_schedule: config/recurring.yml` (handled by generator)
220
238
  - [ ] Solid Queue workers started
221
239
  - [ ] Authentication hooks configured in initializer
222
240
  - [ ] `bin/source_monitor verify` passes
@@ -39,6 +39,7 @@ bin/rails generate source_monitor:install --mount-path=/source_monitor
39
39
  Verify after running:
40
40
  - [ ] `config/routes.rb` contains `mount SourceMonitor::Engine, at: "/source_monitor"`
41
41
  - [ ] `config/initializers/source_monitor.rb` exists
42
+ - [ ] `config/recurring.yml` contains SourceMonitor recurring job entries
42
43
 
43
44
  ## Phase 4: Database Setup
44
45
 
@@ -75,6 +76,8 @@ end
75
76
 
76
77
  ## Phase 6: Configure Workers
77
78
 
79
+ The install generator automatically configures recurring jobs in `config/recurring.yml` (fetch scheduling, scrape scheduling, item cleanup, log cleanup). These run automatically with `bin/dev` or `bin/jobs`.
80
+
78
81
  Ensure `config/solid_queue.yml` (or equivalent) includes the SourceMonitor queues:
79
82
 
80
83
  ```yaml
@@ -83,6 +86,7 @@ production:
83
86
  dispatchers:
84
87
  - polling_interval: 1
85
88
  batch_size: 500
89
+ recurring_schedule: config/recurring.yml
86
90
  workers:
87
91
  - queues: "source_monitor_fetch"
88
92
  threads: 2
@@ -96,7 +100,39 @@ production:
96
100
  bin/rails solid_queue:start
97
101
  ```
98
102
 
103
+ ### Phase 6a: Procfile.dev for Development (Automatic)
104
+
105
+ The install generator automatically patches `Procfile.dev` with a `jobs:` entry for Solid Queue. If no `Procfile.dev` exists, it creates one with `web:` and `jobs:` entries. If the file exists but lacks a `jobs:` entry, it appends one. This is idempotent -- re-running the generator is safe.
106
+
107
+ Verify after running the generator:
108
+ ```
109
+ # Expected Procfile.dev content:
110
+ web: bin/rails server -p 3000
111
+ jobs: bundle exec rake solid_queue:start
112
+ ```
113
+
114
+ If the entry is missing, re-run: `bin/rails generate source_monitor:install`
115
+
116
+ ### Phase 6b: Recurring Schedule Dispatcher Wiring (Automatic)
117
+
118
+ The install generator automatically patches `config/queue.yml` dispatchers with `recurring_schedule: config/recurring.yml`. If no dispatchers section exists, it creates a default one. This is idempotent -- re-running the generator is safe.
119
+
120
+ Verify after running the generator:
121
+ ```yaml
122
+ # Expected in config/queue.yml under dispatchers:
123
+ dispatchers:
124
+ - polling_interval: 1
125
+ batch_size: 500
126
+ recurring_schedule: config/recurring.yml
127
+ ```
128
+
129
+ If the key is missing, re-run: `bin/rails generate source_monitor:install`
130
+
131
+ **Diagnostics:** Run `bin/source_monitor verify` to check that recurring tasks are registered. The RecurringScheduleVerifier will warn if no SourceMonitor recurring tasks are found in Solid Queue.
132
+
99
133
  - [ ] Queue configuration includes `source_monitor_fetch` and `source_monitor_scrape`
134
+ - [x] `Procfile.dev` includes a `jobs:` entry for Solid Queue (handled by generator)
135
+ - [x] Dispatcher config includes `recurring_schedule: config/recurring.yml` (handled by generator)
100
136
  - [ ] Workers started and processing
101
137
 
102
138
  ## Phase 7: Verify Installation
@@ -159,15 +159,16 @@ SourceMonitor.queue_name(:fetch) # => "source_monitor_fetch"
159
159
 
160
160
  ### Recurring Jobs
161
161
 
162
- ScheduleFetchesJob is typically configured as a recurring job in `config/recurring.yml`:
163
-
164
- ```yaml
165
- production:
166
- schedule_fetches:
167
- class: SourceMonitor::ScheduleFetchesJob
168
- schedule: every 1 minute
169
- queue: source_monitor_fetch
170
- ```
162
+ The install generator (`bin/rails generate source_monitor:install`) automatically configures these recurring jobs in `config/recurring.yml`:
163
+
164
+ | Job | Schedule |
165
+ |-----|----------|
166
+ | `SourceMonitor::ScheduleFetchesJob` | every minute |
167
+ | `SourceMonitor::Scraping::Scheduler.run` | every 2 minutes |
168
+ | `SourceMonitor::ItemCleanupJob` | at 2am every day |
169
+ | `SourceMonitor::LogCleanupJob` | at 3am every day |
170
+
171
+ The install generator automatically configures `config/recurring.yml` with these entries AND patches the `config/queue.yml` dispatcher with `recurring_schedule: config/recurring.yml` so recurring jobs load on startup. Both steps are idempotent. If you need to customize schedules, edit `config/recurring.yml` directly.
171
172
 
172
173
  ## Retry Policies
173
174
 
data/.gitignore CHANGED
@@ -15,3 +15,7 @@
15
15
  /app/assets/builds/*
16
16
  !/app/assets/builds/.keep
17
17
  !/app/assets/builds/source_monitor/
18
+ .vbw-planning/.cost-ledger.json
19
+ .vbw-planning/.notification-log.jsonl
20
+ .vbw-planning/.session-log.jsonl
21
+ .vbw-planning/.hook-errors.log
@@ -32,6 +32,28 @@ Core value: Drop-in Rails engine for feed monitoring, content scraping, and oper
32
32
  - [ ] **REQ-14**: Audit and fix any RuboCop violations against omakase ruleset
33
33
  - [ ] **REQ-15**: Ensure all models, controllers, and service objects follow Rails conventions
34
34
 
35
+ ### Generator Enhancements
36
+
37
+ - [ ] **REQ-16**: Install generator patches `Procfile.dev` with a `jobs:` entry for Solid Queue
38
+ - [ ] **REQ-17**: Install generator patches queue config dispatcher with `recurring_schedule: config/recurring.yml`
39
+ - [ ] **REQ-18**: Guided workflow (`Setup::Workflow`) integrates both new generator steps
40
+ - [ ] **REQ-19**: `RecurringScheduleVerifier` checks that recurring tasks are registered with Solid Queue
41
+ - [ ] **REQ-20**: `SolidQueueVerifier` remediation suggests `Procfile.dev` when workers not detected
42
+ - [ ] **REQ-21**: Skills and documentation updated to reflect automated Procfile.dev and recurring_schedule setup
43
+
44
+ ### Dashboard UX
45
+
46
+ - [ ] **REQ-22**: Fetch logs show source URL for both success and failure entries on the dashboard
47
+ - [ ] **REQ-23**: Dashboard links to sources and items are clickable and open in a new tab
48
+
49
+ ### Active Storage Image Downloads
50
+
51
+ - [ ] **REQ-24**: Configurable option to download inline images from items to Active Storage instead of loading from source
52
+
53
+ ### Feed Compatibility
54
+
55
+ - [ ] **REQ-25**: Investigate and fix failing fetch for Netflix Tech Blog feed (https://netflixtechblog.com/feed)
56
+
35
57
  ## v2 Requirements
36
58
 
37
59
  - [ ] **REQ-XX**: Improve optional dependency loading with clear error messages