Commit Graph

42 Commits

Author SHA1 Message Date
tiennm99 3e4e0e5b6e feat(lolschedule): per-chat subscribe/unsubscribe for the daily cron
Replaces the single LOLSCHEDULE_CHAT_ID env var with a KV-backed
subscriber list. New commands /lolschedule_subscribe and
/lolschedule_unsubscribe let each chat opt in/out. The cron now fans
out to every subscriber via Promise.allSettled so one blocked chat
cannot break the others.
2026-04-21 10:38:59 +07:00
tiennm99 c69d1749b7 feat(lolschedule): group by league, rename commands, add daily push cron
- Commands renamed: /lol_today → /lolschedule_today, /lol_week → /lolschedule_week.
- Today view groups events under a league header per section.
- Week view nests leagues under each ICT day.
- LEAGUE_ORDER gives tier-1 tournaments priority (Worlds / MSI / First Stand,
  then LCK / LPL / LEC / LCS, etc).
- New cron "0 1 * * *" (08:00 ICT) pushes today's major-league schedule to
  LOLSCHEDULE_CHAT_ID via the Telegram Bot API. Skips cleanly when chat id
  or token is missing, or when today has no major-league matches.
2026-04-21 10:38:59 +07:00
tiennm99 cbb572fdd8 refactor(lolschedule): drop stale Leaguepedia references and unused params
Module header and api-client top comment still mentioned Leaguepedia /
MatchSchedule / keying by league filter. fetchSchedulePage also exposed
an unused `leagueId` parameter and returned an unused `olderToken`;
remove both to match the actual usage.
2026-04-21 10:11:49 +07:00
tiennm99 e10269ca0a refactor(lolschedule): swap Leaguepedia for lolesports.com esports-api
Leaguepedia's anonymous IP rate limit is too aggressive for a bot even
from CF Worker egress (~1–2 req/min), and authenticated Fandom tokens
don't lift it. Switching to the lolesports.com getSchedule endpoint —
the same data feed powering the official site — removes the limit and
provides richer fields: state (unstarted/inProgress/completed), per-team
result.gameWins and outcome, league metadata, bestOf strategy.

Handlers simplify back to cache-first (120 s fresh / 1 h stale fallback)
with no cron needed. Results are filtered to major leagues (LCK, LPL,
LEC, LCS, worlds, msi, first_stand, LCP, CBLOL, EMEA Masters) to keep
the week view under Telegram's 4096-char message limit.
2026-04-21 10:11:49 +07:00
tiennm99 436664c8a1 fix(lolschedule): structured error logging for cargoquery failures
Swaps the best-effort console.warn for JSON log lines emitted via
console.log so Workers Observability + wrangler tail surface the real
cause (HTTP status, API error info, or non-JSON body) when /lol_today
and /lol_week fall into the error branch.
2026-04-21 09:54:02 +07:00
tiennm99 57c6528af7 docs(plans): add Leaguepedia API verification reports
Captures the feasibility check and auth-token investigation that led to
the lolschedule module: confirmed endpoint + table + field set, and
documented why caching beats token-based rate-limit mitigation on Fandom.
2026-04-21 09:36:39 +07:00
tiennm99 a7797f16b2 feat(lolschedule): add LoL esports match schedule module
New module exposing /lol_today and /lol_week commands, backed by the
Leaguepedia Cargo API (MatchSchedule table). Renders scores for
played/live matches and ICT times for scheduled ones. Caches range
queries in KV (60s today, 300s week) with stale-fallback on fetch error.
2026-04-21 09:36:39 +07:00
tiennm99 b180ea6660 fix(wordle): drop column alignment — use NYT share format (word above colors)
Cross-client column alignment between the marker row and a letter row is
unreliable in Telegram:
- <pre> monospace doesn't enforce equal width for emoji
- fullwidth Latin (U+FF21..FF3A) falls back to base Latin on mobile fonts
- squared-letter emoji (U+1F130/1F170..) render at different intrinsic
  widths than color-square emoji (U+1F7E8/1F7E9/2B1C) on many clients

Instead, render each guess as the word on one line followed by the
colored marker row — the standard NYT Wordle share format:

  CRANE
  🟩🟨🟩🟩

The association between a letter and its color is visually unambiguous
without depending on character-column alignment. Also drops the HTML
parse_mode requirement — replies are plain text again.
2026-04-20 22:52:40 +07:00
tiennm99 b228890ada fix(wordle): render letters as emoji-class characters for exact alignment
Previous fullwidth-Latin approach (U+FF21..FF3A) failed on mobile
Telegram clients because their monospace fonts don't ship fullwidth
glyphs — the codepoints fall back to base Latin at 1 cell, making the
letter row half as wide as the marker row.

Switch to Negative Squared Latin Capital Letters (U+1F170..1F189,
🅰🅱🅲..🆉) — these are emoji-class characters, so both rows are
drawn by the same emoji font at the same cell width. Column alignment
becomes a property of Unicode, not of the client's monospace font.

  🟩🟨🟩🟩
  🅲🆁🅰🅽🅴
2026-04-20 22:45:49 +07:00
tiennm99 568d5d4805 fix(wordle): align markers with letters using fullwidth Latin
Color-square emoji (🟩🟨) render at ~2 monospace cells wide in
Telegram's <pre> blocks; the previous letter row used " X " (3 cells
per letter = 15 cells total for a 5-letter word) against markers that
only span ~10 cells, so columns drifted.

Switching letters to fullwidth Latin (U+FF21..U+FF3A, e.g. 'A' instead
of 'A') puts each letter at exactly 2 cells via East Asian Width =
Fullwidth, matching one emoji per letter with no padding:

  🟩🟨🟩🟩
  CRANE

No spacing heuristics — alignment is a consequence of character width,
which every monospace-capable Telegram client respects.
2026-04-20 22:37:51 +07:00
tiennm99 78de7e1cd3 feat(wordle): render board in monospace so markers align with letters
Wrap renderGuess / renderBoard output in <pre> and send replies with
parse_mode: HTML. In Telegram's monospace font each " X " letter cell
is 3 characters wide, which is roughly the width of a single emoji
marker, so colored squares stack cleanly over the letter they score.

No user-controlled content lands inside the <pre> (guesses are
validated [a-z]{5}), so no HTML escaping is needed in the grid. The
inline <code>/wordle &lt;word&gt;</code> placeholder is properly
entity-encoded for HTML parse mode.
2026-04-20 22:30:17 +07:00
tiennm99 a2f67a7758 fix: project-wide review — trading safety, loldle drift guard, doc refresh
Code fixes:
- trading/handlers + stats-handler: guard ctx.from?.id to prevent
  cross-user state corruption when channel posts or inline queries lack
  a sender
- trading/prices + trading/symbols: encodeURIComponent on ticker before
  interpolating into TCBS API URLs
- trading/stats-handler: parallelize per-stock price fetches with
  Promise.allSettled so N-stock portfolios don't stack serial latency
- loldle/handlers: guard target champion lookup against champions.json
  refresh drift — start a fresh round or fall back to the stored id
- wordle + loldle: explicitly initialize giveup:false in startFreshGame
  for stable state shape
- wordle/lookup: fix stale JSDoc that claimed null return
- biome: ignore auto-generated champions.json / champions-data.js /
  words-data.js
- Apply formatter to src/index.js, loldle/handlers.js imports, and
  loldle/compare.test.js (previously red)

Docs refresh:
- README: 105+ tests -> 200+; wordle/loldle described as real modules
- architecture: module tree updated, test count 105 -> 200, runtime
  ~500ms -> ~2s, stub list narrowed to misc only
- codebase-summary: module table rewritten (wordle/loldle now Complete
  with real command lists and KV schema); test coverage table updated
- loldle/README: full rewrite matching the current implementation
  (was describing the original stub)
- New docs/development-roadmap.md tracking upcoming features
  (daily-mode for wordle + loldle, crypto/gold/forex trading, shared
  picker util, handler-level tests, coverage reporting, staging env)

Tests: 200/200 passing. Lint: clean.
2026-04-20 22:08:58 +07:00
tiennm99 785de9231a feat(wordle): port classic 5-letter guessing game
Replaces the wordle stub with a full implementation mirroring the loldle
module layout: compare/lookup/daily/render/state/handlers/index split,
per-subject KV state, standard 6 guesses, two-pass duplicate-letter
marking.

Commands: /wordle, /wordle_new, /wordle_giveup, /wordle_stats.

Word list (14,855 entries) sourced from dracos's gist
(https://gist.github.com/dracos/dd0668f281e685bad51479e5acaadb93) and
bundled via scripts/build-wordle-data.js. Credits in module README and
generated file headers.

Dispatcher test updated for the new command count (12 → 13).
2026-04-20 22:08:58 +07:00
github-actions[bot] ccbab962ea chore(data): sync champions from loldle-data@7a9d4f6 2026-04-20 14:31:23 +00:00
github-actions[bot] 563e31d207 chore(data): sync champions from loldle-data@b01967f 2026-04-20 14:19:07 +00:00
tiennm99 8ff581cfb8 chore(ci): remove sync workflow; loldle-data now pushes directly 2026-04-20 18:33:20 +07:00
dependabot[bot] 6de35d3e4f chore(deps): bump vite and vitest (#1)
* build(deps): bump vite and vitest

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) to 8.0.8 and updates ancestor dependency [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `vite` from 5.4.21 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `vitest` from 2.1.9 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: indirect
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat(loldle): share game state per-chat in groups

Groups and supergroups now share one daily puzzle + one stats counter
across all members. Private chats remain per-user.

- state.js: renamed key arg from userId to subject (user|chat id)
- handlers.js: getSubject(ctx) picks user id in DM, chat id in groups
- /loldle_stats labels scope as "your" vs "group" accordingly

* feat(loldle): add /loldle_new + switch to self-paced rounds

- /loldle_new starts a new random round. If the previous round is not
  solved/given-up, it's recorded as a loss (auto-giveup) before rerolling.
- Drop daily-seeded targets: each round picks a uniformly-random champion
  (pickRandom in daily.js; pickDaily kept for future use).
- state.js: one active round per subject (no date in key). TTL raised to
  7 days; streak = consecutive wins (round-based, not date-based).
- Register /loldle_new in module index; now 8 public loldle commands.
- Tests: add pickRandom cases; bump expected command count to 12.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: tiennm99 <tiennm99@outlook.com>
2026-04-20 17:55:40 +07:00
tiennm99 763ca8c696 feat(obs): enable Workers Observability (logs + traces)
- wrangler.toml: [observability] block with logs + traces at
  head_sampling_rate=1 (200k free events/day on free plan).
- src/index.js: structured request log emitted on every fetch
  (method/path/status/ms) — JSON shape so Observability's filter UI
  can index individual fields.
- Refactored fetch body into a route() helper so the log line sees the
  final response status.
2026-04-20 17:36:16 +07:00
tiennm99 f3f68293a6 build: add umbrella 'build' script chaining per-module builds 2026-04-20 17:27:18 +07:00
tiennm99 1e01437766 feat(loldle): port classic-mode game from loldle repo
Adds loldle module with classic-mode champion guessing. Ports comparison
logic from tiennm99/loldle (lib/classic-mode.js) and bundles champion
data from tiennm99/loldle-data. Adds GH Actions workflow that re-syncs
champions.json on cross-repo dispatch from loldle-data.

- Three public commands: /loldle, /loldle_giveup, /loldle_stats
- Per-user daily state + streak stats in KV (3-day TTL on games)
- champions-data.js wrapper sidesteps Node 24 / esbuild disagreement on
  JSON import attributes; generator script + npm run build:loldle-data
- register script now tolerates missing .env.deploy (env-file-if-exists)
  so Workers Builds can inject env vars directly
- fix(scripts): escape stray */ in migrate.js docstring that broke node
- 16 new unit tests (compare, daily, lookup); dispatcher test updated
  for the new command set
2026-04-20 17:10:08 +07:00
tiennm99 129a4cfd7d chore: remove completed plans and reports 2026-04-15 13:30:44 +07:00
tiennm99 6a4829e45b chore: add plan and phase reports for D1 + cron rollout 2026-04-15 13:29:48 +07:00
tiennm99 f5e03cfff2 docs: add D1 and Cron guides, update module contract across docs
- docs/using-d1.md and docs/using-cron.md for module authors
- architecture, codebase-summary, adding-a-module, code-standards, deployment-guide refreshed
- CLAUDE.md module contract shows optional crons[] and sql in init
- docs/todo.md tracks manual follow-ups (D1 UUID, first deploy, smoke tests)
2026-04-15 13:29:31 +07:00
tiennm99 97ee30590a chore: add ESLint with eslint-plugin-jsdoc and central typedefs
- flat config, JSDoc-only rules (no stylistic — Biome owns those)
- src/types.js defines Env, Module, Command, Cron, ModuleContext, Trade
- lint script now runs biome check + eslint src
2026-04-15 13:29:23 +07:00
tiennm99 d040ce4161 feat(trading): add trade history and daily FIFO retention cron
- trading_trades table (migration 0001) persists every buy/sell via optional onTrade callback
- /history [n] command shows caller's last N trades (default 10, max 50), HTML-escaped
- daily cron at 0 17 * * * trims to 1000/user + 10000/global via FIFO delete
- persistence failure logs but does not fail the trade reply
2026-04-15 13:29:15 +07:00
tiennm99 8235c9602e feat: add Cron Triggers support to module framework
- modules may declare crons: [{ schedule, name, handler }]
- handler signature (event, { db, sql, env }) matches init context
- scheduled() export in src/index.js dispatches to matching handlers with fan-out and per-handler error isolation
- registry validates cron entries and collects into registry.crons
- wrangler.toml [triggers] crons must still be populated manually by module author
2026-04-15 13:22:17 +07:00
tiennm99 83c6892d6e feat: add D1 storage layer with per-module migration runner
- SqlStore interface + CF D1 wrapper + per-module factory (table prefix convention)
- init signature extended to ({ db, sql, env }); sql is null when DB binding absent
- custom migration runner walks src/modules/*/migrations/*.sql, tracks applied in _migrations table
- npm run db:migrate with --dry-run and --local flags; chained into deploy
- fake-d1 test helper with subset of SQL semantics for retention and history tests
2026-04-15 13:21:53 +07:00
tiennm99 fb8c7518f7 feat: add meta.createdAt to portfolio for future analytics 2026-04-14 18:00:42 +07:00
tiennm99 e265cfa9b5 docs: update all docs to reflect current trading module state
Trading module now VN stocks only with dynamic symbol resolution.
Update test counts (105), remove crypto/gold/forex references from
project-level docs, update architecture file tree descriptions.
2026-04-14 17:38:31 +07:00
tiennm99 a34c1cf85f refactor: move totalvnd into meta.invested for extensibility
Portfolio schema now uses meta object: { currency, assets, meta: { invested } }.
Migrates old totalvnd field automatically on load. The meta object provides
a clean place for future per-user metadata without polluting the top level.
2026-04-14 17:33:11 +07:00
tiennm99 0d4feb9ef8 refactor: dynamic symbol resolution, flat portfolio, VN stocks only
Replace hardcoded 9-symbol registry with dynamic TCBS-based resolution.
Any VN stock ticker is now resolved on first use and cached in KV
permanently. Portfolio flattened from 4 category maps to single assets
map with automatic migration of old format. Crypto, gold, and currency
exchange disabled with "coming soon" message.
2026-04-14 17:22:05 +07:00
tiennm99 86268341d1 feat: use real BIDV bid/ask rates for forex conversion
Replace hardcoded 0.5% spread with live buy/sell rates from BIDV bank
API. Buying USD uses bank's sell rate (higher), selling USD uses bank's
buy rate (lower). Reply shows both rates and actual spread percentage.
2026-04-14 16:53:07 +07:00
tiennm99 f3aaf16d6a feat: simplify topup to VND-only, add bid/ask spread to convert
Topup now only accepts VND — users must convert to get other currencies.
Convert uses a 0.5% bid/ask spread: buying USD costs more VND (ask),
selling USD back gives less VND (bid). Simulates real forex behavior.
2026-04-14 16:06:36 +07:00
tiennm99 c8ce28a15b docs: add per-module READMEs with architecture and DB schemas
Each module now has a README.md documenting commands, architecture,
and database schema (KV keys, JSON structure, field descriptions).
Trading README enhanced with full schemas for user portfolio and
price cache objects.
2026-04-14 15:53:37 +07:00
tiennm99 d7988e38e6 docs: move module-specific docs to module READMEs
Extract trading module details (commands, data model, price APIs, file
layout) from architecture.md and codebase-summary.md into
src/modules/trading/README.md. Project-level docs now only contain
global framework info with pointers to module-local READMEs.
2026-04-14 15:37:33 +07:00
tiennm99 812ec04c81 docs: remove development roadmap — stale-prone, not useful 2026-04-14 15:33:42 +07:00
tiennm99 4277f11c48 docs: add CLAUDE.md and project documentation
Add CLAUDE.md for AI assistant context. Create four new docs:
deployment-guide.md (full deploy flow + secret rotation + rollback),
code-standards.md (formatting, naming, module conventions, testing),
codebase-summary.md (tech stack, modules, data flows, external APIs),
development-roadmap.md (completed phases + planned work).
2026-04-14 15:28:53 +07:00
tiennm99 0f0dc1c108 docs: update architecture and README for trading module
Add trading module section (§13) to architecture.md covering commands,
data model, price sources, and file layout. Update file trees, test
counts (56→110), and module registry snippet in both docs.
2026-04-14 15:24:01 +07:00
tiennm99 c9270764f2 feat: add fake trading module with crypto, stocks, forex and gold
Paper trading system with 5 commands (trade_topup, trade_buy,
trade_sell, trade_convert, trade_stats). Supports VN stocks via TCBS,
crypto via CoinGecko, forex via ER-API, and gold via PAX Gold proxy.
Per-user portfolio stored in KV with 60s price caching. 54 new tests.
2026-04-14 15:16:53 +07:00
tiennm99 e752548733 docs: add architecture guide and polish README intro
- README: add "Why" value prop, request-flow ASCII diagram,
  and "Further reading" links to the docs directory
- docs/architecture.md: new 14-section guide covering cold-start
  flow, module contract, static loader rationale, unified-namespace
  conflict detection, dispatcher minimalism, KVStore prefixing
  mechanics, deploy flow, security posture, testing philosophy,
  and non-goals
2026-04-11 10:25:32 +07:00
tiennm99 c4314f21df feat: scaffold plug-n-play telegram bot on cloudflare workers
grammY-based bot with a module plugin system loaded from the MODULES env
var. Three command visibility levels (public/protected/private) share a
unified command namespace with conflict detection at registry build.

- 4 initial modules (util, wordle, loldle, misc); util fully implemented,
  others are stubs proving the plugin system end-to-end
- util: /info (chat/thread/sender ids) + /help (pure renderer over the
  registry, HTML parse mode, escapes user-influenced strings)
- KVStore interface with CFKVStore and a per-module prefixing factory;
  getJSON/putJSON convenience helpers; other backends drop in via one file
- Webhook at POST /webhook with secret-token validation via grammY's
  webhookCallback; no admin HTTP surface
- Post-deploy register script (npm run deploy = wrangler deploy && node
  --env-file=.env.deploy scripts/register.js) for setWebhook and
  setMyCommands; --dry-run flag for preview
- 56 vitest unit tests across 7 suites covering registry, db wrapper,
  dispatcher, help renderer, validators, and HTML escaper
- biome for lint + format; phased implementation plan under plans/
2026-04-11 09:49:06 +07:00
tiennm99 e76ad8c0ee Initial commit 2026-04-11 08:43:37 +07:00