Known gaps
Who is this for
Section titled “Who is this for”The owner, and any developer or operator taking the project over. Read this before promising anything to a client and before trusting a note inside the repositories.
- Before the next event: there is no working fallback AI provider; the server is shared with another project; backups stay on the same server; parallel load was never measured.
- Legal: storing personal data of Uzbekistan citizens on a server abroad is an unanswered legal question.
- Product: the bot and the kiosk are Russian only; the Uzbek landing text has not been proofread by a native speaker; station branding is hard-coded for one event.
- Landing: mobile LCP is about 3.5 s, limited by first-screen JavaScript.
- Notes vs code: the in-repo notes are partly stale. This site follows the code; the differences are listed below so nobody “fixes” the code to match an old note.
Severity: High — can stop an event or create legal exposure. Medium — degrades quality, cost or maintainability. Low — cosmetic or stale documentation.
Before the next event: the short list
Section titled “Before the next event: the short list”| Priority | Gap | Owner |
|---|---|---|
| 1 | R1 No working fallback AI provider | Owner (account), engineer (test) |
| 2 | R2 Shared droplet | Owner (cost), engineer (move) |
| 3 | R3 Backups not offsite, R5 no external uptime check | Engineer |
| 4 | R4 Parallel load never measured | Engineer |
| 5 | L1 Legal answer on data location | Owner, lawyer |
| 6 | P1 Bot and kiosk Russian only — if the audience is not Russian-speaking | Owner, engineer |
Planned order: Roadmap. Choices still open: Open decisions.
Reliability and infrastructure
Section titled “Reliability and infrastructure”| # | Gap | Severity | Detail | Where to read |
|---|---|---|---|---|
| R1 | No working fallback AI provider | High | The pipeline supports a second provider, but no key is configured and the second provider’s image and vision branches have never run against the real API. A provider outage is a product outage. | ADR-001 |
| R2 | Shared droplet | High | The server also hosts an unrelated project with its own database. It competes for memory and CPU, and a broken web-server file of the neighbour plus a restart takes HTTPS down for both. | Runbook |
| R3 | Backups are not offsite | High | Daily database copies are kept on the same server. Losing the droplet loses the database and its backups, unless a manual snapshot exists. | Backups & data lifecycle |
| R4 | Parallel load never measured on the server | High | The load test completed 7 packs in a stream and stopped; the burst of 3 never ran. Memory with two or more packs in flight is unknown, and the repository’s notes disagree on whether an earlier three-pack measurement was parallel or sequential. | Capacity |
| R5 | No external uptime monitoring | Medium | Alerts are sent by the bot process itself. If the server or the bot is down, nothing alerts. | Monitoring & alerts |
| R6 | Telegram limits for pack creation unknown | Medium | No measurement above one pack at a time; pack creation has no flood-limit handling of its own. | Capacity |
| R7 | Delivery depends on the wording of a public Telegram page | Medium | “Pack exists” is read from the page title of t.me/addstickers/<name>. If Telegram changes it, every delivery stalls and fails at 15 minutes. |
ADR-010 |
| R8 | File clean-up runs only while the bot service runs | Medium | A stopped bot stops the 72-hour deletion. After an event the service must stay up, or files must be removed by hand. | ADR-005 |
| R9 | No money ceiling per event or per day | Medium | There is a cap per job and a daily quota counter, but nothing stops total spend except the provider balance. | Cost |
| R10 | Rate limiters and login lockouts are in memory | Low | A restart of the API resets them. | Security |
| R11 | The 15-minute delivery deadline is a constant | Medium | It covers queue time too, so a burst larger than about 14–28 jobs fails its tail. | Capacity |
| R13 | A wrong leads secret turns into “too many requests” | Medium | After five rejected calls from the landing server within ten minutes the endpoint answers 429, which the landing treats as throttling with no fallback. With mismatched secrets visitors see “too many requests” and the request reaches nobody. Rotate the secret in the right order — see Secrets. |
|
| R14 | A link that could not be sent is not re-sent | Low | If all three send attempts fail, the job still ends as done; the link is in the admin panel. |
|
| R15 | A failed reminder is not retried | Low | Deliberate: the reminder is marked as sent even when sending fails. | |
| R16 | The kiosk API has no authentication or rate limit | Medium | Anyone can create sessions (the rows are never pruned), and anyone with a Telegram account can agree in the bot and upload an arbitrary image through the API. The damage is bounded by one pack per account and the cost cap per job. | |
| R17 | Loopback trust on the shared server | Low | The API trusts forwarded headers from the local machine, so a local process of the neighbouring project could call it directly and spoof client addresses for the lockouts. | |
| R18 | The instance lock is per machine | Medium | It stops a second bot process on the same server, not a copy started on a laptop with the same token. That case shows up as a Telegram conflict error and jobs split between the two. | |
| R19 | The deploy scripts carry the pilot’s domain and server as defaults | Medium | The API domain is a literal in the web-server site file, the update script, one legacy check and a deploy test; the push script defaults to the pilot server. On a new installation always pass the server explicitly and change the domain in those files. | |
| R20 | An updated backup script does not reach the timer by itself | Low | The first-time setup installs copies of the backup and update scripts system-wide; routine updates refresh only unit files. | |
| R12 | Interactive API documentation is not disabled | Low | The framework’s automatic /docs, /redoc and /openapi.json are left on in the API application. They reveal endpoint shapes, not data. |
API reference |
Legal and privacy
Section titled “Legal and privacy”| # | Gap | Severity | Detail |
|---|---|---|---|
| L1 | Data localisation for citizens of Uzbekistan | High | The national personal-data law requires personal data of citizens processed with information technology to be stored on equipment physically located in the country and in a registered database. The server is with a foreign cloud provider and the cards live in Telegram. This applies to lead contact data and to guest data. The team decided to launch without waiting for a lawyer and recorded the risk. A masked-card switch (LEADS_CARD_PII=0) exists as a partial mitigation. Needs a lawyer’s answer. |
| L2 | Server region unconfirmed | Medium | The notes say the region “should be checked”. |
| L3 | Contact data is visible to the whole sales group | Medium | Including other products’ bots that can read group messages. /stickers_new prints contacts even when masked cards are switched on. |
| L4 | Lead data stays in backups for up to 14 copies after erasure | Low | By design; stated to the person on erasure. |
| L5 | Minors | Medium | No age check and no stated policy on who consents for a minor. |
| L6 | The consent text says “72 hours” as fixed text | Low | It does not follow PHOTO_TTL_HOURS. Harmless while the setting is 72 or lower, since the text is then an upper bound. |
| L7 | AI vendor’s handling of submitted photos | Medium | Governed by the vendor’s terms; not documented in the repositories. |
| L9 | Guest rows have no automatic retention | Medium | Photos expire, but the rows in users, sessions and jobs (Telegram id, profile name, pack link) stay until an admin resets the guest. Sessions that were never linked to a guest are never deleted. After a reset the Telegram id and old pack links remain in the audit log. |
| L10 | Automatic database copies are never removed | Medium | The copies made before a migration or a guest reset stay in the data folder; the 72-hour clean-up does not cover them, and they contain whatever the database held. |
| L11 | An appearance summary stays in logs longer than the photo | Medium | Per-job log lines record skin and hair colour, facial hair, age band, glasses and face shape; logs are kept longer than the 72-hour photo lifetime, and another line ties the job to a Telegram user id. |
| L12 | Anonymisation keeps two free-text fields | Low | The event date and guest count of a lead survive erasure. |
| L8 | Private notes inside the source repositories | High if the repositories are public | The repositories’ internal notes, some comments and a few tool docstrings contain a server address, Telegram identifiers, names, client names and commercial terms. This site does not copy them, but it links to those repositories. Clean them or keep the repositories private. |
Product
Section titled “Product”| # | Gap | Severity | Detail |
|---|---|---|---|
| P1 | Bot and kiosk are Russian only | High for non-Russian audiences | The landing page sells in three languages. The kiosk has a ?lang= hook with one language; the bot has none. |
| P2 | Station branding is hard-coded for one event | Medium | The brand layer, logo mark-up and a logo file name tied to a partner are committed code. Re-branding is a code change. See Multi-tenant. |
| P3 | Likeness versus consistency is unresolved | Medium | Three modes exist; which one should be the default is not decided, and classic and hybrid were validated on few faces. The mode on a server is whatever its .env says. See ADR-008. |
| P4 | Pose 10 often cuts the arm | Medium | The flat-cut detector catches it and regenerates, but the regenerated figure is often cut too, and then the original is kept. The pose or its prompt needs changing, with a live check. |
| P5 | Very light skin from an overexposed photo | Low after the fix | Skin shift is capped; outliers in tone are corrected less completely as the price. |
| P6 | Five of eleven catalogue entries are placeholders | Low | Hidden behind SHOW_PLACEHOLDER_TEMPLATES=0. Composite mode has a real body only for one outfit; the others reuse it. |
| P7 | A style reference image is still a TODO in the worker | Low | The file is optional and absent. |
| P8 | Pack limit is global, not per event | Medium | A returning guest at a second event on the same installation is refused. |
| P9 | No guest-facing waiting estimate under load | Low | The bot always says “a couple of minutes”. |
| P10 | Some settings cannot be set to zero | Low | DISPLAY_UTC_OFFSET_HOURS=0 and CONSISTENCY_SKIN_DE_MAX=0 silently fall back to the defaults. |
| P11 | One paid tool has no hard money cap | Medium | tools/consistency_run.py shows an estimate and asks for confirmation, but does not stop at a budget, although the project rule requires a hard stop. |
| P12 | tests/quick_test.py is not under the event guard |
Low | It reads its key from the operating-system environment only, not from .env. |
| P13 | Guest texts live in two places | Low | Most are in bot/texts.py; five are in the start and consent handlers. |
| P14 | Fallback requests are not in the database | Medium | By design, but they are invisible to statistics, reminders and export. |
Landing page
Section titled “Landing page”| # | Gap | Severity | Detail |
|---|---|---|---|
| W1 | Uzbek text not proofread by a native speaker | Medium | The Uzbek page is published and indexed, while the dictionary’s own header calls the proofread a blocker. |
| W2 | Mobile LCP about 3.5–3.6 s | Medium | Limited by about 172 KB (gzip) of first-screen JavaScript; fonts and HTML are already optimised. The specification recorded a mobile performance score of 87 against a target of 90; the repository’s rules call “90+” accepted. No script enforces a score, LCP or JavaScript size. |
| W3 | The audit script sends a real request | Medium | Its form check submits a valid request. Run against a server with leads or bot variables set, it reaches the real group. |
| W4 | Demo sticker sources are in no repository | Medium | The source images of the demo packs are git-ignored on both sides. A fresh clone cannot rebuild the showcase assets or the social preview collage. |
| W5 | Adding a seventh showcase person is not “one folder and one entry” | Low | Tests and scripts pin exactly six people. Replacing one of the six is simple. |
| W6 | Form rules exist twice | Low | Landing schema and server validator must change together. The server accepts any UUID version; the landing sends version 4. |
| W7 | Stale comments and notes | Low | See the table below. |
Line of products
Section titled “Line of products”| # | Gap | Severity | Detail |
|---|---|---|---|
| N1 | The second product’s landing still posts straight to Telegram | Medium | No storage, statuses or statistics, and its message does not name the product, so it cannot be told apart in a shared group. Its form fields do not match the leads server’s required fields. See the playbook. |
| N2 | The leads module is not reusable as is | Medium | The prefix and product name are literals in about two dozen places. |
Where the repositories’ notes disagree with the code
Section titled “Where the repositories’ notes disagree with the code”This site follows the code. Do not change code to match these notes; fix the notes.
Product repository
Section titled “Product repository”| Note says | Code does |
|---|---|
CLAUDE.md, “Status”: no bot, no server, no live run yet |
All three exist; the section is historical. |
CLAUDE.md: the database has three tables |
Nine tables, including cost accounting, audit log, settings and three leads tables. The same stale sentence is in the schema module’s docstring. |
CLAUDE.md: the API logs to the console only |
The API also writes logs/api.log. |
CLAUDE.md, rule 2: photo lifetime “48–72 hours” |
Any value from 1 to 72 hours is accepted; the default is 72. |
CLAUDE.md, rule 6: the AI draws only heads, code composes them on a body |
True for composite mode only. In figure mode — the configured production mode — the AI draws whole figures and code cuts and finishes them. |
CLAUDE.md: the admin handler is /stats only |
There are also /admin_stats, /pause and /resume. |
CLAUDE.md: body templates are placeholders |
Six outfits are ready; five entries are placeholders. |
deploy/README.md: production runs four worker slots |
The code default, the example environment file, the hand-over notes and the runbook say two. A comment in the configuration module also still says four. |
deploy/README.md and the example environment file: sheet aspect 3:2 |
The code default is 2:3; with the default 3x4 layout sheets are square and the setting is unused. |
| Deploy notes, the update script’s messages and the event runbook: a job interrupted by a restart starts again with the attempt counted | A graceful stop returns the job to the queue without spending an attempt, for the first two interruptions; only a hard kill costs one. |
| Hand-over notes: the log marker for waiting on an image slot | The code emits separate markers for image slots and cutting slots, under different names. |
| A docstring: “out of money” is raised when all providers with a key report it | It is raised when any provider reported it and none succeeded. |
| Deploy notes and hand-over notes: a second bot “with the same token” will not start | The lock is a file on the same machine only. |
| Example environment file: changing the IP salt resets repeat detection | Repeats are detected by Telegram username or phone; the salt affects the per-visitor rate limit. |
| Event runbook: four kinds of alert | The code sends eleven distinct alerts — see Monitoring & alerts. |
| Leads specification and the bot’s reply: erased data leaves backups within 14 days | True for the nightly backups only; automatic pre-migration and pre-reset copies and droplet snapshots are not rotated. |
| Hand-over notes, “Architecture”: two sheets generated one after another | The configured default is consistency mode with three parallel sheets. |
| Hand-over notes: figure mode has never run live | Later sections of the same file describe many live figure runs. |
| Event runbook: production currently runs the classic likeness mode | Not verifiable from the repository; the load test describes consistency mode. Check the start-up log line. |
| Load-test report: an earlier measurement ran three packs at once | The hand-over notes describe the same measurement as one pack at a time. |
| Example environment file: with placeholders hidden the guest sees one outfit | The guest sees six. |
Example environment file: the event guard also blocks tests/quick_test.py |
It does not. |
| Example environment file: the admin list gates the statistics commands | It also gates pause and resume. |
| A catalogue comment names the fallback switch after its log marker | The variable is FIGURE_FALLBACK_ENABLED. |
| A tool docstring says the next pack gets a numeric suffix | Packs are named pack<N>_…. |
| A tool docstring says a library is not in the requirements | It is pinned there. |
| A comment in the backup script: the backup holds only guest metadata | It copies the whole database, including lead contact data. |
| The product specification: a pack is created with one call plus one call per further sticker | All 12 stickers go in a single creation call. |
| Leads specification: the server action generates the request id on submit | The form generates it when it opens; the action only falls back to generating one. |
| Leads specification: response list | The endpoint also answers 413 for an oversized body. |
| Leads specification: schema | The repeat-requests table and one index were added later and are missing from the specification. |
| Leads specification: deploy to production awaits approval | Whether the module is live cannot be read from the repository. The landing code that calls it is merged. |
Landing repository
Section titled “Landing repository”| Note says | Code does |
|---|---|
| “Stack”: an animation library is the only motion dependency | No such dependency is installed; all motion is CSS and browser APIs. |
| The site shares nothing with the product repository | It posts requests to the product’s leads endpoint with a shared secret. |
| The domain is not chosen | The owner states the production address is pack.eventtalks.uz; it is configured only in the hosting settings. |
| The test suite checks both values of the Uzbek flag | It also asserts the flag is currently on, so switching it off fails the tests until that assertion is edited. |
| Two different audit subsets for the Uzbek page and for themes | Both options run the same two groups. |
| Device tilt is at most 5° | 5° turn plus 3.5° nod; the audit allows 6°. |
| The hero cycle is about 26 s (rules) or 28 s (a code comment) | The step durations sum to 25.8 s. |
| Durations come only from tokens | Several animations use literal durations. |
| A comment in the motion module: no springs on the page | The stylesheet defines and uses one spring easing. Some exports of that module are unused. |
| A stylesheet comment: one height animation on the page | There are two documented exceptions. |
An audit comment: sections use content-visibility |
They do not; the rules forbid it. |
| Sticker-gap fixes are listed for one demo pack | The script lists a different pack and different stickers. |
| README: the test command covers four areas | It runs six test files. |
| Earlier amendments: two packs, then three people in the showcase | The showcase has six demo people. |
Where to go next
Section titled “Where to go next”- Open decisions — the choices these gaps are waiting for.
- Roadmap — the order to close them.
- Scaling scenarios.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.