Skip to content

feat: support per-keyword rank tracking interval overrides - #40

Open
mvanhorn wants to merge 4 commits into
every-app:mainfrom
mvanhorn:fix/37-per-keyword-rank-intervals
Open

feat: support per-keyword rank tracking interval overrides#40
mvanhorn wants to merge 4 commits into
every-app:mainfrom
mvanhorn:fix/37-per-keyword-rank-intervals

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds optional per-keyword rank-check intervals on top of the existing config-level schedule, so a tracked keyword can be checked on its own cadence (manual / daily / weekly) independent of its config default.

Why this matters

Today the check cadence is fixed at the config (domain) level, so every keyword under a domain is forced onto the same schedule. Issue #37 asks for control at the keyword level: high-priority terms often warrant daily checks while the long tail is fine weekly (or only on demand), and checking everything at the fastest cadence wastes rank-API budget. Per-keyword overrides let users spend checks where they matter without splitting a domain across multiple configs.

What changed

  • Schema + migration (drizzle/0010_per_keyword_intervals.sql, src/db/app.schema.ts): a nullable per-keyword scheduleIntervalOverride that falls back to the config interval when unset.
  • Service (RankTrackingService.ts): resolve the effective interval per keyword, compute and advance per-keyword nextCheckAt, and select the keywords actually due in a scheduled run.
  • Cron wiring (src/server.ts): the scheduled run advances both the config schedule and the per-keyword schedules, and skips manual configs/keywords so they are never auto-advanced.
  • UI (RankTrackingColumns.tsx, RankTrackingTableToolbar.tsx, and the domain/table views): surface and edit the per-keyword interval, with a toolbar control for bulk changes.
  • Tests (RankTrackingService.test.ts): cover interval resolution, schedule advancement, and the manual no-advance case.

Verification

  • tsc --noEmit passes.
  • vitest run RankTrackingService passes (3 tests).

Closes #37

AI was used for assistance.

Greptile Summary

This PR adds per-keyword rank tracking schedule overrides. The main changes are:

  • New keyword-level schedule interval and next-check fields.
  • Service logic to resolve inherited, daily, weekly, and paused keyword schedules.
  • Cron changes to run only due keywords and advance their schedules.
  • UI controls for per-row and bulk schedule changes.
  • Tests for due keyword selection and schedule advancement.

Confidence Score: 4/5

Safe after the migration sequencing issue is fixed.

The scheduling and UI changes are coherent, but the migration filename creates a real schema deployment mismatch for the new columns.

drizzle/0010_per_keyword_intervals.sql

T-Rex T-Rex Logs

What T-Rex did

  • I reproduced a migration version collision using the generated Node migration-order harness, which showed duplicate numeric prefix 0010 for 0010_high_liz_osborn.sql and 0010_per_keyword_intervals.sql, while the max migration prefix is 0025.
  • I noted that schedule_interval_override and next_check_at are added by drizzle/0010_per_keyword_intervals.sql, and there is no next-head migration above 0025 containing those column changes.
  • I validated the RankTracking code by running TypeScript compilation and Vitest tests; the TypeScript step completed with exit code 0 and the Vitest tests all passed with exit code 0 (three tests).

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
drizzle/0010_per_keyword_intervals.sql Adds keyword schedule columns and an index, but the migration filename collides with an existing 0010 migration and sits behind current migration history.
src/server/features/rank-tracking/services/RankTrackingService.ts Implements effective interval resolution, keyword schedule updates, due keyword filtering, and per-keyword schedule advancement.
src/server.ts Adds keyword-interval API handling and updates cron selection to combine config-level and keyword-level due schedules.
src/client/features/rank-tracking/RankTrackingDomainDetail.tsx Fetches keyword schedule metadata and wires bulk/per-row interval mutations into the rank tracking view.
src/client/features/rank-tracking/RankTrackingTableToolbar.tsx Adds keyword schedule API helpers and toolbar menu for filtered-list interval updates.
src/db/app.schema.ts Extends rankTrackingKeywords with per-keyword interval override and next-check timestamp fields.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant UI as RankTracking UI
participant API as /api/rank-tracking/keyword-intervals
participant Service as RankTrackingService
participant DB as rank_tracking_keywords
participant Cron as Scheduled cron
participant Workflow as RankCheckWorkflow

