Skip to content

feat: per-target timezone support#3

Open
veronoicc wants to merge 2 commits into
Privex-chat:mainfrom
veronoicc:timezone
Open

feat: per-target timezone support#3
veronoicc wants to merge 2 commits into
Privex-chat:mainfrom
veronoicc:timezone

Conversation

@veronoicc
Copy link
Copy Markdown

@veronoicc veronoicc commented May 18, 2026

Summary

  • Add timezone column to targets table (SQLite migration v6 + Supabase schema)
  • New $timezone <@user> [tz] selfbot command to view/set/clear timezone per target
  • Full timezone parsing utility supporting IANA names (Europe/Berlin), abbreviations (EST, CET, JST), UTC/GMT offsets (UTC+2, GMT-5), bare offsets (+3), and plain numbers
  • Routine detector and sleep schedule analyzer now compute hours in the target's timezone (DST-aware via Intl.DateTimeFormat)
  • Selfbot commands $pattern, $history, $seen, $streak display times in target's timezone
  • API PATCH /api/targets/:userId validates and stores timezone server-side

Companion PR: Privex-chat/sentinel-web#2 — web UI changes (timezone modal, toggle, heatmap shifting, timezone-aware formatting across all pages).

Summary by CodeRabbit

  • New Features
    • Users can now set and manage a custom timezone preference to personalize analytics outputs
    • Added $timezone command to configure or clear timezone settings
    • Analytics commands now display results in the user's configured timezone instead of server time
    • Sleep schedule and routine detection now respect timezone settings for accurate tracking

Review Change Stack

Add timezone column to targets table (migration v6) with full
parsing utility supporting IANA names, abbreviations (EST/CET/JST),
UTC/GMT offsets, and bare numeric offsets. New $timezone selfbot
command to view/set/clear. Routine detector, sleep analyzer, and
time-displaying commands ($pattern, $history, $seen, $streak)
now use target's timezone for hour/day calculations. API PATCH
route validates and stores timezone via parseTimezone().
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Warning

Review limit reached

@Privex-chat, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 58 minutes and 48 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 415b734e-160f-47c6-90eb-a131766fe11c

📥 Commits

Reviewing files that changed from the base of the PR and between 2dc6c00 and 42a9789.

📒 Files selected for processing (6)
  • src/analyzers/sleep-schedule.ts
  • src/api/routes/targets.ts
  • src/commands/handler.ts
  • src/database/migrations.ts
  • src/utils/timezone.ts
  • supabase-schema.sql
📝 Walkthrough

Walkthrough

This PR adds per-target timezone support to the selfbot application. A new timezone utility module handles parsing and formatting, the database schema gains a timezone column, the API route persists timezone settings, and activity analyzers and command handlers use timezone-aware calculations for schedules and command output display.

Changes

Timezone-aware activity tracking and command output

Layer / File(s) Summary
Timezone utility library
src/utils/timezone.ts
New module provides timezone parsing from IANA names, abbreviations, and UTC offsets with fallback canonicalization, offset calculation for DST awareness, and timestamp formatting in target timezones using Intl or offset-based fallback.
Database schema and migration
src/database/schema.ts, src/database/migrations.ts, supabase-schema.sql
Schema version incremented to 6, targets table gains timezone TEXT column, and migration v6 adds the column with error tolerance.
API timezone update endpoint
src/api/routes/targets.ts
PATCH /api/targets/:userId route accepts timezone field, validates non-empty values via parseTimezone, clears with null or empty string, and rejects unrecognized timezone strings with 400 error.
Routine detector timezone integration
src/analyzers/routine-detector.ts
detectRoutine fetches target timezone from database and uses timezone-aware helpers to compute day-of-week and hour buckets for the 7x24 event grid instead of server-local Date methods.
Sleep schedule analyzer timezone integration
src/analyzers/sleep-schedule.ts
analyzeSleepSchedule refactors sleep periods to store millisecond timestamps, computes bedtime/wake time and all-nighter detection in target timezone, and uses timezone-adjusted day-of-week for weekday vs weekend bucketing.
Command handler timezone integration
src/commands/handler.ts
Commands $seen, $streak, $history, and $pattern now format activity timestamps in target timezone; new $timezone command parses and persists timezone settings; $help and command routing updated; output indicates configured timezone or "local time" fallback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Timezones across the world, now tracked and true,
Every schedule, streak, and pattern shines in your view,
From Tokyo to Madrid, the hours align,
No more server-local confusion—your timezone will shine! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: per-target timezone support' directly summarizes the main feature: adding timezone configuration per user target throughout the system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase-schema.sql (1)

