390 Commits
Author SHA1 Message Date
tiennm99 887ddcce88 docs: add amlich improvement research, brainstorm decision, and completed plan 2026-08-18 23:00:39 +07:00
tiennm99 027a87f3c0 feat(amlich): hint ambiguous leap-month input and flag disputed month boundaries
- /duonglich appends a nhuan hint when the queried month is also that
  lunar year's leap month and the exact leap date exists
- both commands append a caveat when the result falls in a lunar month
  starting or ending on one of the seven razor-edge boundaries from
  2072 on (new moon within ~2 minutes of UTC+7 midnight)
- freeze the verified 1800-2199 month structure as golden testdata so
  a self-consistent engine change cannot silently shift boundaries
- close known-issues open questions 1 and 2
2026-08-18 23:00:31 +07:00
tiennm99 43589d3fe4 docs: remove completed plans, superseded reports, and journals
All eight plans are completed or cancelled and their behavior is now
described in README. Three reports contradicted shipped code: one
recommended keeping the lolesports gql client over PandaScore, two
analyzed the transport that migration removed. The rest is
pre-implementation research whose conclusions live in the code.

Drop the conventions reference to the deleted schema research.
2026-08-16 22:47:32 +07:00
tiennm99 a58aa150fe docs: correct stale module claims and document amlich edge cases
README omitted /lol_subscribe and /lol_unsubscribe, called gold opt-in
though an empty MODULES loads every catalog module, and left out both
the amlich 1800-2199 bound and the lol module's PandaScore token. The
gold factory comment repeated the same opt-in claim.

Promote the lunar algorithm decision record and known-issue list into
docs/ so they survive cleanups of plans/.
2026-08-16 22:47:24 +07:00
tiennm99 1652320150 docs: add lol schedule research, tgs feasibility reports, and pandascore journal 2026-08-10 10:00:53 +07:00
tiennm99 91b7461fe5 fix(amlich): extract continuity condition to satisfy staticcheck QF1001 2026-08-08 23:29:42 +07:00
tiennm99 aa7026706b docs(amlich): add algorithm comparison, calendar rules research, and known-issues reports
Research and A/B evaluation of the truncated-Meeus port vs a from-the-rules
Meeus ch.49 implementation: 9 disputed lunations in 1800-2199, both
historically verifiable ones side with the current implementation. Decision:
keep truncated-Meeus. Known-issues report scoped to dates from 1970 onward.
2026-08-08 23:27:14 +07:00
tiennm99 50e1cff5ce fix(amlich): correct lunation overshoot returning lunar day 0 for 4 dates
The mean-cycle lunation estimate in solarToLunar can overshoot by one when
the target day falls just before a new moon whose UTC+7 calendar day rounds
forward; the reference implementation steps back only once and returns day 0
for 13/4/1877, 16/3/1885, 7/5/2054 and 9/4/2062. Loop the step-back until the
month start is on or before the target day.

Tests: extend round-trip to the full 1800-2199 range with day-continuity
checks, pin the 4 overshoot dates, anchor leap-month placement to published
tables (15 leap + 5 no-leap years), and pin the two razor-edge lunations
(20/6/1944, 7/7/1967) verified against published Vietnamese calendars.
2026-08-08 23:27:14 +07:00
tiennm99 b6ea0059ce feat(amlich): add Vietnamese lunar calendar conversion module
/amlich converts duong lich to am lich (defaults to today, Asia/Saigon);
/duonglich converts am lich to duong lich with a nhuan flag for leap
months. Dates accept d, d/m, or d/m/yyyy - missing parts fill from today
in the input's calendar. Conversion is a dependency-free port of Ho Ngoc
Duc's algorithm at UTC+7, with rejection of impossible lunar inputs,
anchored by known Tet/leap-month dates and a 1950-2050 round-trip test.
2026-08-08 22:40:03 +07:00
tiennm99 203b4ba6a1 fix(wheelofnames): center spoiler winner between underscores instead of blank padding 2026-08-06 12:11:00 +07:00
tiennm99 d06d95df7d fix(wheelofnames): pad spoiler with figure space to match letter width in proportional font 2026-08-06 11:18:13 +07:00
tiennm99 95c60774bb fix(wheelofnames): left-pad spoiler winner instead of centering 2026-08-06 11:09:26 +07:00
tiennm99 fafc90b1d3 fix(wheelofnames): center spoiler winner with nbsp padding instead of leading dots 2026-08-06 11:05:52 +07:00
tiennm99 c4c32ba716 fix(wheelofnames): pad spoiler result to longest option so length can't leak winner 2026-08-06 10:54:04 +07:00
tiennm99 15f7e50305 feat(lol): replace schedule source with PandaScore API
Swap the lol module upstream from the lolesports.com gql persisted-query
client to PandaScore REST (/lol/matches, Bearer LOL_PANDASCORE_TOKEN,
free tier 1000 req/h). The gql transport broke whenever Riot redeployed
their frontend; PandaScore is a stable versioned contract.

ScheduleEvent, formatters, cron, and the bson cache shape are unchanged:
only the transport and response mapping moved. PandaScore league slugs
canonicalize to the existing major-league allowlist; results join to
opponents by team_id so reversed arrays cannot swap scores; outcomes are
declared only once upstream commits a winner, preserving the
score-pending rendering. A still-full final page now logs
lol_page_budget_exhausted and the live page budget covers 500 raw
matches per window.

Missing token short-circuits with lol_token_missing before any upstream
call; the 60-minute stale cache still covers outages. Document the new
env var and cancel the superseded Leaguepedia score-enrichment plan.
2026-08-05 17:39:20 +07:00
tiennm99 b76d0ca799 fix(lol): switch schedule fetch to lolesports.com gql persisted queries
Riot retired esports-api.lolesports.com and revoked its public x-api-key
(403 for everyone), so fetch schedules from the lolesports.com /api/gql
gateway instead using the web client's registered homeEvents operation.
The persisted-query ID comes from the frontend's operations manifest;
the package doc records how to refresh it when Riot rotates it, and a
rotated ID logs lol_persisted_query_rotated instead of caching an empty
schedule. Date windows are padded a day each side and filtered to exact
instants locally because the gateway's date params have unspecified
timezone semantics.
2026-08-05 16:29:25 +07:00
tiennm99 189d00105f fix(stock): key pending dividends by user and event to stop duplicate buttons
Repeated /stock_portfolio calls stacked a random-token pending key per
call, and applying the dividend cleaned up only the pressed one — the
rest kept live buttons until the TTL sweep. Keys are now deterministic
(pending-dividend:<userID>:<eventID>) so re-suggesting overwrites the
previous action, retires the old message's button, and applying leaves
exactly one key to delete. Callback data carries <userID>:<eventID>
instead of a token; presses are still validated against the stored
owner, chat, and message binding.
2026-08-05 13:59:48 +07:00
tiennm99 9b46c5f250 feat(stock): reduce cost basis when applying cash dividends
Cash dividends are a return of capital: the payout still credits the
VND balance, and now also lowers the position's remaining cost basis
(floored at zero) so the ticker's unrealized P&L reflects dividends
already received. Zero basis is now a valid open-position state;
negative basis remains invalid. Share dividends are unchanged.
2026-08-05 11:43:21 +07:00
tiennm99 1a7d327c90 feat(monkeyd): add /monkeyd_tags to report a novel's tags as hashtags
Replies with a hashtag line led by #MonkeyD, then a blank line and the novel
URL, sent as a code block so it can be copied in one tap. Each genre label
becomes one hashtag with spaces and punctuation stripped and every word
capitalised, since Telegram ends a hashtag at the first character that is not a
letter, digit, or underscore. Diacritics survive; a label with no letters is
dropped rather than emitted as a bare hash.

The command costs one request and runs inline under a short timeout rather than
in the background: handlers are dispatched one at a time, so a stalled fetch
would block every other command. It shares the export page cache.

Advance the submodule to the tag parser, which matches itemprop="genre"
microdata so the site-wide genre navigation stays out of the result.
2026-07-30 00:52:10 +07:00
tiennm99 365ee281d4 feat(monkeyd): accept an optional font size argument
/monkeyd_crawl <url> [font_size] sets the body text size in points, half points
included, bounded to 6-24. Omitting it sends no size at all so the crawler's
default applies, rather than defining a second default here that could drift.
The document caption reports the size used.

Also advance the submodule to the lower 10pt default: on the 90x160mm phone
page that fits about 43 characters per line instead of 36.
2026-07-30 00:16:57 +07:00
tiennm99 84ef519fb1 fix(monkeyd): make /monkeyd_crawl public and fix PDF font resolution
Advance the crawler submodule to the fix for the production failure "stat
usr/share/fonts/...: no such file or directory": the PDF writer was handing
fpdf a font path, and fpdf rewrote the absolute path into a
working-directory-relative one. Font data is now passed as bytes, with a
fallback font compiled into the binary when the host has none.

The runtime image therefore no longer installs DejaVuSans, which also removes
the font layer from the builder stage.

/monkeyd_crawl becomes public. The host allowlist and the single in-flight
export were already the controls that bound its cost; they now carry that job
alone, so both are load-bearing.
2026-07-29 23:46:33 +07:00
tiennm99 e05b0f5be0 feat(monkeyd): export monkeydd.com novels as PDF via /monkeyd_crawl
Add the monkeyd module, which crawls a novel and sends the rendered PDF back
as a Telegram document. Crawling and rendering come from the monkeyd-crawler
submodule, resolved through a go.mod replace directive.

The command is admin-only and restricted to monkeydd.com: one run makes
hundreds of outbound requests over minutes, and the extractor only understands
that site. Exports run one at a time and on a detached goroutine, because
handlers are dispatched synchronously and an inline crawl would block every
other command.

The runtime image gains DejaVuSans; font discovery probes system paths and the
distroless base ships none, so PDF rendering would otherwise fail in
production. CI checks out submodules and the builder copies the submodule
go.mod before go mod download, which needs it to resolve the build list.
2026-07-29 23:07:23 +07:00
tiennm99 126485c3cc docs(lol): add leaguepedia score enrichment research and plan
Research: after lolesports began marking matches completed without
publishing results, evaluated independent score sources. Established
that wrappers of lolesports (lolesportsapi.com, Pupix, Apify) inherit
the gap, and that getCompletedEvents/getStandings are equally stale, so
only a source with its own ingestion helps. Leaguepedia is the only free
candidate; commercial providers run $2k-10k/mo.

Plan: 4 phases for filling missing gameWins/outcome from Leaguepedia's
Cargo API. lolesports stays authoritative for schedule and for any score
it does publish. Additive throughout - every failure path degrades to the
existing "score pending" line.

Joins on team name plus date rather than team codes: Riot already sends
Team.Name matching Leaguepedia's Team1/Team2, which removes the code
mapping table and the per-split OverviewPage handling that the research
had flagged as the main maintenance burden.

Phase 1 is a hard gate. Fandom rate-limited and then refused the
connection during research, so it was never confirmed that Leaguepedia
actually holds the scores Riot is missing. That check can cancel the plan.

Plan is pre-validation. A verification pass found three items not yet
recorded in the files: slices.ContainsFunc in phase 4 has no precedent in
this repo (explicit loops are the house style), the TTL index citation
should read startup.go:35-52, and the Bo2 tie handling in phase 3 is
unnecessary - all 26 allowlisted-league events are Bo3 with zero ties.
2026-07-26 12:57:56 +07:00
tiennm99 20d950661b fix(lol): omit score when a finished match has none published
lolesports drives event.state off the broadcast timeline but fills
result.outcome and result.gameWins from a separate per-game ingestion
path. A match therefore reads as "completed" for hours before gaining a
score, and in that window every team carries {"outcome": null,
"gameWins": 0}.