User->>UI: choose keyword interval override
UI->>API: PATCH projectId/configId/keywordIds/override
API->>Service: updateKeywordScheduleOverride()
Service->>DB: set schedule_interval_override and next_check_at
Cron->>Service: getDueKeywordsForScheduledRun(config, keywords, now)
Service-->>Cron: due keyword subset
Cron->>Service: advanceKeywordSchedulesForScheduledRun(dueKeywords)
Service->>DB: advance per-keyword next_check_at
Cron->>Workflow: beginRankCheckRun(keywordIds)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant UI as RankTracking UI
participant API as /api/rank-tracking/keyword-intervals
participant Service as RankTrackingService
participant DB as rank_tracking_keywords
participant Cron as Scheduled cron
participant Workflow as RankCheckWorkflow

User->>UI: choose keyword interval override
UI->>API: PATCH projectId/configId/keywordIds/override
API->>Service: updateKeywordScheduleOverride()
Service->>DB: set schedule_interval_override and next_check_at
Cron->>Service: getDueKeywordsForScheduledRun(config, keywords, now)
Service-->>Cron: due keyword subset
Cron->>Service: advanceKeywordSchedulesForScheduledRun(dueKeywords)
Service->>DB: advance per-keyword next_check_at
Cron->>Workflow: beginRankCheckRun(keywordIds)
Loading

Fix All in Claude Code Fix All in Codex Fix All in Cursor

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
drizzle/0010_per_keyword_intervals.sql:1
**Migration version collision**
This migration is numbered `0010`, but the repo already has `drizzle/0010_high_liz_osborn.sql` and later migrations through `0025`. On databases that have applied the existing history, adding another lower-numbered `0010` leaves this schema change outside the current migration sequence, so `schedule_interval_override` and `next_check_at` will be missing when the new code queries or updates them. Please regenerate this as the next migration after the current head.

Reviews (1): Last reviewed commit: "feat: per-keyword rank-check intervals f..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Adds optional per-keyword check intervals (manual/daily/weekly) on top of the
existing config-level schedule, with a migration, service + schema support,
cron wiring that advances both config and keyword schedules, UI columns and a
toolbar control, and service tests. Closes every-app#37.
@bensenescu

Copy link
Copy Markdown
Contributor

Hey @mvanhorn , thanks for you other contributions! Just giving you a heads up that I'm going to let this one sit for a few weeks because I want to give this feature more thought.

@bensenescu bensenescu added the waiting Not adding now given maintenance burden. label Jun 28, 2026
@bensenescu

Copy link
Copy Markdown
Contributor

@greptileai

@@ -0,0 +1,3 @@
ALTER TABLE `rank_tracking_keywords` ADD `schedule_interval_override` text DEFAULT 'inherit' NOT NULL;--> statement-breakpoint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Migration version collision
This migration is numbered 0010, but the repo already has drizzle/0010_high_liz_osborn.sql and later migrations through 0025. On databases that have applied the existing history, adding another lower-numbered 0010 leaves this schema change outside the current migration sequence, so schedule_interval_override and next_check_at will be missing when the new code queries or updates them. Please regenerate this as the next migration after the current head.

Artifacts

Repro: generated migration collision check harness

  • Contains supporting evidence from the run (text/javascript; charset=utf-8).

Repro: migration collision check output

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: drizzle/0010_per_keyword_intervals.sql
Line: 1

Comment:
**Migration version collision**
This migration is numbered `0010`, but the repo already has `drizzle/0010_high_liz_osborn.sql` and later migrations through `0025`. On databases that have applied the existing history, adding another lower-numbered `0010` leaves this schema change outside the current migration sequence, so `schedule_interval_override` and `next_check_at` will be missing when the new code queries or updates them. Please regenerate this as the next migration after the current head.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex Fix in Cursor

web-site-profile pushed a commit to web-site-profile/open-seo that referenced this pull request Jul 23, 2026
* chore: serve Docker self-hosting with vite preview

* save

@sorcerai sorcerai left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head is not merge-safe: drizzle/0010_per_keyword_intervals.sql collides with the existing 0010 migration; the change updates SQLite schema only (no src/db/pg parity or PG migration); and service/cron due logic narrows intervals to daily|weekly, breaking existing monthly configurations and inherited monthly keywords. Please regenerate migrations at the next sequence for both dialects, preserve monthly semantics end to end, and rerun focused interval/parity tests.

@sorcerai flagged three things. The migration moves off the colliding 0010
to 0026, the next sequence per drizzle/meta/_journal.json, with its journal
entry and snapshot. ScheduledInterval regains "monthly" so existing monthly
configs and inherited monthly keywords are no longer narrowed to daily or
weekly.