38-49: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

This script can hard-fail on existing databases before migration runs.

For pre-existing public.targets tables, Line 48 (COMMENT ON COLUMN ...timezone) errors if the column does not exist yet. Since the file is meant to be re-run, add an idempotent ALTER TABLE ... ADD COLUMN guard before that comment.

Suggested fix
 CREATE TABLE IF NOT EXISTS public.targets (
@@
     CONSTRAINT targets_active_bool           CHECK (active IN (0, 1))
 );
 
+DO $$ BEGIN
+    IF NOT EXISTS (
+        SELECT 1
+        FROM information_schema.columns
+        WHERE table_schema='public' AND table_name='targets' AND column_name='timezone'
+    ) THEN
+        ALTER TABLE public.targets ADD COLUMN timezone TEXT;
+    END IF;
+END $$;
+
 COMMENT ON TABLE  public.targets            IS 'Tracked Discord user accounts.';
 COMMENT ON COLUMN public.targets.user_id    IS 'Discord snowflake ID (17-20 digit string).';
 COMMENT ON COLUMN public.targets.added_at   IS 'Unix epoch ms when the target was added.';
 COMMENT ON COLUMN public.targets.active     IS '1 = actively tracked, 0 = paused.';
 COMMENT ON COLUMN public.targets.timezone   IS 'IANA timezone identifier (e.g. Europe/Berlin) or UTC offset (e.g. UTC+2). NULL = server local time.';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase-schema.sql` around lines 38 - 49, The COMMENT ON COLUMN for
public.targets.timezone will fail if the timezone column doesn't exist; add an
idempotent ALTER TABLE guard before that comment that ensures the timezone
column exists (i.e., run an ALTER TABLE public.targets ... ADD COLUMN IF NOT
EXISTS timezone TEXT so the script can be re-run safely) and then keep the
existing COMMENT ON COLUMN public.targets.timezone line unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/analyzers/sleep-schedule.ts`:
- Around line 90-93: The all-nighter message uses UTC formatting (new
Date(s.startMs).toISOString()) which can shift the reported day for non-UTC
targets; update the code around getHourInTimezone(s.startMs, tz) to format the
date in the target timezone (tz) instead — e.g., replace the new
Date(...).toISOString().split("T")[0] usage with a timezone-aware formatter
(toLocaleDateString with { timeZone: tz } or a small helper like
getDateInTimezone) so the string pushed into irregularities (`All-nighter on
${dateStr}`) reflects the target timezone date for s.startMs.

In `@src/api/routes/targets.ts`:
- Around line 110-117: The timezone handling should trim input before deciding
whether to clear or parse: in the update handler where body.timezone is checked
(references: body.timezone, parseTimezone, setParts, params, reply), normalize
with body.timezone = typeof body.timezone === "string" ? body.timezone.trim() :
body.timezone (or equivalent) before the null/empty check so that
whitespace-only strings are treated as empty and push null into params/setParts
to clear the setting, and only call parseTimezone on the trimmed non-empty value
and return the 400 via reply for truly unrecognized timezones.

In `@src/commands/handler.ts`:
- Line 563: Update the usage examples sent by sendTempMessage so they show
accepted mention/raw ID formats; modify the message constructed in the
sendTempMessage call (where sendTempMessage(channelId, "...") is invoked) to
replace `@user` examples with `<`@user`>` and include a raw snowflake example like
`123456789012345678`, e.g. `$timezone <`@123456789012345678`> EST` or `$timezone
123456789012345678 UTC+2`, ensuring the displayed examples match the parser that
accepts `<@...>` mentions or raw IDs.
- Around line 527-531: The loop currently slices segments using UTC-hour
boundaries (segEnd = Math.min(t + (3_600_000 - (t % 3_600_000)), end)) which
mis-allocates time for non-integer timezone offsets and DST; change the
segmentation to compute the next local-hour boundary in the target timezone and
use that as segEnd instead. Replace the UTC-based segEnd calc in the loop that
updates buckets and uses getHourInTimezone(t, tz) with a timezone-aware helper
(e.g. getStartOfNextLocalHour(timestamp, tz) or equivalent) so segEnd =
Math.min(getStartOfNextLocalHour(t, tz), end), then continue to increment
buckets[getHourInTimezone(t, tz)] by segEnd - t so hour attribution matches
local hours and correctly handles DST and fractional offsets.

In `@src/database/migrations.ts`:
- Around line 134-137: The migration currently swallows all errors when running
db.exec("ALTER TABLE targets ADD COLUMN timezone TEXT") which can mark v6 as
applied even if the column wasn't added; change the logic in the v6 migration to
(1) attempt the ALTER inside a try/catch that only ignores the known "duplicate
column" error, (2) after the ALTER (or ignore) query the schema (e.g., PRAGMA
table_info('targets') or equivalent) to assert that the "timezone" column
exists, and (3) if the post-condition check fails rethrow or log a fatal error
so the migration fails instead of logging success; refer to db.exec, the v6
migration block, and log.info("Migration v6: targets.timezone column added")
when making these changes.

In `@src/utils/timezone.ts`:
- Around line 139-143: The current branch converts a numeric string like "5.5"
by truncating via Math.trunc (numVal) which silently accepts fractional offsets;
instead require plain-number inputs to be integers. In the block handling raw →
numVal (variables raw and numVal) and before calling buildFromOffset, add a
guard using Number.isInteger(numVal) (or equivalent) so only integer numeric
values with Math.abs(numVal) <= 14 are accepted; if the value is fractional, do
not call buildFromOffset (treat as invalid input so the caller falls through to
other parsing or returns an error). Keep buildFromOffset usage and sign
calculation the same for integer values.
- Around line 186-191: The current offset calculation using toLocaleString ->
new Date (variables d, utcStr, tzStr, utcD, tzD) is non-portable; replace that
round-trip with Intl.DateTimeFormat.formatToParts: use formatToParts on the
original Date (d) for timeZone "UTC" and for the target tz, extract numeric
parts (year, month, day, hour, minute, second), build timestamps via
Date.UTC(...) for each set of parts, then compute (tzTimestamp -
utcTimestamp)/60000 and round to minutes; update the function that returns the
offset so it uses these formatToParts-derived UTC values instead of parsing
locale strings.

---

Outside diff comments:
In `@supabase-schema.sql`:
- Around line 38-49: The COMMENT ON COLUMN for public.targets.timezone will fail
if the timezone column doesn't exist; add an idempotent ALTER TABLE guard before
that comment that ensures the timezone column exists (i.e., run an ALTER TABLE
public.targets ... ADD COLUMN IF NOT EXISTS timezone TEXT so the script can be
re-run safely) and then keep the existing COMMENT ON COLUMN
public.targets.timezone line unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 761b6370-4352-4c56-9f5e-2f9a6c8cd268

📥 Commits

Reviewing files that changed from the base of the PR and between bd42e24 and 2dc6c00.

📒 Files selected for processing (8)
  • src/analyzers/routine-detector.ts
  • src/analyzers/sleep-schedule.ts
  • src/api/routes/targets.ts
  • src/commands/handler.ts
  • src/database/migrations.ts
  • src/database/schema.ts
  • src/utils/timezone.ts
  • supabase-schema.sql

Comment thread src/analyzers/sleep-schedule.ts
Comment thread src/api/routes/targets.ts
Comment thread src/commands/handler.ts
Comment thread src/commands/handler.ts Outdated
Comment thread src/database/migrations.ts
Comment thread src/utils/timezone.ts
Comment thread src/utils/timezone.ts Outdated
- Sleep analyzer: format all-nighter dates in target tz, not UTC
- API: trim whitespace-only timezone strings before parsing
- Command: fix usage examples to show <@id>/snowflake formats
- Pattern: use local-hour boundaries for bucket segmentation
  (handles DST transitions and fractional offsets correctly)
- Migration v6: verify column exists post-ALTER, only ignore
  duplicate-column errors instead of swallowing all exceptions
- Parsing: reject fractional numeric inputs (e.g. '5.5')
- Offset calc: replace toLocaleString round-trip with portable
  Intl.DateTimeFormat.formatToParts
- Supabase: add idempotent ALTER guard before timezone COMMENT
@Privex-chat
Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants