Skip to content

feat(v3.0): full production upgrade — menu lifecycle, pagination, security, 23 Block 1 fixes - #110

Merged
gamblecodezcom merged 4 commits into
mainfrom
claude/plan-v3-deployment-VPMpw
Feb 28, 2026
Merged

feat(v3.0): full production upgrade — menu lifecycle, pagination, security, 23 Block 1 fixes#110
gamblecodezcom merged 4 commits into
mainfrom
claude/plan-v3-deployment-VPMpw

Conversation

@gamblecodezcom

@gamblecodezcom gamblecodezcom commented Feb 28, 2026

Copy link
Copy Markdown
Owner

User description

Menu lifecycle overhaul:

  • replyMenu() TTL auto-vanish via ttlMs parameter
  • clearStaleMenuIds() on startup (24h threshold)
  • mainMenuSentAt / adminMenuSentAt timestamps on user schema
  • sendHelpMenu() fixed: bare ctx.reply() → replyMenu() (panel stacking bug)
  • replaceCallbackPanel() fallback now tracks message ID in user state
  • Admin mode toggle double-fire fixed

Feature upgrades:

  • User giveaway list: sendUserGiveawaysPage() 5/page + 2-min auto-vanish
  • Admin giveaway panel: activeGiveawaysKeyboard(page) 5/page pagination
  • pmenu_referral sub-menu with share deep-link button
  • Admin stats: active window indicator + Refresh button
  • Admin health panel: inline memory/errors/users/persist-age/uptime
  • Broadcast builder: Preview button sends to admin DM before mass send

Block 1 security & logic fixes:

  • getStartAppLink(): encodeURIComponent + regex whitelist
  • referralShareHTML(): escapeHtml() on code param
  • unwrapTelegramUrl(): safe fallback, scheme whitelist, www.t.me support
  • getDiscordLink(): null when not configured (no hardcoded fallback)
  • evaluatePendingActionTimeout(): >= boundary, NaN guard, no createdAt mutation
  • getPlayLink(): legacy user.playMode fallback restored
  • Weighted winner pool: splice-after-pick guarantees termination
  • deploy_status + logs: exec() → execFile() (no shell spawn)
  • SSHV: rejects null bytes, backticks, $( and ${ before exec
  • Startup env warnings: ADMIN_IDS, TELEGRAM_CHANNEL_ID, TELEGRAM_GROUP_ID

Centralized helpers: computeParticipantWeight(), getRealGiveaways(), isNewUserPromoEligible()

Metrics: added runewager_menu_stale_recoveries, pending_actions_timed_out, uptime_seconds

load_tooltips.sh: full rewrite — atomic write, --push/--pull flags, parameterized REPO_DIR,
HTML-safe tooltips, --dry-run mode, permission checks, guarded git ops

Tests: readdirSync wrapped in try/catch; isCatchAllRegexPattern expanded;
extractCommandHandlerNames supports let/var/no-semicolon

Infrastructure: pre-deploy-checks gate 3b (menu symbols), CHANGELOG.md created,
package.json bumped to 3.0.0

All 60 tests pass. node --check clean.

https://claude.ai/code/session_01VijmtzjN63WZJy5gYgJAKs

Summary by Sourcery

Upgrade bot to v3.0 with a hardened menu lifecycle, safer link and command handling, richer admin/user panels, and improved deployment tooling and metrics.

New Features:

  • Add TTL-based auto-vanish support for menu replies and startup cleanup of stale menu message IDs using new per-user menu timestamps.
  • Introduce paginated giveaway views for users and admins, a referral submenu with deep-link sharing, an upgraded admin health dashboard, and broadcast preview in the announce builder.
  • Expose new Prometheus metrics for stale menu recoveries, pending action timeouts, and uptime, and enhance health checks to support HTTP/HTTPS auto-detection.

Bug Fixes:

  • Sanitize start-app and referral links, harden Telegram/Discord URL unwrapping, and remove unsafe URL schemes and hardcoded Discord fallbacks.
  • Fix pending action timeout logic, restore legacy play-mode fallback, and ensure giveaway winner selection always terminates via a properly managed weighted pool.
  • Prevent menu stacking and untracked fallback panels, eliminate double admin-menu refreshes, and make ephemeral bonus prompts concurrency-safe.
  • Tighten SSH command validation and switch deploy/log inspection commands to safer execFile-based execution.
  • Expand catch-all regex detection and directory walking robustness in smoke tests to avoid false positives and IO failures.

Enhancements:

  • Centralize helpers for participant weighting, filtering real giveaways, and new-user promo eligibility to reduce duplication and clarify intent.
  • Refine admin stats navigation with active-window indicators and refresh controls, and keep the persistent user menu in sync with settings changes.
  • Improve health TLS configuration by allowing multiple safe certificate directories and adding a shared health request helper.

Build:

  • Rewrite load_tooltips.sh to support atomic writes, configurable repo location, dry-run mode, and guarded optional git operations.
  • Strengthen deploy and runtime scripts with directory creation, ownership and permission hardening, and an extra pre-deploy menu-symbol gate.

Deployment:

  • Add startup warnings for missing but important environment variables and enhance service/runtime scripts with stricter file and directory permissions.

Documentation:

  • Add a detailed CHANGELOG for versions 2.0.0–3.0.0 and update internal task and AI-instructions documentation to cover the v3.0 upgrade scope.

Tests:

  • Broaden smoke tests for regex pattern classification and handler-name extraction while making file traversal resilient to filesystem errors.

Chores:

  • Bump package version to 3.0.0 and refresh the project to-do board to reflect the completed v3.0 production upgrade.

CodeAnt-AI Description

v3.0: menu lifecycle, paginated giveaways, referral submenu, admin health, TTL auto-vanish, and security fixes

What Changed

  • Persistent and ephemeral menus no longer stack: stale menu IDs are cleared on startup, persistent menus record sent timestamps, replies use a single-menu flow, and replyMenu supports an auto-vanish TTL so temporary menus are removed after a timeout.
  • Giveaways are paginated: users see a 5-per-page giveaway list that auto-vanishes after 2 minutes; admins get a paginated admin giveaways panel with navigation callbacks.
  • Referral and promo flows improved: a referral submenu shows a share deep-link and safe HTML-escaped referral codes; ephemeral new-user promo prompts are transactional and dismissible.
  • Admin UX upgrades: inline health panel shows uptime, memory, active users (24h), persist age and error rate; admin stats keyboard highlights the active window and adds a Refresh button; broadcast builder now can send a preview DM to the admin before mass sending.
  • Safety and reliability fixes affecting visible outcomes: unsafe or malformed URLs are rejected (no accidental redirects or unsafe schemes), Discord link function returns null instead of a hardcoded fallback, deploy/logs outputs use safer commands so admins see correct output, SSHV rejects commands containing null bytes/backticks/command-substitution, pending-action expiry is detected strictly and expired actions are cleared.
  • Observability: new /metrics counters expose menu stale recoveries, pending-action timeouts, and uptime; startup warns when key optional env vars (admins, channel, group) are missing.
  • Operational scripts and tests: tooltip loading script rewritten for atomic writes, permission checks and optional git ops; smoke tests expanded to detect more catch-all regex forms and tolerate read errors.

Impact

✅ Fewer stacked/duplicate menus for users
✅ Clearer and safer referral/share links
✅ Fewer admin command injection and unsafe-output risks

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Paginated giveaway admin UI; preview for broadcast announcements; expanded health/metrics reporting.
  • Security

    • Tightened URL validation, TLS/path safety, stricter runtime file permissions and service umask, startup env validations.
  • Bug Fixes

    • Prevent duplicate giveaway winners; improved menu stale recovery and startup robustness.
  • Documentation

    • Added comprehensive CHANGELOG and upgrade guidance; expanded upgrade/deployment notes.
  • Scripts/Tooling

    • Rewrote tooltip loader with atomic writes, flags, dry-run, and optional git flow; updated deploy/startup scripts.
  • Tests

    • Hardened smoke tests and expanded parsing/regex handling.
  • Chores

    • Bumped version to 3.0.0; added pre-deploy checks.

…urity, 23 Block 1 fixes

Menu lifecycle overhaul:
- replyMenu() TTL auto-vanish via ttlMs parameter
- clearStaleMenuIds() on startup (24h threshold)
- mainMenuSentAt / adminMenuSentAt timestamps on user schema
- sendHelpMenu() fixed: bare ctx.reply() → replyMenu() (panel stacking bug)
- replaceCallbackPanel() fallback now tracks message ID in user state
- Admin mode toggle double-fire fixed

Feature upgrades:
- User giveaway list: sendUserGiveawaysPage() 5/page + 2-min auto-vanish
- Admin giveaway panel: activeGiveawaysKeyboard(page) 5/page pagination
- pmenu_referral sub-menu with share deep-link button
- Admin stats: active window indicator + Refresh button
- Admin health panel: inline memory/errors/users/persist-age/uptime
- Broadcast builder: Preview button sends to admin DM before mass send

Block 1 security & logic fixes:
- getStartAppLink(): encodeURIComponent + regex whitelist
- referralShareHTML(): escapeHtml() on code param
- unwrapTelegramUrl(): safe fallback, scheme whitelist, www.t.me support
- getDiscordLink(): null when not configured (no hardcoded fallback)
- evaluatePendingActionTimeout(): >= boundary, NaN guard, no createdAt mutation
- getPlayLink(): legacy user.playMode fallback restored
- Weighted winner pool: splice-after-pick guarantees termination
- deploy_status + logs: exec() → execFile() (no shell spawn)
- SSHV: rejects null bytes, backticks, $( and ${ before exec
- Startup env warnings: ADMIN_IDS, TELEGRAM_CHANNEL_ID, TELEGRAM_GROUP_ID

Centralized helpers: computeParticipantWeight(), getRealGiveaways(), isNewUserPromoEligible()

Metrics: added runewager_menu_stale_recoveries, pending_actions_timed_out, uptime_seconds

load_tooltips.sh: full rewrite — atomic write, --push/--pull flags, parameterized REPO_DIR,
  HTML-safe tooltips, --dry-run mode, permission checks, guarded git ops

Tests: readdirSync wrapped in try/catch; isCatchAllRegexPattern expanded;
  extractCommandHandlerNames supports let/var/no-semicolon

Infrastructure: pre-deploy-checks gate 3b (menu symbols), CHANGELOG.md created,
  package.json bumped to 3.0.0

All 60 tests pass. node --check clean.

https://claude.ai/code/session_01VijmtzjN63WZJy5gYgJAKs
… during PR merges

Restores 12 categories of codex changes that were incorrectly overwritten by
"current" (main) when resolving merge conflicts across 6 merge commits.

Security / TLS hardening (index.js):
- Restore resolveTlsCertPathIfAllowed(): multi-directory fallback for TLS certs
  (PROJECT_DIR, certs/, /etc/ssl, /etc/letsencrypt) — was collapsed to PROJECT_DIR only
- Restore isHealthTlsEnabled() to use resolveTlsCertPathIfAllowed()
- Restore requestHealthPayload(): centralized HTTP/HTTPS health fetcher with
  dynamic protocol detection — was inlined with hardcoded https in two places
- Update /health command to use requestHealthPayload() (shows protocol label)
- Update pamenu_tools_health to use requestHealthPayload()
- Update startHealthServer() TLS cert read to use resolveTlsCertPathIfAllowed()

Command injection protection (index.js):
- commandNeedsConfirmation(): restore pipe-to-bash/zsh detection,
  destructive-cmd-piped pattern, redirect pattern — main simplified to only catch "sh"
- commandBlocked(): restore explicit pipe-to-shell blocker pattern

Race condition fixes (index.js):
- deleteEphemeralBonusPrompt(): restore runUserMutation guards for safe
  read-delete-write of ephemeralBonusMsgId/ChatId
- sendEphemeralBonusPrompt(): restore guards (claimedPromo check, active promo lookup),
  per-user dynamic promo lookup via getActivePromoCodeForUser() (was hardcoded
  promoStore.code), "I Have Claimed" callback button, mutation-safe writes
- Add promo_confirm_claimed_next callback handler (button was added, handler missing)

Deploy/runtime hardening (deploy.sh, prod-run.sh, runewager.service):
- deploy.sh: restore data/backups/ mkdir, chown -R APP_NAME, chmod 0750/0640/0600
  permission hardening after npm install
- prod-run.sh: restore chmod 0750 for data/data/backups/logs dirs,
  chmod 0640 for log+session files, chown -R to service user after dir creation
- runewager.service: add UMask=0077 (prevents world-readable files from service)

.gitignore:
- Restore legacy root-level data file patterns: users.json, giveaways.json,
  promo.json, env.json, analytics*.json (pre-data/ directory structure)

All 60 tests pass. node --check clean.

https://claude.ai/code/session_01VijmtzjN63WZJy5gYgJAKs
@codeant-ai

codeant-ai Bot commented Feb 28, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@sourcery-ai

sourcery-ai Bot commented Feb 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements the v3.0 production upgrade with a menu lifecycle overhaul (stale menu cleanup, TTL-based auto-vanish, better tracking), paginated giveaway views for users and admins, a richer admin health/stats UX, safer URL/command handling and SSHV restrictions, centralized helpers and new metrics, along with a hardened load_tooltips.sh pipeline, improved tests, deployment checks, and file permission tightening.

Sequence diagram for paginated user giveaways menu with TTL auto-vanish

sequenceDiagram
  actor User
  participant Telegram
  participant Bot
  participant sendUserGiveawaysPage
  participant replyMenu

  User->>Telegram: Tap pmenu_giveaways button
  Telegram->>Bot: Callback pmenu_giveaways
  Bot->>Bot: getUser(ctx)
  Bot->>Telegram: answerCbQuery()
  Bot->>sendUserGiveawaysPage: ctx, user, page=1

  alt no running giveaways
    sendUserGiveawaysPage->>replyMenu: ctx, user, text("No giveaways"), extra(parse_mode, keyboard, ttlMs=120000)
    replyMenu->>Bot: clearOldMenus(ctx, user)
    Bot-->>replyMenu: menus cleared
    replyMenu->>Telegram: ctx.reply(text, telegramExtra)
    Telegram-->>replyMenu: sentMessage(message_id, chat.id)
    replyMenu->>Bot: store user.lastMenuMsgId, lastMenuChatId
    replyMenu->>Telegram: schedule delete after ttlMs
  else running giveaways
    sendUserGiveawaysPage->>sendUserGiveawaysPage: getRealGiveaways()
    sendUserGiveawaysPage->>sendUserGiveawaysPage: compute totalPages, safePage
    sendUserGiveawaysPage->>sendUserGiveawaysPage: slice current page giveaways
    sendUserGiveawaysPage->>sendUserGiveawaysPage: build text + join buttons + nav buttons
    sendUserGiveawaysPage->>replyMenu: ctx, user, text(paginated list), extra(parse_mode, keyboard, ttlMs=120000)
    replyMenu->>Bot: clearOldMenus(ctx, user)
    Bot-->>replyMenu: menus cleared
    replyMenu->>Telegram: ctx.reply(text, telegramExtra)
    Telegram-->>replyMenu: sentMessage(message_id, chat.id)
    replyMenu->>Bot: store user.lastMenuMsgId, lastMenuChatId
    replyMenu->>Telegram: schedule delete after ttlMs
  end

  %% Pagination callback
  User->>Telegram: Tap Next or Prev page button
  Telegram->>Bot: Callback user_giveaways_page_N
  Bot->>Bot: getUser(ctx)
  Bot->>Telegram: answerCbQuery()
  Bot->>sendUserGiveawaysPage: ctx, user, page=N
  sendUserGiveawaysPage->>replyMenu: ctx, user, text(new page), extra(parse_mode, keyboard, ttlMs=120000)
  replyMenu->>Bot: clearOldMenus(ctx, user)
  Bot-->>replyMenu: menus cleared
  replyMenu->>Telegram: ctx.reply(text, telegramExtra)
  Telegram-->>replyMenu: sentMessage(message_id, chat.id)
  replyMenu->>Bot: update user.lastMenuMsgId, lastMenuChatId
  replyMenu->>Telegram: schedule delete after ttlMs
Loading

Sequence diagram for admin health tools using requestHealthPayload

sequenceDiagram
  actor Admin
  participant Telegram
  participant Bot
  participant requestHealthPayload
  participant HealthServer

  Admin->>Telegram: Send /health command
  Telegram->>Bot: Command health
  Bot->>Bot: requireAdmin(ctx)
  Bot->>requestHealthPayload:()

  requestHealthPayload->>requestHealthPayload: read PORT, HTTPS_KEY_PATH, HTTPS_CERT_PATH
  requestHealthPayload->>Bot: isHealthTlsEnabled()
  Bot-->>requestHealthPayload: true/false
  alt TLS enabled
    requestHealthPayload->>HealthServer: HTTPS GET /health (rejectUnauthorized=false)
    HealthServer-->>requestHealthPayload: statusCode, JSON body, protocol https
  else TLS disabled
    requestHealthPayload->>HealthServer: HTTP GET /health
    HealthServer-->>requestHealthPayload: statusCode, JSON body, protocol http
  end
  requestHealthPayload-->>Bot: { data, statusCode, protocol }
  Bot->>Telegram: ctx.reply(formatted JSON with protocol label)

  %% Inline admin panel health check
  Admin->>Telegram: Tap pamenu_tools_health button
  Telegram->>Bot: Callback pamenu_tools_health
  Bot->>Bot: requireAdmin(ctx)
  Bot->>Telegram: answerCbQuery("Running health check...")
  Bot->>requestHealthPayload:()
  requestHealthPayload->>Bot: isHealthTlsEnabled()
  Bot-->>requestHealthPayload: true/false
  requestHealthPayload->>HealthServer: HTTP(S) GET /health
  HealthServer-->>requestHealthPayload: data, statusCode, protocol
  requestHealthPayload-->>Bot: { data, statusCode, protocol }
  Bot->>Telegram: ctx.reply("Health Check (PROTOCOL):" + data slice)
Loading

Flow diagram for rewritten load_tooltips.sh pipeline

graph TD
  A_start[Start load_tooltips.sh] --> B_resolve[Resolve SCRIPT_DIR and REPO_DIR]
  B_resolve --> C_parse_flags[Parse flags --push --dry-run --pull]

  C_parse_flags --> D_check_dry_run{DRY_RUN?}
  D_check_dry_run -->|yes| E_dry_info[Print target path and tooltip JSON]
  E_dry_info --> F_dry_validate[Validate JSON with python3 -m json.tool]
  F_dry_validate --> G_dry_exit[Exit without writing files]

  D_check_dry_run -->|no| H_perm[Ensure DATA_DIR exists and is writable]

  H_perm --> I_pull_check{DO_PULL?}
  I_pull_check -->|yes| J_git_pull[git -C REPO_DIR pull origin main]
  I_pull_check -->|no| K_skip_pull[Skip git pull]
  J_git_pull --> L_build_json
  K_skip_pull --> L_build_json[Build TOOLTIP_JSON string]

  L_build_json --> M_tmp[Write JSON to TMP_OUT]
  M_tmp --> N_validate[Validate TMP_OUT with python3 -m json.tool]
  N_validate --> O_atomic[Atomic mv TMP_OUT -> OUT]

  O_atomic --> P_gitignore_check{.gitignore exists?}
  P_gitignore_check -->|yes| Q_gitignore_entry{Has data/tooltips.json entry?}
  Q_gitignore_entry -->|no| R_append_gitignore[Append data/tooltips.json to .gitignore]
  Q_gitignore_entry -->|yes| S_skip_gitignore[Skip .gitignore change]
  P_gitignore_check -->|no| T_no_gitignore[Skip .gitignore handling]

  R_append_gitignore --> U_push_check
  S_skip_gitignore --> U_push_check[Check DO_PUSH]
  T_no_gitignore --> U_push_check{DO_PUSH?}

  U_push_check -->|no| V_done[Print completion message and exit]
  U_push_check -->|yes| W_stage[git add .gitignore in REPO_DIR]

  W_stage --> X_diff{Has staged .gitignore changes?}
  X_diff -->|no| Y_no_commit[No changes to commit]
  X_diff -->|yes| Z_commit_push[Commit and push .gitignore]

  Y_no_commit --> V_done
  Z_commit_push --> V_done
Loading

File-Level Changes

Change Details Files
Menu lifecycle overhaul with stale menu cleanup, timestamps, TTL, and safer panel replacement.
  • Add mainMenuSentAt/adminMenuSentAt to user schema and stamp them when persistent menus are sent.
  • Introduce clearStaleMenuIds() run at startup to clear transient and 24h-stale menu IDs and increment menuStaleRecoveries metric.
  • Extend replyMenu() to support ttlMs for auto-deletion and ensure clearOldMenus() runs before reply; also track auto-vanish deletions in user state.
  • Update sendHelpMenu() to use replyMenu() so help panels no longer stack.
  • Update replaceCallbackPanel() fallback path to track the sent message ID as lastMenuMsgId/lastMenuChatId.
index.js
Giveaway UX improvements: paginated user/admin views and centralized helper logic.
  • Add computeParticipantWeight() and getRealGiveaways() helpers and use them in finalizeGiveaway() and giveaway views.
  • Implement sendUserGiveawaysPage() with 5-per-page pagination, auto-vanish TTL, and navigation callbacks (user_giveaways_page_N).
  • Update activeGiveawaysKeyboard() to support pagination (5 per page) and add admin_gw_page_N handlers.
  • Fix weighted winner pool selection by splicing out chosen users to guarantee loop termination.
index.js
Referral menu and promo flow refinements with HTML safety.
  • Add pmenu_referral action that shows referral stats, boost status, and a share-deep-link button using t.me start parameters.
  • Harden referralShareHTML() by escaping the referral code via escapeHtml() and correcting copy punctuation.
  • Refactor sendEphemeralBonusPrompt() and deleteEphemeralBonusPrompt() to use runUserMutation and to gate on claimed/active promo state, plus a new promo_confirm_claimed_next action that marks promos claimed.
index.js
Admin stats/health panels and broadcast builder upgraded for better observability and safety.
  • Enhance adminStatsKeyboard() to show the active window (24h/7d/30d/lifetime) and add a Refresh button; wire all admin_stats_* callbacks to use it.
  • Implement admin_cmd_health to show an inline health panel with uptime, memory, active users, giveaways, persist age, error rate, and version, with a Refresh and navigation buttons.
  • Abstract health endpoint fetching into requestHealthPayload() and reuse it in /health and pamenu_tools_health handlers, including protocol detection and timeout handling.
  • Add an announce_preview action that sends a preview of the broadcast to the admin’s DM before sending to users.
index.js
URL, play-link, pending-action, and SSHV security hardening plus safer deploy/log shell commands.
  • Harden unwrapTelegramUrl() with scheme whitelisting, better t.me/telegram.me handling, and safe fallbacks instead of returning raw URLs on parse failure.
  • Change getDiscordLink() to return null instead of a hardcoded invite when the configured URL is missing or invalid.
  • Add getStartAppLink() route sanitization (regex whitelist + encodeURIComponent) and restore legacy user.playMode fallback in getPlayLink() with mode validation.
  • Rework evaluatePendingActionTimeout() to avoid mutating createdAt, treat NaN as expired, enforce >= timeout boundaries, and increment pendingActionsTimedOut metric.
  • Extend commandNeedsConfirmation()/commandBlocked() patterns for pipe-to-shell detection and harden SSHV commands by rejecting null bytes, backticks, and command substitution; keep exec for SSHV but rely on admin-only plus filters.
  • Switch deploy_status and logs commands from exec() to execFile() with validated arguments and remove shell redirection concatenation.
index.js
Metrics, startup/env checks, and TLS/health-path helpers for observability and configuration safety.
  • Add menuStaleRecoveries and pendingActionsTimedOut counters and expose them plus uptime_seconds via the /metrics endpoint.
  • Introduce startup env validation warnings for missing ADMIN_IDS, TELEGRAM_CHANNEL_ID/ANNOUNCE_CHANNEL, and TELEGRAM_GROUP_ID.
  • Add resolveTlsCertPathIfAllowed() for HTTPS cert/key resolution against whitelisted directories, integrate it into isHealthTlsEnabled() and health server startup.
  • Factor out requestHealthPayload() to standardize health endpoint HTTP/HTTPS probing with timeout and error handling.
index.js
Promo, ephemeral message, and settings-related behavioral fixes.
  • Refactor deleteEphemeralBonusPrompt() and sendEphemeralBonusPrompt() to avoid races using runUserMutation, enforce eligibility checks, and consistently clear tracking IDs after auto-delete.
  • Ensure settings_toggle_playmode also refreshes the persistent user menu so Play button labels update immediately.
  • Fix admin mode toggling actions to avoid double-firing refreshAdminMenuHeader() and rely on sendPersistentAdminMenu() to refresh the interface.
index.js
Production tooling: load_tooltips.sh rewritten for safety, atomicity, and configurability.
  • Rewrite load_tooltips.sh to resolve REPO_DIR dynamically, support --push/--pull/--dry-run flags, and work without hardcoded paths.
  • Implement atomic writes for data/tooltips.json using a temp file + JSON validation before mv, with permission checks and optional git operations limited to .gitignore.
  • Generate HTML-safe tooltip JSON with minimal tags and ensure data/tooltips.json is ignored via .gitignore, with optional commit/push for the ignore rule.
load_tooltips.sh
Smoke-test robustness and regex/command extraction improvements.
  • Wrap readdirSync in try/catch in the test directory walker to skip unreadable directories safely.
  • Expand isCatchAllRegexPattern() to recognize many more catch-all regex forms while avoiding false positives for specific action patterns.
  • Update extractCommandHandlerNames() to parse const/let/var command-name assignments and tolerate missing semicolons.
test/smoke.test.js
Deployment and runtime scripts gain stronger permission handling and pre-deploy safety checks.
  • Enhance deploy.sh to create data/backups and logs directories, chown them (and .env) to the app user when present, and tighten directory/file permissions.
  • Add a menu system symbol check (Gate 3b) to scripts/pre-deploy-checks.sh to verify key menu functions exist in index.js before deploy.
  • Harden prod-run.sh to ensure runtime directories exist with restrictive ownership and permissions for logs and data files.
deploy.sh
scripts/pre-deploy-checks.sh
prod-run.sh
Documentation and metadata updates for the v3.0 release.
  • Create CHANGELOG.md with detailed entries for versions 3.0.0, 2.1.0, and 2.0.0 following Keep a Changelog style.
  • Update todolist.md with a v3.0 deployment section summarizing implemented tasks and deferrals.
  • Bump package.json version from 2.1.0 to 3.0.0 and document the session in CLAUDE.md.
CHANGELOG.md
todolist.md
package.json
CLAUDE.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements version 3.0.0 with comprehensive enhancements: security hardening (TLS path validation, URL normalization, environment checks), observability improvements (health metrics, startup validations), giveaway system features (pagination, weighted selection), menu lifecycle management, deployment automation (permission controls, atomic writes), and extensive documentation.

Changes

Cohort / File(s) Summary
Documentation & Changelog
CHANGELOG.md, CLAUDE.md, todolist.md
Added comprehensive v3.0 release documentation, feature descriptions, and production deployment roadmap with status tracking and deferred items for v3.1.
Configuration & Manifest
.gitignore, package.json
Updated version to 3.0.0 and added legacy runtime data files (users.json, giveaways.json, promo.json, env.json, analytics\*.json) to ignore patterns.
Deployment & Runtime Scripts
deploy.sh, prod-run.sh, runewager.service
Hardened file permissions and ownership handling; added data/backups directory creation with 0750 permissions, chown to service user, and systemd UMask=0077 for restrictive file creation.
Core Application Logic
index.js
Introduced startup validations, TLS path resolution, health endpoint probing, menu stale recovery, weighted lottery computation, giveaway filtering, promo eligibility checks, and updated admin UI signatures for pagination support; hardened URL handling with Telegram/Discord link safety checks.
Tooltip Loading & Pre-Deploy
load_tooltips.sh, scripts/pre-deploy-checks.sh
Rewrote load_tooltips.sh from hardcoded to argument-driven with atomic writes, dry-run, git integration, and JSON validation; added pre-deploy gate for menu system symbol validation.
Test Infrastructure
test/smoke.test.js
Hardened directory traversal with error handling, expanded catch-all regex detection with whitespace normalization, broadened command handler literal extraction, and adjusted test expectations for enhanced pattern recognition.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex, size:XXL

Poem

🐰 Hop, hop, three-point-oh!
With TLS paths and metrics flow,
Weighted draws and menus clean,
The finest v3 you've seen!
Permissions tight, permissions right,
This upgrade shines so bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: a v3.0 production upgrade focusing on menu lifecycle, pagination, security fixes, and Block 1 improvements across the codebase.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/plan-v3-deployment-VPMpw

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.

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Feb 28, 2026

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 issues, and left some high level feedback:

  • In requestHealthPayload() you dynamically require('http')/require('https') on each call; consider lifting these requires to the module scope to avoid repeated synchronous requires in hot admin paths.
  • The SSHV executor comment mentions spawn but the implementation still uses exec; either switch to spawn as described or update the comment to reflect the actual execution strategy to avoid confusion for future maintainers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `requestHealthPayload()` you dynamically `require('http')`/`require('https')` on each call; consider lifting these requires to the module scope to avoid repeated synchronous requires in hot admin paths.
- The SSHV executor comment mentions `spawn` but the implementation still uses `exec`; either switch to `spawn` as described or update the comment to reflect the actual execution strategy to avoid confusion for future maintainers.

## Individual Comments

### Comment 1
<location path="index.js" line_range="557-566" />
<code_context>
+  if (!Number.isFinite(created) || !Number.isFinite(now)) {
+    const expiredType = ACTION_LABELS[user.pendingAction.type] || 'current action';
+    user.pendingAction = null;
+    pendingActionsTimedOut++;
+    return { hadPending: true, expired: true, expiredType };
+  }
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid incrementing `pendingActionsTimedOut` before it is initialized to prevent a temporal dead-zone ReferenceError.

Since `pendingActionsTimedOut` is declared with `let` later in the file, any future call to `evaluatePendingActionTimeout` that runs before the declaration will throw a `ReferenceError` due to the temporal dead zone. To make this robust against refactors, either move the `menuStaleRecoveries`/`pendingActionsTimedOut` declarations above all usages, or switch to `var` if you explicitly want hoisting semantics.
</issue_to_address>

### Comment 2
<location path="index.js" line_range="6862-6864" />
<code_context>
-  const { exec } = require('child_process');
-  const cmd = `journalctl -u runewager.service -n ${lines} --no-pager 2>/dev/null || tail -n ${lines} /tmp/runewager-3000.log 2>/dev/null || echo "No logs found."`;
-  exec(cmd, { timeout: 8000 }, async (err, stdout) => {
+  const lineCount = String(Math.min(Number(parts[1]) || 50, 200));
+  // v3.0 fix: use execFile with shell:false — lineCount is a validated safe integer string
+  execFile('journalctl', ['-u', 'runewager.service', '-n', lineCount, '--no-pager'], { timeout: 8000 }, async (err, stdout) => {
     const output = (stdout || '').trim() || (err ? `Error: ${err.message}` : 'No logs found.');
     const chunks = [];
</code_context>
<issue_to_address>
**suggestion:** Consider preserving a non-journalctl fallback for the logs command while keeping `execFile`-based hardening.

Switching to `execFile('journalctl', ...)` drops the earlier `tail` fallback when `journalctl` is missing or the unit name is wrong. On systems without systemd or with different service names, this will now just return an error instead of any logs. To keep both the security hardening and resilience, consider calling `execFile('tail', ['-n', lineCount, '/tmp/runewager-3000.log'])` if the `journalctl` call fails and `stdout` is empty.

Suggested implementation:

```javascript
const { execFile } = require('child_process');

bot.command(
  'logs',
  safeAdminHandler(
    'logs',
    { usage: '/logs [lines]', example: '/logs 50' },
    async (ctx) => {
      if (!requireAdmin(ctx)) return;
      const parts = (ctx.message.text || '').trim().split(/\s+/);
      const lineCount = String(Math.min(Number(parts[1]) || 50, 200));

      // helper to split output into Telegram-safe chunks and send
      const sendOutput = async (text) => {
        const chunks = [];
        for (let i = 0; i < text.length; i += 3900) {
          chunks.push(text.slice(i, i + 3900));
        }

        for (const chunk of chunks) {
          // adjust parse_mode/formatting if your bot uses a different convention
          // Using MarkdownV2 code block to avoid log formatting issues.
          await ctx
            .reply(`\`\`\`\n${chunk}\n\`\`\``, { parse_mode: 'MarkdownV2' })
            .catch(() => {});
        }
      };

      await new Promise((resolve) => {
        // Primary: systemd journal logs
        execFile(
          'journalctl',
          ['-u', 'runewager.service', '-n', lineCount, '--no-pager'],
          { timeout: 8000 },
          (err, stdout) => {
            const primaryError = err;
            const journalOutput = (stdout || '').trim();

            if (journalOutput) {
              // We got logs from journalctl; no fallback needed.
              sendOutput(journalOutput).finally(resolve);
              return;
            }

            // Fallback: logs from the local file via tail
            execFile(
              'tail',
              ['-n', lineCount, '/tmp/runewager-3000.log'],
              { timeout: 8000 },
              (tailErr, tailStdout) => {
                const fallbackOutput = (tailStdout || '').trim();
                const finalOutput =
                  fallbackOutput ||
                  (tailErr || primaryError
                    ? `Error: ${(tailErr || primaryError).message}`
                    : 'No logs found.');

                sendOutput(finalOutput).finally(resolve);
              }
            );
          }
        );
      });
    }
  )
);

```

1. If `index.js` already imports from `child_process`, adjust the `SEARCH`/`REPLACE` so you either add `execFile` to the existing destructuring (e.g. `const { execFile } = require('child_process');` or `const { exec, execFile } = require('child_process');`), rather than introducing a duplicate `require`.
2. If the file already defines a common `sendOutput`/chunking helper for other commands, you may want to refactor the duplicated logic here to call that helper instead of inlining it.
3. If your bot does not use `MarkdownV2` / code blocks for logs, update the `ctx.reply` options to match your existing formatting conventions.
</issue_to_address>

### Comment 3
<location path="index.js" line_range="2530-2538" />
<code_context>
     }
   }

+  // v3.0 fix: reject null bytes, backticks, command substitution to reduce injection surface
+  if (/[\x00`]|\$\(|\$\{/.test(command)) {
+    session.buffer = '[SSHV] Command rejected: contains disallowed characters (null byte, backtick, or $( / ${).';
+    await renderSshvConsole(ctx, session, 'Command rejected.');
+    return;
+  }
   await ctx.reply(`⏳ Running: \`${escapeMarkdownFull(command)}\``, { parse_mode: 'MarkdownV2' });
   await new Promise((resolve) => {
+    // Use spawn with shell:true but command is admin-only and blocked list is enforced above
     const child = exec(command, { cwd: session.cwd, timeout: 8000, maxBuffer: 2 * 1024 * 1024 }, async (err, stdout, stderr) => {
       const out = `${stdout || ''}${stderr || ''}`.trim();
</code_context>
<issue_to_address>
**nitpick:** The comment mentions `spawn` but the code still uses `exec`, which can be misleading for future maintainers.

In `executeSshvCommand`, the inline comment says "Use spawn with shell:true" but the code still calls `exec(command, ...)`. Please either update the comment to match the current `exec` usage or switch to `spawn` if that’s what you intended, to avoid confusion in this security-sensitive path.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread index.js
Comment thread index.js Outdated
Comment thread index.js Outdated
@codeant-ai

codeant-ai Bot commented Feb 28, 2026

Copy link
Copy Markdown

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Permissions Risk
    The new chown/chmod block may leave log/data files unwritable for the service if the service user/group isn't present or chown fails. Confirm ownership after chown and ensure fallback permissions keep logs writable by the process that runs the bot.

  • Atomic write ownership
    The script performs an atomic mv "$TMP_OUT" "$OUT" but does not set explicit ownership or file mode on the newly-written $OUT. Depending on who runs this script, the file may be owned by a different user than the app expects. Consider setting explicit chown/chmod for $OUT after the move (or create with desired owner/mode).

  • UMask Impact
    Adding UMask=0077 tightens file creation permissions for the service (owner-only). Verify this doesn't conflict with other systems (logrotate, external utilities) that expect group/other access or specific file modes.

  • Silent Skip
    The new directory walker swallows readdirSync errors and returns early; this can silently omit large parts of the tree (permission issues, transient I/O) and lead to incomplete file lists / false negatives in tests. Consider surfacing a debug/metric or falling back to a different behavior for known error classes.

  • Catch-all Detection
    The new isCatchAllRegexPattern set and checks cover many explicit forms but may miss equivalent catch-all expressions (e.g. unwrapped '[\s\S]', other equivalent quantifier combinations) and contains an odd entry '(\.|[\s\S])' which looks like a mixed/possibly unintended form. This can cause false negatives and let catch-all handlers slip through coverage checks.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (3)
index.js (3)

14-19: ⚠️ Potential issue | 🟠 Major

ADMIN_IDS needs strict numeric validation, not just non-empty checks

Line 29 warns only on empty arrays, but ADMIN_IDS can still be non-empty with invalid values (NaN) from Line 18. That silently breaks admin auth and admin-only workflows.

Suggested fix
 const ADMIN_IDS = (process.env.ADMIN_IDS || '')
   .split(',')
   .map((id) => id.trim())
   .filter(Boolean)
-  .map((id) => Number(id));
+  .map((id) => Number(id))
+  .filter((id) => Number.isInteger(id) && id > 0);

 // Startup env var validation — warn on missing optional-but-important vars
 if (!ADMIN_IDS || ADMIN_IDS.length === 0) {
   console.error('[WARN] ADMIN_IDS is empty — no admins configured. Admin commands will be inaccessible.');
+  throw new Error('Invalid ADMIN_IDS: provide a comma-separated list of numeric Telegram user IDs.');
 }

Based on learnings: Validate required environment variables are present and properly formatted before bot startup.

Also applies to: 27-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 14 - 19, ADMIN_IDS parsing currently coerces env
values with Number(...) which can produce NaN and silently break admin checks;
update the logic that builds ADMIN_IDS (using process.env.ADMIN_IDS and constant
ADMIN_IDS) to validate each trimmed entry is a valid integer (e.g., use a
numeric parse check and Number.isFinite/Number.isInteger) and either filter out
invalid entries with a warning or throw an error on startup if any invalid IDs
are present; ensure you log the offending raw values and fail fast when
ADMIN_IDS is required and empty after validation so admin-only workflows don't
silently break.

4215-4237: ⚠️ Potential issue | 🟡 Minor

Admin giveaway text and action list are built from different datasets

buildActiveGiveawaysText() excludes test giveaways, but the paginated keyboard flow here is fed with unfiltered running giveaways. That can create page/action mismatches vs the visible text.

Also applies to: 7763-7774

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 4215 - 4237, activeGiveawaysKeyboard is building the
paginated keyboard from the unfiltered giveaways list, which can desynchronize
it from the visible text produced by buildActiveGiveawaysText (which excludes
test giveaways); fix by applying the same filtering used by
buildActiveGiveawaysText to the giveaways array before paging (or accept a
pre-filtered list) in activeGiveawaysKeyboard and the other duplicate
implementation (lines noted around the second occurrence), so that functions
like activeGiveawaysKeyboard, buildActiveGiveawaysText and the admin_gw_page
callbacks operate on the identical filtered dataset and produce matching buttons
and text.

66-78: ⚠️ Potential issue | 🟠 Major

Nullable Discord links can propagate into invalid URL buttons

At Line 68 and Line 77, getDiscordLink() returns null for invalid input. Several menu/command builders pass Discord URLs directly into Markup.button.url(...); passing null can fail at runtime when those flows render.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 66 - 78, getDiscordLink currently returns null for
invalid inputs which then flows into UI builders (e.g., Markup.button.url) and
causes runtime failures; change getDiscordLink (and its use sites) so it never
passes null into URL buttons: make getDiscordLink return undefined for non-valid
links (or a guaranteed-valid fallback string) and update all call sites that
pass its result into Markup.button.url to guard on truthiness (e.g., const
discord = getDiscordLink(...); if (discord) add Markup.button.url(discord) ).
Also keep the parsing logic using unwrapTelegramUrl and URL intact and only
return a URL string when parsed.protocol === 'https:' and hostname matches
discord hosts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.js`:
- Around line 8835-8844: The health panel reads a nonexistent field
_errorRate.windowErrors so it always shows 0; update the template to use the
actual tracked property (use _errorRate.count) or compute the windowed error
total if intended—replace `_errorRate.windowErrors` with `(_errorRate && typeof
_errorRate.count === 'number' ? _errorRate.count : 0)` in the healthText
construction (or implement the proper window calculation using
_errorRate.windowStart/_errorRate.count if a time-windowed value is required).
- Around line 6862-6864: The current calculation for lineCount allows zero or
negative values (const lineCount = String(Math.min(Number(parts[1]) || 50,
200))) which can produce invalid journalctl behavior; replace this with a
clamped positive integer in the range [1,200] by parsing Number(parts[1]) into
an integer, forcing a default (e.g., 50) when NaN, then applying Math.max(1,
Math.min(parsed, 200)), and finally converting to a string before passing to
execFile (update the variable used in the execFile call that targets
'journalctl' with ['-u','runewager.service','-n', lineCount, '--no-pager']).
- Around line 561-563: The timeout boundary was flipped so an age exactly equal
to PENDING_ACTION_TIMEOUT_MS is treated as expired, conflicting with the
`/testall` checks; update the expiration check around (now - created) to treat
age === PENDING_ACTION_TIMEOUT_MS as NOT expired (use <= instead of < for the
non-expired branch) so the returned object from this check
(hadPending/expired/expiredType) preserves the `/testall` expectations and keeps
the final summary `TestAll complete — X passed, Y warnings, Z failures`
unchanged.

In `@load_tooltips.sh`:
- Around line 110-119: The conditional that checks for staged .gitignore changes
currently has "|| true", which forces the if to always succeed; remove the "||
true" from the if condition so the git diff --cached --quiet -- .gitignore
return code drives the branch (i.e. change the line to: if git -C "$REPO_DIR"
diff --cached --quiet -- .gitignore; then ...), or alternatively temporarily
disable errexit just for that check (e.g. run the diff under set +e / set -e) so
the info/warn/git commit/push logic in load_tooltips.sh runs correctly when
.gitignore has changes.

---

Outside diff comments:
In `@index.js`:
- Around line 14-19: ADMIN_IDS parsing currently coerces env values with
Number(...) which can produce NaN and silently break admin checks; update the
logic that builds ADMIN_IDS (using process.env.ADMIN_IDS and constant ADMIN_IDS)
to validate each trimmed entry is a valid integer (e.g., use a numeric parse
check and Number.isFinite/Number.isInteger) and either filter out invalid
entries with a warning or throw an error on startup if any invalid IDs are
present; ensure you log the offending raw values and fail fast when ADMIN_IDS is
required and empty after validation so admin-only workflows don't silently
break.
- Around line 4215-4237: activeGiveawaysKeyboard is building the paginated
keyboard from the unfiltered giveaways list, which can desynchronize it from the
visible text produced by buildActiveGiveawaysText (which excludes test
giveaways); fix by applying the same filtering used by buildActiveGiveawaysText
to the giveaways array before paging (or accept a pre-filtered list) in
activeGiveawaysKeyboard and the other duplicate implementation (lines noted
around the second occurrence), so that functions like activeGiveawaysKeyboard,
buildActiveGiveawaysText and the admin_gw_page callbacks operate on the
identical filtered dataset and produce matching buttons and text.
- Around line 66-78: getDiscordLink currently returns null for invalid inputs
which then flows into UI builders (e.g., Markup.button.url) and causes runtime
failures; change getDiscordLink (and its use sites) so it never passes null into
URL buttons: make getDiscordLink return undefined for non-valid links (or a
guaranteed-valid fallback string) and update all call sites that pass its result
into Markup.button.url to guard on truthiness (e.g., const discord =
getDiscordLink(...); if (discord) add Markup.button.url(discord) ). Also keep
the parsing logic using unwrapTelegramUrl and URL intact and only return a URL
string when parsed.protocol === 'https:' and hostname matches discord hosts.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e343847 and 92e93e6.

📒 Files selected for processing (12)
  • .gitignore
  • CHANGELOG.md
  • CLAUDE.md
  • deploy.sh
  • index.js
  • load_tooltips.sh
  • package.json
  • prod-run.sh
  • runewager.service
  • scripts/pre-deploy-checks.sh
  • test/smoke.test.js
  • todolist.md

Comment thread index.js Outdated
Comment thread index.js Outdated
Comment thread index.js Outdated
Comment thread load_tooltips.sh Outdated
Comment thread load_tooltips.sh Outdated
Comment on lines +110 to +119
# Only commit if .gitignore actually changed
if git -C "$REPO_DIR" diff --cached --quiet -- .gitignore || true; then
info "No .gitignore changes to commit."
else
git -C "$REPO_DIR" commit -m "chore: ensure data/tooltips.json is in .gitignore" -- .gitignore \
|| warn "git commit failed (non-fatal)"
info "Pushing to origin..."
git -C "$REPO_DIR" push origin main || error "git push failed"
info "Push complete."
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The condition that checks whether .gitignore has staged changes is incorrectly written as if git diff ... || true; then, which always evaluates to success and therefore always executes the "no changes" branch, meaning the script will never actually commit or push .gitignore even when there are real changes; remove the || true and rely on the if conditional semantics so the commit/push path runs only when there are staged changes. [logic error]

Severity Level: Major ⚠️
- .gitignore auto-commit never runs when using --push.
- Remote repo may miss data/tooltips.json ignore rule.
- Other clones risk committing data/tooltips.json accidentally.
- --push flag behavior contradicts documented script usage.
Suggested change
# Only commit if .gitignore actually changed
if git -C "$REPO_DIR" diff --cached --quiet -- .gitignore || true; then
info "No .gitignore changes to commit."
else
git -C "$REPO_DIR" commit -m "chore: ensure data/tooltips.json is in .gitignore" -- .gitignore \
|| warn "git commit failed (non-fatal)"
info "Pushing to origin..."
git -C "$REPO_DIR" push origin main || error "git push failed"
info "Push complete."
fi
# Only commit if .gitignore actually changed
if git -C "$REPO_DIR" diff --cached --quiet -- .gitignore; then
info "No .gitignore changes to commit."
else
git -C "$REPO_DIR" commit -m "chore: ensure data/tooltips.json is in .gitignore" -- .gitignore \
|| warn "git commit failed (non-fatal)"
info "Pushing to origin..."
git -C "$REPO_DIR" push origin main || error "git push failed"
info "Push complete."
fi
Steps of Reproduction ✅
1. From the repository root `/workspace/Runewager`, ensure `.gitignore` does not yet
contain the line `data/tooltips.json` (this is what the script is meant to add/guard at
`load_tooltips.sh:98-101`).

2. Run the script with push enabled: `./load_tooltips.sh --push` (entry point at
`/workspace/Runewager/load_tooltips.sh:1`, push logic at lines 105-120).

3. The script appends `data/tooltips.json` to `.gitignore` if missing
(`load_tooltips.sh:98-101`), then stages `.gitignore` via `git -C "$REPO_DIR" add
"$GITIGNORE"` (`line 108`).

4. The conditional at `load_tooltips.sh:111` executes: `if git -C "$REPO_DIR" diff
--cached --quiet -- .gitignore || true; then`. Because of `|| true`, this `if` branch
always evaluates as success, so the script always logs `info "No .gitignore changes to
commit."` (`line 112`) and never enters the `else` block.

5. As a result, the commit and push commands in the `else` block (`git commit` and `git
push` at lines 114-117) are never executed, even though `.gitignore` was actually changed
and staged. Verifying with `git status` shows `.gitignore` modified/staged locally but no
commit created; the remote repository remains without the updated ignore rule.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** load_tooltips.sh
**Line:** 110:119
**Comment:**
	*Logic Error: The condition that checks whether `.gitignore` has staged changes is incorrectly written as `if git diff ... || true; then`, which always evaluates to success and therefore always executes the "no changes" branch, meaning the script will never actually commit or push `.gitignore` even when there are real changes; remove the `|| true` and rely on the `if` conditional semantics so the commit/push path runs only when there are staged changes.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

@codeant-ai

codeant-ai Bot commented Feb 28, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

- TDZ fix: move pendingActionsTimedOut/menuStaleRecoveries declarations
  before evaluatePendingActionTimeout to eliminate temporal dead-zone risk
- Timeout boundary: revert < to <= so age exactly equal to 15m is NOT
  expired — aligns with /testall check and unit test expectations
- /logs line count: clamp to [1, 200] (adds Math.max(1,...) lower bound
  to reject negative/zero values from user input)
- Health panel: fix _errorRate.windowErrors → _errorRate.count (field
  did not exist; .count is the correct field on _errorRate object)
- /logs fallback: add execFile('tail') fallback when journalctl errors
  with no output (non-systemd systems); uses BOT_LOG_FILE env or default
- executeSshvCommand: fix comment — said "spawn" but code uses exec();
  updated to accurately describe exec with shell:true (admin-only)
- load_tooltips.sh: remove || true that defeated diff --cached check,
  causing .gitignore changes to never be committed on --push

All 60 tests pass. node --check clean.

https://claude.ai/code/session_01VijmtzjN63WZJy5gYgJAKs
@gamblecodezcom
gamblecodezcom merged commit 274815b into main Feb 28, 2026
5 of 6 checks passed
@gamblecodezcom
gamblecodezcom deleted the claude/plan-v3-deployment-VPMpw branch February 28, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants