docs(plans): remove plan artifacts

This commit is contained in:
tiennm99 committed 2026-07-07 10:19:51 +07:00
1 parent aa74379f81
commit 8e748c3153
12 files changed
-1273

No files matched your search

@@ -1,86 +0,0 @@
---
phase: 1
title: Implement command
status: completed
priority: P2
dependencies: []
---
# Phase 1: Implement command
## Overview
Add the `/wheelofnames` command inside `internal/modules/misc/misc.go`, using the current misc module pattern: small command factory returning `modules.Command`, `chathelper.ArgAfterCommand` for parsing, and `chathelper.Reply` for plain text response.
## Requirements
- Functional: parse text after `/wheelofnames` as comma-separated options.
- Functional: trim whitespace and drop empty options.
- Functional: reply usage when no valid options exist.
- Functional: randomly pick one option from valid entries; preserve duplicates as weighting.
- Non-functional: keep implementation local to misc; no storage dependency; no new module.
## Architecture
Flow:
```text
Telegram update
-> dispatcher matches wheelofnames
-> wheelOfNamesCommand handler
-> chathelper.ArgAfterCommand
-> splitWheelOptions
-> math/rand/v2 rand.N(len(options))
-> chathelper.Reply
```
Use a tiny pure helper for parsing:
```go
func splitWheelOptions(arg string) []string {
parts := strings.Split(arg, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
if s := strings.TrimSpace(part); s != "" {
out = append(out, s)
}
}
return out
}
```
No crypto-grade randomness required; this is a casual selection command, not auth, payment, or security behavior.
## Related Code Files
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/misc.go`
- Create: none
- Delete: none
## Implementation Steps
1. Add `math/rand/v2` import, unless existing Go version/toolchain rejects it; fallback to standard `math/rand` only if needed.
2. Update package comment to include `/wheelofnames`.
3. Add `wheelOfNamesUsage` constant, e.g. `Usage: /wheelofnames <option1>, <option2>, ...`.
4. Add `wheelOfNamesCommand()` returning public `modules.Command`.
5. Add command to `New()` after other public misc commands or near `/ping`.
6. Add `splitWheelOptions(arg string) []string` helper near the command.
7. Handler:
- return nil for nil message
- parse args with `chathelper.ArgAfterCommand(update.Message.Text)`
- reply usage if parsed options length is zero
- pick `options[rand.N(len(options))]`
- reply selected option as plain text
## Success Criteria
- [x] `misc.New` registers `wheelofnames` as public command.
- [x] Handler replies usage for missing/empty option list.
- [x] Handler never returns an empty option.
- [x] Handler uses plain text reply, preserving forum topic behavior through `chathelper.Reply`.
## Risk Assessment
Main risk: flaky tests if randomness is asserted too strictly. Mitigate by testing deterministic one-option input and set membership for multi-option input.
Security: no HTML parse mode; user-supplied option text is plain text, so no HTML escaping needed.
@@ -1,54 +0,0 @@
---
phase: 2
title: Update command surfaces
status: completed
priority: P2
dependencies:
- 1
---
# Phase 2: Update command surfaces
## Overview
Update all user-facing command surfaces required by `AGENTS.md` for a new Telegram command. This is an additive command, so no stats migration is needed.
## Requirements
- Functional: command appears in `/help` because it is public and registered in the misc module.
- Functional: command appears in `telegram-commands.json` for Telegram command menu setup.
- Functional: README misc module summary reflects the new command.
- Non-functional: no command rename/delete, so preserve stats behavior by doing nothing special.
## Architecture
The source of runtime behavior is `internal/modules/misc/misc.go`. `telegram-commands.json` is a manual Telegram command menu source, and `README.md` is the human-facing module overview.
Stats compatibility note:
- Adding `/wheelofnames` creates new command usage rows naturally when the stats hook records invocations.
- No existing stats rows need migration because no command is renamed or deleted.
## Related Code Files
- Modify: `/config/workspace/tiennm99/miti99bot/telegram-commands.json`
- Modify: `/config/workspace/tiennm99/miti99bot/README.md`
- Create: none
- Delete: none
## Implementation Steps
1. Add command entry to `telegram-commands.json` near other misc commands:
- `command`: `wheelofnames`
- `description`: concise public description, e.g. `Pick one random comma-separated option`
2. Update README Modules table for `misc` to include `/wheelofnames`.
3. Do not update stats migrations or system state; this is add-only behavior.
## Success Criteria
- [x] `telegram-commands.json` remains valid JSON.
- [x] README `misc` command list includes `/wheelofnames`.
- [x] No migration or legacy stats marker added.
## Risk Assessment
Risk: runtime registration and manual command menu source drift. Mitigate by updating both `misc.go` and `telegram-commands.json` in same implementation.
@@ -1,68 +0,0 @@
---
phase: 3
title: Validate behavior
status: completed
priority: P2
dependencies:
- 1
- 2
---
# Phase 3: Validate behavior
## Overview
Add focused tests for the new handler and run command-surface validation required for command changes.
## Requirements
- Functional: tests cover registration, usage reply, one-option selection, trimming, and empty segment handling.
- Non-functional: run focused tests first, then full suite and vet because command/menu/shared behavior changed.
## Architecture
Use existing misc test harness:
- `installMisc` in `internal/modules/misc/handlers_test.go` wires the misc module to `testutil.RecordingBot`.
- `TestNew_RegistersExpectedCommands` in `internal/modules/misc/misc_test.go` locks command names and visibility.
Avoid brittle randomness assertions:
- Single-option input should return that option exactly.
- Multi-option input should assert the reply is one of the non-empty trimmed options.
## Related Code Files
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/misc_test.go`
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go`
- Create: none
- Delete: none
## Implementation Steps
1. Update `TestNew_RegistersExpectedCommands` expected map:
- add `wheelofnames: modules.VisibilityPublic`
2. Add handler tests:
- `/wheelofnames` returns usage.
- `/wheelofnames , ,` returns usage.
- `/wheelofnames Alice` returns `Alice`.
- `/wheelofnames Alice, Bob, Carol` returns one of those exact strings.
- `/wheelofnames , Alice , , Bob ,` returns `Alice` or `Bob`, never empty/spaced.
3. Run formatting and focused tests:
- `gofmt -w internal/modules/misc/misc.go internal/modules/misc/misc_test.go internal/modules/misc/handlers_test.go`
- `go test ./internal/modules/misc`
4. Run broader validation because command menu/user-facing contract changes:
- `go test ./cmd/server ./internal/modules/misc`
- `go test ./...`
- `go vet ./...`
## Success Criteria
- [x] Focused misc tests pass.
- [x] Command menu/server tests pass.
- [x] Full `go test ./...` passes.
- [x] Full `go vet ./...` passes.
## Risk Assessment
Risk: random selection makes tests nondeterministic. Mitigate by avoiding exact expected value for multi-option tests.
Rollback: remove `wheelOfNamesCommand`, helper, tests, README entry, and `telegram-commands.json` entry.
@@ -1,63 +0,0 @@
---
title: Wheel of Names Misc Command
description: >-
Add public /wheelofnames command to pick one random comma-separated option in
the misc module.
status: completed
priority: P2
branch: main
tags:
- feature
- backend
blockedBy: []
blocks: []
created: '2026-07-03T04:04:35.848Z'
createdBy: 'ck:plan'
source: skill
---
# Wheel of Names Misc Command
## Overview
Add `/wheelofnames` to the existing `misc` module. The command accepts comma-separated options after the command text, trims whitespace, ignores empty entries, then replies with one randomly selected option. No storage, migrations, auth changes, or new module needed.
Assumptions:
- Public command, visible in `/help` and Telegram command menu.
- Usage reply when no valid comma-separated entries remain.
- One valid entry is allowed and returns that entry.
- Duplicate entries stay duplicated, so users can weight an option intentionally.
## Phases
| Phase | Name | Status |
|-------|------|--------|
| 1 | [Implement command](./phase-01-implement-command.md) | Completed |
| 2 | [Update command surfaces](./phase-02-update-command-surfaces.md) | Completed |
| 3 | [Validate behavior](./phase-03-validate-behavior.md) | Completed |
## Cross-Plan Dependencies
None. No unfinished project plans found under `plans/`.
## Key Files
| File | Action |
|------|--------|
| `internal/modules/misc/misc.go` | Add command registration, parser/helper, handler |
| `internal/modules/misc/misc_test.go` | Update registration expectations |
| `internal/modules/misc/handlers_test.go` | Add handler behavior tests |
| `telegram-commands.json` | Add public Telegram command menu entry |
| `README.md` | Update misc module command list |
## Acceptance Criteria
- `/wheelofnames a, b, c` replies with exactly one trimmed item from `a`, `b`, `c`.
- `/wheelofnames` and `/wheelofnames , ,` reply with usage text.
- Empty comma segments do not appear as choices.
- Command is public and included in module registration, `/help`, and `telegram-commands.json`.
- Focused tests pass, then full `go test ./...` and `go vet ./...` pass.
## Unresolved Questions
None.
@@ -1,34 +0,0 @@
# Plan Complete: Wheel of Names Misc Command
## Summary
| Field | Value |
|---|---|
| Plan | `plans/260703-0404-wheelofnames-misc-command/plan.md` |
| Status | completed |
| Phases | 3/3 |
| Files changed | 5 implementation/docs files + plan files |
| Tests | `go test ./internal/modules/misc`, `go test ./cmd/server ./internal/modules/misc`, `go test ./...`, `go vet ./...` |
## Work Completed
- [x] Added public `/wheelofnames` command in `misc`.
- [x] Parses comma-separated options, trims whitespace, ignores empty entries.
- [x] Replies usage when no valid option exists.
- [x] Picks one valid option with casual randomness.
- [x] Updated README and `telegram-commands.json`.
- [x] Added focused handler and registration tests.
## Documentation Updates
- README module command list updated.
- No `docs/` update needed; no architecture, setup, security, or deploy behavior changed.
## Known Limitations
- Randomness is non-cryptographic by design.
- Duplicate options are preserved as weighting.
## Unresolved Questions
None.
@@ -1,114 +0,0 @@
---
phase: 1
title: API Client And Env Config
status: completed
priority: P2
dependencies: []
---
# Phase 1: API Client And Env Config
## Overview
Create a small misc-local HTTP client for the wheelofnames API and load its
endpoint/token from system env. This phase adds the remote render capability
without changing command behavior yet.
## Requirements
- Functional: Read `WHEELOFNAMES_API_URL` and `WHEELOFNAMES_API_TOKEN` from env.
- Functional: POST JSON to the configured full endpoint URL.
- Functional: Include Bearer auth only when token is non-empty.
- Functional: Return GIF bytes and metadata needed by the command.
- Non-functional: Use a bounded `http.Client` timeout, around 30 seconds.
- Non-functional: Never log or expose `WHEELOFNAMES_API_TOKEN`.
- Non-functional: Allow `http://` for private Coolify/Docker networks and
`https://` for public deployments; reject other schemes.
## Architecture
Add `wheelofnames_beta_api_client.go` in package `misc`.
Suggested types:
```go
type wheelBetaAPIClient struct {
HTTP *http.Client
URL string
Token string
}
type wheelBetaAPIRequest struct {
Options []string `json:"options"`
WinnerIndex int `json:"winnerIndex"`
DurationMs int `json:"durationMs"`
HoldMs int `json:"holdMs"`
FPS int `json:"fps"`
Size int `json:"size"`
Theme string `json:"theme"`
}
```
Use a method like:
```go
func (c *wheelBetaAPIClient) Render(ctx context.Context, options []string, winner int) ([]byte, error)
```
Return errors for:
- client not configured
- invalid URL scheme or malformed URL
- request build/transport failure
- non-2xx response
- non-`image/gif` content type
- empty or oversized body
Keep body reads bounded. Use a conservative max such as 12 MiB because the bot
default remote render is now 512px, 20fps, and 7 seconds total.
## Related Code Files
- Create: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client.go`
- Create: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client_test.go`
- Modify: none in command path yet
## Implementation Steps
1. Add env constants:
- `WHEELOFNAMES_API_URL`
- `WHEELOFNAMES_API_TOKEN`
2. Add `newWheelBetaAPIClientFromEnv()` that trims env strings.
3. Implement URL validation and request construction.
4. Set request headers:
- `Content-Type: application/json`
- `Accept: image/gif`
- `Authorization: Bearer <token>` when token is non-empty
5. Encode request with fixed render options:
- `durationMs: 6000`
- `holdMs: 1000`
- `fps: 20`
- `size: 512`
- `theme: "classic"`
6. Read response body through `io.LimitReader`.
7. Add focused client tests using `httptest.Server`.
## Success Criteria
- [ ] URL unset returns a typed/configuration error that command can treat as fallback.
- [ ] Valid request test confirms JSON body and winner index.
- [ ] Valid request test confirms default render fields:
`durationMs=6000`, `holdMs=1000`, `fps=20`, `size=512`, `theme=classic`.
- [ ] Token test confirms Bearer auth header.
- [ ] HTTP 500 and 401 return errors without leaking response body as user text.
- [ ] Non-GIF content type returns error.
- [ ] Oversized response returns error.
## Risk Assessment
- Risk: Env URL points at service base URL instead of `/api/gif`.
Mitigation: Plan/docs explicitly define `WHEELOFNAMES_API_URL` as full endpoint.
- Risk: Remote service token appears in logs.
Mitigation: Never log request headers or token value; log only high-level status.
- Risk: 512px/20fps render takes longer than older 384px smoke path.
Mitigation: 30-second HTTP timeout and local fallback; tune only after
production timing data.
@@ -1,105 +0,0 @@
---
phase: 2
title: Command Integration And Fallback
status: completed
priority: P2
dependencies:
- 1
---
# Phase 2: Command Integration And Fallback
## Overview
Wire `/wheelofnamesbeta` to try the remote API first when configured, while
preserving the existing local GIF renderer as fallback and preserving Telegram
upload behavior.
## Requirements
- Functional: Keep `splitWheelOptions` and `pickWheelOption` as the only winner
selection source in the bot.
- Functional: Use remote GIF only when API call succeeds with valid GIF bytes.
- Functional: Fall back to `renderWheelOfNamesBetaGIF(options, winner)` on any
remote error.
- Functional: Preserve `sendAnimation`, `MessageThreadID`, filename, and
non-spoiler caption.
- Non-functional: Remote failure should log enough to diagnose status/error but
should not reply with stack traces or token-bearing details.
- Non-functional: Existing command behavior remains unchanged when env is unset.
## Architecture
Introduce a small orchestration helper in package `misc`:
```go
func renderWheelOfNamesBetaAnimation(ctx context.Context, options []string, winner int) ([]byte, int, int, int, error)
```
Return bytes plus Telegram metadata (`duration`, `width`, `height`). Suggested
metadata:
- Remote success: duration `7`, width/height `512`.
- Local fallback: existing `wheelBetaDuration`, `wheelBetaSize`,
`wheelBetaSize`.
Alternative: return a tiny struct:
```go
type wheelBetaAnimation struct {
Data []byte
Duration int
Width int
Height int
}
```
The command handler should stay simple:
1. parse options
2. pick winner
3. call `renderWheelOfNamesBetaAnimation`
4. send animation
5. on final render/upload failure, text fallback remains `options[winner]`
## Related Code Files
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_command.go`
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta.go` only if helper placement requires it
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go`
## Implementation Steps
1. Add the orchestration helper that creates the env client and attempts remote
render.
2. If client is unconfigured, skip remote without warning noise.
3. If client is configured but fails, log warning with safe fields:
- command: `wheelofnamesbeta`
- status/error category
- never token or request body
4. On remote success, return remote bytes and remote metadata.
5. On remote failure, call current local `renderWheelOfNamesBetaGIF`.
6. Update `wheelOfNamesBetaCommand` to use returned metadata in
`SendAnimationParams`.
7. Keep caption exactly `"Spinning..."`.
## Success Criteria
- [ ] URL unset path still sends local GIF and existing tests pass.
- [ ] URL configured path calls remote service and sends remote GIF.
- [ ] Remote success path uses Telegram metadata `duration=7`,
`width=512`, `height=512`.
- [ ] Remote failure path sends local GIF, not text, when local renderer works.
- [ ] Telegram upload failure still replies with the selected winner text.
- [ ] Caption does not contain the winner.
- [ ] Message thread forwarding remains covered.
## Risk Assessment
- Risk: Remote and local metadata diverge.
Mitigation: use explicit metadata struct; tests assert remote width/height and
local compatibility.
- Risk: Remote API chooses a different winner.
Mitigation: always pass `winnerIndex`; ignore remote winner headers for bot
caption/source of truth.
- Risk: A configured but down service makes the command slower.
Mitigation: timeout and fallback; consider lowering timeout later only after
production timing data.
@@ -1,88 +0,0 @@
---
phase: 3
title: Deployment Docs And Env Surfaces
status: completed
priority: P2
dependencies:
- 1
- 2
---
# Phase 3: Deployment Docs And Env Surfaces
## Overview
Document the new optional environment variables across local and Coolify
deployment surfaces so the bot can find the wheelofnames service without code
changes.
## Requirements
- Functional: Document `WHEELOFNAMES_API_URL` as the full `/api/gif` endpoint.
- Functional: Document `WHEELOFNAMES_API_TOKEN` as the Bearer token matching the
wheelofnames service `API_TOKEN`.
- Non-functional: `.env.example` must use placeholders, not real secrets.
- Non-functional: Deploy docs must explain private network HTTP URL vs public
HTTPS URL options.
- Non-functional: Compose comments should not force the feature on by default
if the service is not running.
## Architecture
The bot remains a single long-polling service. The wheelofnames service is a
separate container or external service. The bot uses outbound HTTP only.
The current wheelofnames service `compose.yml` forwards its own env from the
shell with `${VAR:-default}` fallbacks, including `PORT`, `API_TOKEN`,
`MAX_CONCURRENT_RENDERS`, and render limits.
Coolify/private-network example:
```env
WHEELOFNAMES_API_URL=http://wheelofnames:3000/api/gif
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>
```
Public URL example:
```env
WHEELOFNAMES_API_URL=https://wheelofnames.example.com/api/gif
WHEELOFNAMES_API_TOKEN=<same value as wheelofnames API_TOKEN>
```
## Related Code Files
- Modify: `/config/workspace/tiennm99/miti99bot/.env.example`
- Modify: `/config/workspace/tiennm99/miti99bot/compose.yml`
- Modify: `/config/workspace/tiennm99/miti99bot/docs/deploy-coolify-selfhosted.md`
- Optional modify: `/config/workspace/tiennm99/miti99bot/README.md` only if the
command description needs to mention remote rendering.
## Implementation Steps
1. Add optional env entries to `.env.example` under operational settings.
2. Add commented or placeholder entries in `compose.yml` near other operational
env vars.
3. Update deploy docs required/optional env table.
4. Add a short deployment note:
- URL unset = local renderer fallback.
- URL set + token = remote Remotion GIF rendering.
- Token should match wheelofnames service `API_TOKEN`.
- Bot default remote render request is 512px, 20fps, 7 seconds total.
5. Avoid suggesting multiple bot replicas; existing single-replica rule still
applies.
## Success Criteria
- [ ] `.env.example` includes both variables with safe placeholder values.
- [ ] `compose.yml` includes the env names without embedding secrets.
- [ ] Deploy docs tell operators how to wire both services.
- [ ] Docs state fallback behavior when env is absent or remote fails.
## Risk Assessment
- Risk: Docs imply a public bot ingress is required.
Mitigation: explicitly state this is outbound HTTP from the bot to
wheelofnames; Telegram transport stays long polling.
- Risk: Operators put a service base URL without `/api/gif`.
Mitigation: examples show full endpoint and implementation returns clear
safe error/fallback.
@@ -1,84 +0,0 @@
---
phase: 4
title: Validation And Regression Tests
status: completed
priority: P2
dependencies:
- 1
- 2
- 3
---
# Phase 4: Validation And Regression Tests
## Overview
Add focused regression tests and run the repo quality gates required for a
command integration touching HTTP, env config, and Telegram upload behavior.
## Requirements
- Functional: Test remote success, remote failure fallback, token header, and
malformed/non-GIF responses.
- Functional: Existing local renderer tests remain valid.
- Non-functional: Run lint/test gates required by `AGENTS.md`.
- Non-functional: Do not require a live wheelofnames service for unit tests.
## Architecture
Use `httptest.Server` for the wheelofnames API client. Keep tests in package
`misc` so they can use unexported helpers and existing test utilities.
Test matrix:
| Area | Scenario | Expected |
|---|---|---|
| Client | valid `200 image/gif` | returns GIF bytes |
| Client | default request payload | sends 6000ms spin, 1000ms hold, 20fps, 512px, classic |
| Client | token configured | sends `Authorization: Bearer ...` |
| Client | `401`/`500` | returns error |
| Client | `text/plain` 200 | returns error |
| Client | bad URL scheme | returns error |
| Command | env unset | local GIF path still sends animation |
| Command | remote success | upstream receives request; bot sends animation |
| Command | remote failure | bot sends local animation |
| Command | remote success in thread | message thread id preserved |
## Related Code Files
- Create: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client_test.go`
- Modify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go`
- Verify: `/config/workspace/tiennm99/miti99bot/internal/modules/misc/misc_test.go`
## Implementation Steps
1. Write client tests before wiring command behavior where practical.
2. Add command tests with `t.Setenv` and `httptest.Server`.
3. Confirm tests do not depend on real network.
4. Run focused package tests:
```sh
go test ./internal/modules/misc
```
5. Run full gates:
```sh
go test ./...
go vet ./...
```
6. If docs changed only, no Telegram command registration update is needed.
## Success Criteria
- [ ] `go test ./internal/modules/misc` passes.
- [ ] `go test ./...` passes.
- [ ] `go vet ./...` passes.
- [ ] Tests prove winner index is sent to remote API.
- [ ] Tests prove default render config is sent to remote API.
- [ ] Tests prove fallback keeps user-visible behavior stable.
## Risk Assessment
- Risk: Tests become flaky due package-level env.
Mitigation: use `t.Setenv`, avoid `t.Parallel` in env-sensitive tests.
- Risk: Recording bot cannot inspect uploaded file bytes.
Mitigation: assert upstream request count/body plus `sendAnimation` metadata;
client tests validate returned bytes.
@@ -1,161 +0,0 @@
---
title: Wheelofnamesbeta API Integration
description: >-
Route /wheelofnamesbeta GIF rendering through the self-hosted wheelofnames API
when configured, with the current local renderer as fallback.
status: completed
priority: P2
branch: main
tags:
- feature
- backend
- api
blockedBy: []
blocks: []
created: '2026-07-06T15:20:57.412Z'
createdBy: 'ck:plan'
source: skill
---
# Wheelofnamesbeta API Integration
## Overview
Integrate the existing Go `/wheelofnamesbeta` command with the new
`wheelofnames` Remotion service by reading the API endpoint from system env.
When `WHEELOFNAMES_API_URL` is set, the command sends the parsed options and
locally selected `winnerIndex` to that endpoint and uploads the returned GIF to
Telegram. When the env is unset, invalid, timed out, unauthorized, or the
service returns non-GIF/non-2xx, the bot keeps using the current local GIF
renderer.
Checked service repo state on 2026-07-06:
- `/config/workspaces/tiennm99/wheelofnames` is clean.
- Latest commit: `4cf72ea fix: align wheel labels and compose env`.
- Service now has dedicated radial label layout logic in
`src/remotion/wheel-label-layout.js`, tests for label layout, and
`compose.yml` env defaults via `${VAR:-default}`.
- API schema still accepts `durationMs`, `holdMs`, `fps`, `size`, and `theme`.
- Service composition duration is `durationMs + holdMs`; the requested bot
payload below renders a 7-second GIF.
Scope Challenge:
- Existing code: `/wheelofnamesbeta` already parses options, picks the winner,
renders local GIF bytes, sends `sendAnimation`, preserves message threads,
and falls back to text on local render/upload failure.
- Minimum changes: add a small API client, wire command render path through
remote-or-local selection, add env docs, and test remote success/failure.
- Complexity: expected 5-7 touched files, one new client file, four focused
phases. No command rename, stats migration, persistent jobs, or async flow.
- Selected mode: SCOPE REDUCTION / fast plan. The API service already exists;
this plan only integrates the command.
## Architecture
```text
/wheelofnamesbeta message
|
| splitWheelOptions + pickWheelOption (existing)
v
renderWheelOfNamesBetaAnimation(ctx, options, winner)
|
| if WHEELOFNAMES_API_URL unset
| -> existing renderWheelOfNamesBetaGIF fallback
|
| POST WHEELOFNAMES_API_URL
| Authorization: Bearer $WHEELOFNAMES_API_TOKEN when set
| JSON: { options, winnerIndex, durationMs, holdMs, fps, size, theme }
v
remote GIF bytes -> Telegram sendAnimation
|
| on any remote error
v
existing local GIF renderer -> Telegram sendAnimation
```
Environment contract:
- `WHEELOFNAMES_API_URL`: optional full endpoint URL, expected to include
`/api/gif`, for example `http://wheelofnames:3000/api/gif`.
- `WHEELOFNAMES_API_TOKEN`: optional Bearer token. Required for production
`wheelofnames` service deployments because that service refuses production
startup without `API_TOKEN`.
Remote render request values should be fixed in code for now:
```json
{
"options": ["alice", "bob", "carol"],
"winnerIndex": 1,
"durationMs": 6000,
"holdMs": 1000,
"fps": 20,
"size": 512,
"theme": "classic"
}
```
Do not add user-facing flags, persistent storage, or dynamic theme selection in
this integration pass.
## Phases
| Phase | Name | Status |
|-------|------|--------|
| 1 | [API Client And Env Config](./phase-01-api-client-and-env-config.md) | Completed |
| 2 | [Command Integration And Fallback](./phase-02-command-integration-and-fallback.md) | Completed |
| 3 | [Deployment Docs And Env Surfaces](./phase-03-deployment-docs-and-env-surfaces.md) | Completed |
| 4 | [Validation And Regression Tests](./phase-04-validation-and-regression-tests.md) | Completed |
## Dependencies
## Cross-Plan Dependencies
| Relationship | Plan | Status |
|---|---|---|
| Uses output from | `plans/260706-1441-wheelofnames-remotion-api/plan.md` | completed |
No unfinished project plans overlap this scope.
## Key Files
| File | Action |
|---|---|
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client.go` | Create remote API client and env loader |
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_command.go` | Route command through remote-or-local renderer |
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta.go` | Keep local renderer unchanged as fallback |
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/handlers_test.go` | Add command-level remote success/fallback tests |
| `/config/workspace/tiennm99/miti99bot/internal/modules/misc/wheelofnames_beta_api_client_test.go` | Create focused client tests |
| `/config/workspace/tiennm99/miti99bot/.env.example` | Document optional API env vars |
| `/config/workspace/tiennm99/miti99bot/compose.yml` | Add commented Coolify/private-network env hints |
| `/config/workspace/tiennm99/miti99bot/docs/deploy-coolify-selfhosted.md` | Document deployment wiring with wheelofnames service |
## Acceptance Criteria
- With `WHEELOFNAMES_API_URL` unset, `/wheelofnamesbeta` behaves like today.
- With `WHEELOFNAMES_API_URL` set and remote returns `200 image/gif`, the bot
uploads those GIF bytes via `sendAnimation`.
- The remote request uses `durationMs=6000`, `holdMs=1000`, `fps=20`,
`size=512`, and `theme=classic` by default.
- The request body contains the original parsed options and the locally chosen
`winnerIndex`; the service must not be the source of truth for the winner.
- If `WHEELOFNAMES_API_TOKEN` is set, request includes
`Authorization: Bearer <token>` and tests verify it.
- Remote failures fall back to the existing local GIF renderer and do not reveal
the winner in the caption.
- Timeout is bounded with an HTTP client timeout; no goroutine can hang on a
wedged render service.
- Docs and env examples explain that URL is optional and token is required for
production wheelofnames service auth.
- Focused tests pass, then `go test ./...` and `go vet ./...` pass.
## Out Of Scope
- Replacing `/wheelofnames`; only `/wheelofnamesbeta` changes.
- Removing the local Go GIF renderer.
- Async job polling, object storage, or cached GIF URLs.
- User-selectable themes, sizes, fps, or command syntax changes.
- Stats migrations or Telegram command menu changes.
## Unresolved Questions
None.
@@ -1,265 +0,0 @@
---
type: research
topic: mongodb-stats-design
created_at: 2026-07-01T03:00:06Z
---
# Research Report: MongoDB Stats Design
## Executive Summary
For this bot's `/stats` feature, best primary model is one aggregate document per
`(command, user)` pair, plus one anonymous command bucket when sender has no
username. This keeps documents bounded, uses MongoDB atomic `$inc`, and makes all
current views queryable without dynamic object keys.
Do not store stats as "one user document with commands map" or "one command
document with users map" as primary shape. Those designs make one query easy and
the opposite query expensive, create hot growing documents, and push us toward
dynamic field names. Pair docs are boring and better.
Recommended document:
```js
{ _id: "ping:42", cmd: "ping", uid: 42, user: "alice", n: 3 }
{ _id: "ping", cmd: "ping", n: 2 } // no public username
```
## Methodology
- Sources consulted: MongoDB official docs, Go driver docs, local codebase.
- Date: 2026-07-01.
- Key terms: MongoDB schema design, unbounded arrays, `$inc`, atomicity,
aggregation `$group`, `$sort`, compound indexes, ESR guideline.
- Scope: stats module only. Bot persistence remains one collection per module.
## Codebase Context
- Repo: Go Telegram bot.
- Storage: `internal/storage` has one MongoDB collection per module.
- Current stats: `count:*`, `user:*`, `pair:*` key families in one collection.
- Current views:
- `/stats`: top commands.
- `/stats users`: top users.
- `/stats user <name>`: commands by user.
- `/stats cmd <name>`: users by command.
- Mongo docs already store flattened BSON fields, so stats fields can be queried.
## Key Findings
### 1. Avoid Growing Nested User/Command Maps
MongoDB docs warn against unbounded arrays because growing documents hurt
performance and risk document size limits. The same practical concern applies to
ever-growing embedded maps like `commands` under a user or `users` under a
command: one document grows forever and becomes a write hotspot.
Source: MongoDB "Avoid Unbounded Arrays":
https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/unbounded-arrays/
### 2. Use Atomic `$inc` For Counts
MongoDB `$inc` creates missing numeric fields, increments by the given amount,
and is atomic within one document. This fits per-pair counter documents exactly.
Source: MongoDB `$inc` operator:
https://www.mongodb.com/docs/manual/reference/operator/update/inc/
MongoDB write operations are atomic at single-document level. Multi-document
updates are not atomic as a whole, so each counter increment should update one
aggregate document.
Source: MongoDB atomicity:
https://www.mongodb.com/docs/manual/core/write-operations-atomicity/
### 3. Use Aggregation For Cross-Dimension Totals
MongoDB `$group` combines documents by a group key. That fits:
```js
// top commands
[{ $group: { _id: "$cmd", n: { $sum: "$n" } } }]
// top users
[{ $match: { uid: { $gt: 0 } } },
{ $group: { _id: "$uid", user: { $first: "$user" }, n: { $sum: "$n" } } }]
```
Source: MongoDB `$group`:
https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/
Use `$sort` then `$limit` for top K. MongoDB can coalesce adjacent sort/limit so
only top N must be kept in memory.
Source: MongoDB `$sort` optimization:
https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/
### 4. Index From Query Shape, Not Guesswork
MongoDB aggregation can use indexes, especially early `$match` and `$sort`
stages. The ESR guideline says equality fields first, then sort, then range for
compound indexes.
Sources:
- Aggregation pipeline optimization:
https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/
- ESR guideline:
https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/
- Indexing strategies:
https://www.mongodb.com/docs/manual/applications/indexes/
For this module, start with no extra indexes or a tiny set. Traffic likely low.
Add indexes once data grows or `/stats` gets slow.
Useful indexes if needed:
```js
db.stats.createIndex({ cmd: 1, n: -1 }) // /stats cmd <name>
db.stats.createIndex({ uid: 1, n: -1 }) // /stats user <name>
db.stats.createIndex({ user: 1, updatedAt: -1 }) // resolve username
```
Do not add many indexes early. Each index costs memory/disk and write work.
## Comparative Analysis
| Shape | Example | Good | Bad | Verdict |
|---|---|---|---|---|
| User-rooted | `{ uid, user, commands: { ping: 3 } }` | `/stats user` easy | `/stats cmd` scans users, dynamic keys, growing doc | Reject |
| Command-rooted | `{ cmd, users: { "42": { n: 3 } } }` | `/stats cmd` easy | `/stats user` scans commands, hot command docs, dynamic keys | Reject |
| Pair-rooted | `{ cmd, uid, user, n }` | All 4 views queryable, bounded docs, atomic `$inc` | Aggregation needed for totals | Recommend |
| Event log | `{ cmd, uid, user, at }` per invocation | Full history | Too much data, needs rollups | Overkill |
## Implementation Recommendations
### Primary Model
Use one document per aggregate:
```go
type usageEntry struct {
Cmd string `bson:"cmd" json:"cmd"`
UserID int64 `bson:"uid,omitempty" json:"uid,omitempty"`
Username string `bson:"user,omitempty" json:"user,omitempty"`
N int64 `bson:"n" json:"n"`
}
```
Keys:
```text
<cmd> anonymous/no-public-username bucket
<cmd>:<uid> named user pair bucket
```
### Write Path
Use MongoDB `UpdateOne` with `upsert: true`:
```js
db.stats.updateOne(
{ _id: "ping:42" },
{
$set: { cmd: "ping", uid: 42, user: "alice" },
$inc: { n: 1 },
$currentDate: { updatedAt: true }
},
{ upsert: true }
)
```
This avoids read-modify-write races in the current generic `DocStore` loop.
### Read Path
Use Mongo queries/aggregation when backend is MongoDB. Keep in-memory fallback
for tests and no-database local run.
Queries:
```js
// /stats
db.stats.aggregate([
{ $group: { _id: "$cmd", n: { $sum: "$n" } } },
{ $sort: { n: -1, _id: 1 } },
{ $limit: 20 }
])
// /stats users
db.stats.aggregate([
{ $match: { uid: { $gt: 0 }, user: { $type: "string", $ne: "" } } },
{ $group: { _id: "$uid", user: { $first: "$user" }, n: { $sum: "$n" } } },
{ $sort: { n: -1, user: 1 } },
{ $limit: 20 }
])
// /stats user alice
db.stats.find({ uid: 42 }).sort({ n: -1, cmd: 1 }).limit(20)
// /stats cmd ping
db.stats.find({ cmd: "ping", uid: { $gt: 0 } }).sort({ n: -1, user: 1 }).limit(20)
```
### Username Rename
When a user calls a command with a new username, refresh all docs for that `uid`
with `UpdateMany({ uid }, { $set: { user } })`. It is not atomic as a whole, but
stats are best-effort and eventual consistency is acceptable.
### Migration
Existing prod data likely in old key prefixes. Options:
1. No migration: simplest, stats reset after deploy.
2. Read old + new for one release: more code, little value.
3. One-off migration script: only if historical stats matter.
Recommendation: no migration unless user explicitly wants historical stats.
## Common Pitfalls
- Do not use dynamic fields like `commands.ping` or `users.42` as primary model.
- Do not grow one document forever.
- Do not use generic get-then-put for hot counters on MongoDB.
- Do not over-index small collections.
- Do not use username as identity. Telegram username can change; use `uid`.
## Decision
Recommended: pair-rooted aggregate docs, Mongo atomic upsert increments, Mongo
aggregation for display views, memory fallback for tests.
## References
- MongoDB Avoid Unbounded Arrays:
https://www.mongodb.com/docs/manual/data-modeling/design-antipatterns/unbounded-arrays/
- MongoDB `$inc`:
https://www.mongodb.com/docs/manual/reference/operator/update/inc/
- MongoDB Atomicity:
https://www.mongodb.com/docs/manual/core/write-operations-atomicity/
- MongoDB `$group`:
https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/
- MongoDB `$sort`:
https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/
- MongoDB Aggregation Optimization:
https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/
- MongoDB ESR Guideline:
https://www.mongodb.com/docs/manual/tutorial/equality-sort-range-guideline/
- MongoDB Indexing Strategies:
https://www.mongodb.com/docs/manual/applications/indexes/
- MongoDB Go Driver Update:
https://www.mongodb.com/docs/drivers/go/current/crud/update/
- MongoDB Go Driver Indexes:
https://www.mongodb.com/docs/drivers/go/current/indexes/
## Next Steps
1. Confirm pair-rooted aggregate docs.
2. Finish stats implementation.
3. Run `go test ./internal/modules/stats ./internal/storage`.
4. Optional later: add Mongo integration test if local Mongo available.
## Unresolved Questions
- Keep old stats by migration, or accept reset?
- Add indexes now, or wait until data size/latency proves need?
@@ -1,151 +0,0 @@
---
type: research
date: 2026-07-06T11:34:00Z
topic: wheelofnamesbeta gif generation and Telegram delivery
skills:
- ck:research
- ck:project-organization
---
# Research Report: Wheelofnamesbeta Gif Generation
## Executive Summary
Use Go stdlib `image/gif` for GIF encoding. It supports multi-frame GIFs via
`EncodeAll`, frame delay in 1/100s, and loop control. Add only
`golang.org/x/image/font/basicfont` for self-contained bitmap text if labels or
winner text must appear inside frames. Avoid ffmpeg/cgo/runtime binaries.
Send the generated GIF with Telegram `sendAnimation`, not `sendPhoto`.
Telegram Bot API defines `sendAnimation` for GIF or silent H.264/MPEG-4
animation files, max 50 MB at current docs. Generate in memory, upload with
`models.InputFileUpload`, and set caption to the winner.
Recommended `/wheelofnamesbeta` behavior: parse same comma input as
`/wheelofnames`, choose winner once, render 6s spin plus 1s hold. Keep canvas
small, frame count low, and palette fixed to avoid CPU/file-size spikes.
## Research Methodology
- Sources consulted: 4
- Date: 2026-07-06
- Gemini: disabled by `.ck.json`; used WebSearch
- Key terms: Go animated GIF EncodeAll, golang.org/x/image font basicfont,
Telegram Bot API sendAnimation, GIF optimization frame count palette
## Key Findings
### 1. Technology Overview
`image/gif` is in Go stdlib and implements GIF decoding/encoding. `EncodeAll`
writes multiple paletted images as one GIF. The `GIF` struct stores successive
frames, delays in 1/100s, and loop count. This maps directly to "6s spin + 1s
hold": e.g. 30 frames at 20/100s each, then 1 final frame at 100/100s.
`golang.org/x/image/font` provides text drawing interfaces. `basicfont` provides
fixed-size font faces. `Face7x13` is self-contained, uses printable ASCII, and
needs no font files.
Telegram Bot API `sendAnimation` sends GIF or silent H.264/MPEG-4 animation
files and returns a sent message. Current limit: 50 MB. It supports normal
`chat_id` and `message_thread_id`, so it works for groups/topics unlike
`sendMessageDraft`.
### 2. Current State And Trends
- GIF is simple but inefficient compared to MP4. For this bot, low-res generated
vector-ish frames are small enough.
- Go stdlib avoids external binary dependencies and is deploy-safe on Coolify.
- `x/image/basicfont` is enough for readable short text; no custom TTF asset.
### 3. Best Practices
- Render fixed small canvas, e.g. 320x320.
- Use fixed palette and `image.NewPaletted`.
- Keep frame count low: 30 spin frames + 1 hold frame.
- Delay: spin frames 20/100s = 6s total; final hold 100/100s = 1s.
- Set `LoopCount: -1` so the GIF plays once. Telegram clients may still expose
replay behavior, but file intent is one pass.
- Put exact winner in caption too, because GIF autoplay/download can vary by
client settings.
### 4. Security Considerations
- No user-provided image decoding; generated pixels only, so image bomb risk low.
- Bound input names and rendered text length to avoid huge memory/caption work.
- Never write temp files; generate in memory.
- Avoid shelling to ffmpeg from command handler.
### 5. Performance Insights
GIF frame memory is width * height * frame count. At 320x320 and 31 frames, raw
paletted data is about 3.2 MB before encoding plus overhead. Acceptable for one
long-polling handler.
Drawing simple wedges/lines/text in Go is CPU-cheap. Avoid antialiasing and
large canvases. 20/100s frame delay keeps total frames modest.
## Comparative Analysis
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| stdlib `image/gif` | No new heavy dependency, pure Go, direct timing control | Basic drawing only | Use |
| `x/image/font/basicfont` | Small official Go subrepo, no font files | ASCII-ish visual, small font | Use for labels |
| `gg` | Easier antialiased drawing/text | Adds third-party dependency | Reject for beta |
| ffmpeg MP4/GIF | Better compression/quality | External binary, shell/runtime complexity | Reject |
## Implementation Recommendations
### Quick Start Guide
1. Add `/wheelofnamesbeta` command in `misc`.
2. Reuse `splitWheelOptions` and `pickWheelOption`.
3. Generate GIF bytes in memory.
4. Send using `b.SendAnimation` with `models.InputFileUpload`.
5. Caption final winner.
6. Add tests that assert `sendAnimation` and encoded GIF timing.
### Code Sketch
```go
buf := bytes.Buffer{}
err := gif.EncodeAll(&buf, &gif.GIF{
Image: frames,
Delay: delays, // 30 x 20, then 100
LoopCount: -1,
})
_, err = b.SendAnimation(ctx, &bot.SendAnimationParams{
ChatID: msg.Chat.ID,
MessageThreadID: msg.MessageThreadID,
Animation: &models.InputFileUpload{
Filename: "wheelofnamesbeta.gif",
Data: bytes.NewReader(buf.Bytes()),
},
Duration: 7,
Caption: "Winner: " + winner,
})
```
### Common Pitfalls
- `sendPhoto` can flatten GIF to a static image. Use `sendAnimation`.
- GIF delays are centiseconds, not milliseconds.
- Large canvases or many frames can bloat files fast.
- Telegram client autoplay is user-controlled; caption must contain winner.
## Resources And References
- Go `image/gif` docs: https://pkg.go.dev/image/gif
- Go `golang.org/x/image/font` docs: https://pkg.go.dev/golang.org/x/image/font
- Go `basicfont` docs: https://pkg.go.dev/golang.org/x/image/font/basicfont
- Telegram `sendAnimation`: https://core.telegram.org/bots/api#sendanimation
## Next Steps
1. Implement pure-Go GIF renderer in `internal/modules/misc`.
2. Add `/wheelofnamesbeta` command surfaces and tests.
3. Run focused misc tests, full `go test ./...`, and `go vet ./...`.
## Unresolved Questions
- None.