PG parity is not included: the repo has no Postgres dialect, schema, or
migration location to generate into.
@mvanhorn

Copy link
Copy Markdown
Contributor Author

daf88d1 covers two of the three.

The migration is renumbered to 0026, which is the next sequence per drizzle/meta/_journal.json, and it now has its journal entry and snapshot rather than sitting on disk unregistered. Monthly is back in ScheduledInterval, so existing monthly configs and keywords inheriting monthly are no longer narrowed to daily or weekly. Focused interval tests pass 4/4.

PG parity I could not do, and I would rather ask than guess: both drizzle.config.ts and drizzle-prod.config.ts declare dialect: "sqlite" with out: "./drizzle", and there is no PG schema or migrations directory in the repo. Where should PG migrations be generated? Once I know that I will add the parity migration and rerun the tests.

mvanhorn added 2 commits July 31, 2026 21:40
Main moved a long way since this branch, so this is a port rather than a
textual merge:

- Migration renumbered. Main reached 0036, so the interval migration was
  regenerated with drizzle-kit as 0037 rather than renumbered by hand, and the
  snapshot and journal come from the generator.
- Postgres dialect added on main after this branch, so the same two columns and
  index are mirrored into src/db/pg/app.schema.ts with migration 0014.
- Main extracted the cron body into runScheduledRankChecks. The per-keyword due
  filtering moved into that service instead of staying inline in server.ts.
- Main switched useRankTrackingColumns to an options object and added a
  location-aware volume column; the interval column adopts both.
- Main moved the filter helpers into RankTrackingFilters.logic.ts. The
  duplicates here were dropped and applyFilters was made generic in the row type
  so callers passing schedule-carrying rows get them back unwidened.
- Keyword scheduling split out of RankTrackingService into keywordScheduling.ts,
  with getValidatedConfig moved to its own module so the two do not form an
  import cycle. The DomainDetail schedule state moved into a useKeywordSchedules
  hook. Both were over the repo's size limits after the merge.

tsc, oxlint --type-aware and vitest (772 tests) all pass.
prettier formatting, and drop the flat re-exports from RankTrackingService that
knip flagged as unused. The RankTrackingService object is the public surface, so
callers that want the scheduling helpers directly now import them from
keywordScheduling, and the duplicated interval types are gone in favour of the
ones in @/types/schemas/rank-tracking.
@mvanhorn

mvanhorn commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@sorcerai all three are addressed, and main having moved on since your review answered the parity question I had left open.

Migration collision. Rather than renumber by hand I dropped my migration, merged main, and regenerated with drizzle-kit. Main had reached 0036, so it came out as drizzle/0037_rapid_reaper.sql with a generator-written snapshot and journal entry. The only files under drizzle/ that differ from main are the new migration, its snapshot, and the journal append.

PG parity. This is the part I said I would rather ask about than guess. Main has since added src/db/pg/schema.ts and drizzle-pg.config.ts, so there is now a real answer: the same two columns and the index are mirrored into src/db/pg/app.schema.ts, and drizzle-kit generated drizzle-pg/0014_blushing_vermin.sql. Both dialects now carry the change.

Monthly semantics. isScheduledInterval still returns true for monthly, and there is a test that keeps inherited keywords due on a monthly configuration. The config-level path in the cron now goes through main's isScheduledRankTrackingInterval, which treats everything except manual as scheduled, and computeNextCheckAt has its own monthly branch, so monthly survives on both the config and keyword paths.

Main had moved far enough that this was a port rather than a merge, so worth flagging what moved:

  • The cron body is now runScheduledRankChecks, so the per-keyword due filtering went in there instead of staying inline in server.ts.
  • useRankTrackingColumns became an options object and gained a location-aware volume column; the interval column adopts both.
  • The filter helpers moved to RankTrackingFilters.logic.ts. I deleted my duplicates and made applyFilters generic in the row type, otherwise rows carrying schedule fields get widened back to RankTrackingRow.
  • Keyword scheduling is now its own module rather than more surface on RankTrackingService, and the DomainDetail schedule state is a useKeywordSchedules hook. Both files were over the repo's size limits after the merge.

pnpm run ci:check passes, which is prettier, knip, both tsc projects and oxlint --type-aware, and vitest is 772 passing across 94 files. CI is green and the PR is mergeable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting Not adding now given maintenance burden.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support per-keyword rank tracking intervals

3 participants