The renderer used `Result != nil` as its has-a-score test, which cannot
catch this: the result object is present, only its contents are empty.
The absent gameWins fell through to Go's zero value and printed as a
literal 0, so an unscored series was reported as a definitive draw
("MKOI 0-0 KC") with neither side bolded.

Gate the score on a declared outcome instead, and render unscored
finished matches as the matchup alone with a pending marker. An outcome
on either side is enough to trust the score - it proves the ingestion
ran. inProgress keeps rendering 0-0, which is truthful for a live series
that has not resolved its first game.

Also collapses the score extraction both branches duplicated into
shared helpers.
2026-07-26 09:16:39 +07:00
tiennm99 824e71b3ec docs(stock): record stock info delivery 2026-07-23 12:27:12 +07:00
tiennm99 26b969b5ef feat(stock): add detailed stock info command 2026-07-23 12:27:12 +07:00
tiennm99 34ce93aa69 docs(stock): finalize stock-events delivery 2026-07-23 10:08:47 +07:00
tiennm99 7c5da0f7f6 docs(stock): add stock-events plan artifacts 2026-07-23 10:05:44 +07:00
tiennm99 7123ea77c3 feat(stock): add stock events lookup 2026-07-23 10:03:41 +07:00
tiennm99 e59da33241 docs(stock): record dividend retirement workflow 2026-07-22 18:56:07 +07:00
tiennm99 27820970d9 fix(stock): retire dividend command and migrate stats 2026-07-22 18:55:57 +07:00
tiennm99 12cee156c8 docs(portfolio): document mobile column layout 2026-07-22 17:36:29 +07:00
tiennm99 7fad8067e1 fix(portfolio): tighten mobile column formatting 2026-07-22 17:36:29 +07:00
tiennm99 039780749a feat(stock): persist per-user dividend history 2026-07-22 16:17:29 +07:00
tiennm99 8ed0048005 feat: format compact portfolio numbers 2026-07-22 12:27:52 +07:00
tiennm99 f4f7d02861 fix(stock): note currency in portfolio title 2026-07-22 10:59:40 +07:00
tiennm99 a964fe6d96 docs: remove completed implementation records 2026-07-21 19:13:34 +07:00
tiennm99 9849fcec87 docs(deploy): refresh persisted storage layout 2026-07-21 19:13:28 +07:00
tiennm99 1cc7e4cae6 refactor(coin): retire completed startup cleanup 2026-07-21 19:13:23 +07:00
tiennm99 58f293b45c refactor(metrics): remove unused AI counters 2026-07-21 19:13:15 +07:00
tiennm99 36a6db8957 chore(stock): log dividend event checks 2026-07-21 18:50:48 +07:00
tiennm99 de498f92b9 fix(coin): remove stale dividend cursor fields 2026-07-21 18:50:30 +07:00
tiennm99 2ae3b505b6 docs(stock): document dividend event buttons 2026-07-21 18:20:14 +07:00
tiennm99 a739cdca06 feat(stock): add dividend event buttons 2026-07-21 18:19:50 +07:00
tiennm99 31f921b913 docs(stock): research dividend event APIs 2026-07-21 17:13:43 +07:00
tiennm99 dbe0c9ca70 refactor(portfolio): retire completed migrations 2026-07-21 17:13:19 +07:00
tiennm99 6fe91f0e4e feat(portfolio): nest asset positions 2026-07-21 16:42:33 +07:00
tiennm99 1c6e0ad8a0 docs(portfolio): document cost basis and P&L 2026-07-21 15:52:30 +07:00
tiennm99 e5dc898fde feat(portfolio): track stock and coin cost basis 2026-07-21 15:52:05 +07:00
tiennm99 ad31801c32 refactor(commands): standardize parameter conventions 2026-07-21 14:51:28 +07:00