Releases: Gonzalez8/checktime
Release list
v1.9.6 — Gemini defaults: flash-lite + thinkingBudget=0
Tuned against a real CheckJC captcha
Tested the LLM solver against the same composite image the human
flow sends via Telegram, with all four models we have in the
dropdown. Findings:
| Model | Result |
|---|---|
| gemini-2.5-flash-lite | ✅ 100% accurate, free tier, cheapest |
| gemini-2.5-flash | |
| gemini-2.5-pro | ❌ HTTP 429 — free tier quota is 0 |
| gemini-2.0-flash | ❌ HTTP 429 — legacy, existing customers only |
Changes:
- Default model is now
gemini-2.5-flash-lite(was 2.5-flash).
Cheapest of the four, works on the free tier without tuning. - Request body now always sends
thinkingConfig: {thinkingBudget: 0}. This makesgemini-2.5-flashanswer correctly (it's a
thinking model — without this it spends the output budget reasoning
and returns truncated text). Other models ignore the field
harmlessly. maxOutputTokensbumped from 32 to 64. 16 digits is ~10 tokens,
so 64 leaves comfortable headroom at no measurable extra cost.- Profile dropdown order updated to put the recommended option
first, with help text flagging which models need a paid plan. - The standalone debug script (
tests/debug_gemini_solver.py) is
updated to match the same defaults.
Image
ghcr.io/gonzalez8/checktime:1.9.6 / :1.9 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.9.6/' stack.env
docker compose pull app
docker compose up -d appAfter the deploy, new users who paste a Gemini API key without
touching the dropdown will use gemini-2.5-flash-lite automatically.
Existing users keep whatever model they previously selected.
Verify locally
python tests/debug_gemini_solver.py --api-key YOUR_KEY --image /path/to/composite.png
# Should print "SUCCESS: Gemini returned a well-formed 16-digit reply."v1.9.5 — Detect CheckJC account lock + roll back v1.9.4 retry
Why this exists
A real CheckTime user's CheckJC account got hit with a per-user
lockout banner:
¡Error!: No se permitirán nuevos intentos de acceso para el usuario
47779708z hasta dentro de 2 meses, 30 días, 23 horas, 17 minutos.
Contacte con su supervisor o administrador de la plataforma.
This is a hard server-side ban that no client-side retry can recover
from. The contributing factor was v1.9.4's double-submit retry — each
scheduler tick was burning 2 failed logins instead of 1, and over the
course of a day that crossed whatever threshold CheckJC enforces.
What this release does
-
Detect the per-user lock banner up-front. New
CheckJCAccountLockedexception and a parser that matches "No se
permitirán nuevos intentos…". The login flow checks for it
before filling the form, so a locked account never sends
another submit and can't extend its own ban. -
Revert v1.9.4's retry. A rejected submit now raises
immediately like in v1.9.3 and earlier. The 3-second pre-click
hydration wait on lite bodies stays (it's harmless and helps the
CSRF token finish loading). -
Better Telegram message when locked: ⛔ icon plus instructions
to contact the CheckJC admin and disable auto-checkin in CheckTime
while the ban lasts. -
Post-submit banner re-check. If the timeout fires, we re-read
the page and surfaceCheckJCAccountLockedorCheckJCIPBlocked
if the server set those, rather than the generic "rejected".
What you should do right now
- Contact your CheckJC supervisor / admin to unlock account
47779708z. Nothing on our side can undo this. - Disable auto-checkin for the affected user in CheckTime
profile while waiting. That stops the scheduler from making it
worse. - Upgrade to v1.9.5 so this can't happen again for anyone else.
Image
ghcr.io/gonzalez8/checktime:1.9.5 / :1.9 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.9.5/' stack.env
docker compose pull app
docker compose up -d appLog lines to expect
If the locked user is still on auto_checkin_enabled=True (you
forgot to disable), the next tick logs:
CheckJC account for 47779708z is locked. Remaining: 2 meses, 30 días, 23 horas...
The user must ask their CheckJC supervisor or admin to unlock the account.
and Telegram says:
⛔ Check in for Jose: tu cuenta de CheckJC está BLOQUEADA. ...
No further submits happen. Once unlocked, re-enable auto-checkin in
the profile and the next tick should fichar normally.
v1.9.4 — Hydration wait + retry on login rejection
Login was getting rejected with lite-variant bodies
After the captcha-relay work, production hit a new failure mode:
Form usable despite small body (7183 bytes) ...
Login button clicked
... 15s later ...
CheckJCLoginRejected: still at /login after submit
Diagnosis: when CheckJC serves the lite variant of /login the visible
username and password inputs become interactable before Stencil has
finished hydrating the form's hidden CSRF token. We click the
submit button right away → the POST goes with an empty token → the
server silently bounces us back to /login → we surface
CheckJCLoginRejected.
This is the same <input type="hidden" name="token"> documented in
docs/migrations/checkjc-v7.4.md; it's just that on lite bodies it
isn't there yet when our code reads the DOM.
Fix
Two layers in CheckJCClient.login():
-
Extra wait on lite bodies before submit. If
body_size < _LITE_BODY_THRESHOLD(~20KB), wait 3s after locating the form so
Stencil can populate the hidden token. Normal-sized bodies skip
this and behave as before. -
Retry once on login rejection. If we still end up on /login
(no IP-block banner), reload the form and try a second time with
a 6s warm-up. Only after the second rejection do we raise
CheckJCLoginRejected. IP blocks still short-circuit immediately.
Image
ghcr.io/gonzalez8/checktime:1.9.4 / :1.9 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.9.4/' stack.env
docker compose pull app
docker compose up -d appLog lines to look for
Best case:
Form usable despite small body (7183 bytes) ...
Body is small (7183 bytes); waiting 3000ms extra for Stencil to hydrate ...
Login button clicked for ... (submit attempt 1/2)
Login successful for ... landed at /portal/employee/verification
Retry path:
Login submit rejected for ... on attempt 1, reloading and retrying
Body is small (...); waiting 6000ms extra ...
Login button clicked for ... (submit attempt 2/2)
Login successful ...
If you still see CheckJCLoginRejected after 2 attempts consistently,
the credentials are likely wrong or CheckJC has put a hard block on
the account — log in manually in a browser to confirm.
v1.9.3 — Real hotfix: profile 500 was a template bug
The real cause of the /auth/profile 500
v1.9.1 and v1.9.2 fixed the migration plumbing, but the 500 the user
kept seeing was never about the database. It was this in
profile.html:
{{ telegram_form.submit(class="btn btn-primary", name="telegram_submit") }}WTForms always injects the field's own name into html_params when
rendering a widget. Passing name= as an extra kwarg makes
html_params receive name twice:
TypeError: html_params() got multiple values for keyword argument 'name'
I introduced this in v1.8.1's refactor and the same mistake was
copied into the new Google API form in v1.9.0. The redundant
name="X_submit" was never doing anything anyway: the existing
<input type="hidden" name="X_submit" value="1"> is what
discriminates the form on submit.
Fix: drop the bad kwarg from both forms.
Migration hardening from v1.9.1/v1.9.2 stays
Even though it wasn't the cause, the bulletproof per-statement
transactions and information_schema-guarded column adds are real
improvements and stay in.
Image
ghcr.io/gonzalez8/checktime:1.9.3 / :1.9 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.9.3/' stack.env
docker compose pull app
docker compose up -d appAfter the restart, /auth/profile should load again. The new "API Key
de Google" tab will show the Gemini model dropdown.
v1.9.1 — Hotfix profile 500 + Gemini model dropdown
Two fixes
1. Profile page returned 500 after v1.9.0 deploy
_apply_lightweight_migrations ran every ALTER TABLE inside a single
db.engine.begin() transaction. Postgres aborts a whole transaction
on the first error — and from v1.8.1 onwards an earlier migration
(ALTER TABLE pending_captcha ALTER COLUMN response TYPE VARCHAR(32))
could fail on fresh databases. The aborted transaction silently
dropped every later statement, so v1.9.0's ADD COLUMN google_api_key never ran. Once the User model expected a column the
DB didn't have, any SELECT from /auth/profile crashed with a
PostgreSQL error and Flask returned 500.
Fix: each ALTER TABLE now runs in its own db.engine.begin() block.
A single failing migration no longer poisons the rest.
2. Configurable Gemini model
- Default model is now
gemini-2.5-flash(Google moved
gemini-2.0-flashto legacy-only in March 2026). - The profile page gains a model dropdown under the API key
field. Options:gemini-2.5-flash,gemini-2.5-pro,
gemini-2.5-flash-lite,gemini-2.0-flash. New nullable column
user.gemini_modelstores the per-user choice. LLMVisionSolverreads the user's preferred model at call time
and falls back toDEFAULT_MODELif not set.
Image
ghcr.io/gonzalez8/checktime:1.9.1 / :1.9 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.9.1/' stack.env
docker compose pull app
docker compose up -d appMigration is auto. After the restart, the profile page should load
again. Users who already had v1.9.0 with the missing google_api_key
column will see it created on first boot of v1.9.1.
Verifying the fix
docker compose exec db psql -U $POSTGRES_USER $POSTGRES_DB -c \
"\d \"user\"" | grep -E "google_api_key|gemini_model"Should list both google_api_key (varchar 512) and gemini_model
(varchar 64). If you saw the 500 before, both columns will appear
the first time you boot v1.9.1.
v1.9.0 — Per-user Gemini API key (Telegram fallback intact)
Add your own Gemini key, get hands-free fichaje
CheckTime now reads each user's optional Google Gemini API key from
the profile page and uses it to solve the CheckJC verification
captcha automatically. The Telegram human flow that works today is
unchanged — it stays as the safety net for users without a key and
for any LLM failure.
How it works
HybridCaptchaSolver is what the scheduler uses:
- Has the user a
google_api_key? → callLLMVisionSolver(Gemini). - Did Gemini return a usable 6-letter sequence? → done.
- Otherwise (no key, or LLM error / timeout / bad reply) →
TelegramHumanSolvertakes over and the user gets the same image
on Telegram they got in v1.8.x.
Same CaptchaSolver.solve() contract for both implementations, same
composite image, same parsing helpers. The change is purely additive.
Profile setup (one-time, per user)
- Open https://aistudio.google.com/app/apikey, sign in, click
Create API key. - In CheckTime go to Profile → API Key de Google and paste it.
The key is stored encrypted at rest (same crypto as the CheckJC
password). Clear it any time with the Remove API key button.
Migration
- New column
user.google_api_key VARCHAR(512)added automatically
on startup (ADD COLUMN IF NOT EXISTS). - No new env vars.
- No changes to existing CheckJC / Telegram flows.
Image
ghcr.io/gonzalez8/checktime:1.9.0 / :1.9 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.9.0/' stack.env
docker compose pull app
docker compose up -d appLogs you'll see when it works
Login successful for ... landed at .../portal/employee/verification
LLM solved captcha for user Jose on attempt 1 via gemini-2.0-flash
Submitting captcha for Jose (attempt 1): letters [A, P, ...]
Captcha cleared for Jose on attempt 1, landed at .../portal/employee
Check in completed successfully for user Jose.
Logs you'll see when it falls back
Login successful for ... landed at .../portal/employee/verification
LLM solver failed for user Jose; falling back to Telegram human
Telegram captcha relay started for user Jose (row=NN, attempt=1, ttl=300s)
... [user replies on Telegram] ...
Captcha reply recorded for user Jose (row=NN): 4036572918198142
Full design rationale in docs/migrations/captcha-relay.md.
v1.8.2 — Hotfix app context + Telegram 400
Hotfix on top of v1.8.1
Production logs revealed two bugs after deploying v1.8.1.
1. RuntimeError: Working outside of application context
TelegramHumanSolver.solve() calls PendingCaptcha.create() which
hits db.session.add(). Flask-SQLAlchemy needs an active app context
for that to resolve. The scheduler's perform_check_for_user only
opened the context inside helpers like is_working_day (which closes
when the helper returns), so by the time CheckJCClient.login() ran,
no context was active.
Wrapped the entire try/except block (including the with CheckJCClient(...) lifetime and the post-fichaje Telegram
notification) in a single with app.app_context(). The captcha solver
and its DB polling now run inside that context.
2. Telegram 400 Bad Request on error notifications
The error notification message produced by _format_error_for_telegram
contained ".com" and other punctuation that Telegram's Markdown parser
rejects with 400. Switched the error notification path to
parse_mode=None (plain text). Also made TelegramClient.send_message
tolerate parse_mode=None by dropping the key from the payload.
Image
ghcr.io/gonzalez8/checktime:1.8.2 / :1.8 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.8.2/' stack.env
docker compose pull app
docker compose up -d appNo schema changes, no new env vars.
v1.8.1 — Drop Tesseract, single 16-digit reply
Fix v1.8.0's keypad OCR failure
v1.8.0 used Tesseract to read the 10 clean keypad button PNGs and
build the letter→digit mapping automatically, asking the user for
just the 6 captcha digits. Production results: 0/10 buttons read
successfully. Local reproduction confirmed Tesseract systemically
confuses 1 with 7 on these images (the v7.4 decorative underline
fools it), and silently returns empty on the lighter-colored ones.
No amount of preprocessing fixed it reliably.
New approach
The solver now hands the user everything they need to read in a
single composed image:
- Distorted captcha on top.
- 10 keypad button PNGs in a strip below, each labelled
1–10.
The user replies with 16 digits in one go: the 10 keypad positions
(left to right) followed by the 6 captcha digits. Example reply if
the keypad reads 8 5 2 6 7 9 3 0 1 4 and the captcha is 198142:
8526793014198142
The solver maps captcha digits → keypad letters using the order the
user provided and returns the 6-letter click sequence to
CheckJCClient. From there it's the same as before: re-read DOM, click
each letter's current position, submit.
Why this is better than v1.8.0's OCR
- 100% reliable for the cases v1.8.0 broke.
- Zero new dependencies (Tesseract gone, image is smaller).
- Same
CaptchaSolver.solve()contract, so anLLMVisionSolver
plugged in later reads the same 16 digits from the same composite
image we already build for the human.
Trade-off: the user types 10 extra digits per fichaje (still 30
seconds total). The composite image's 1..10 labels make it obvious.
Files
- Dropped
keypad_reader.py,pytesseract,tesseract-ocr. - Kept
Pillowfor the image composition. - Bot listener now expects 16-digit replies.
pending_captcha.responsewidened toVARCHAR(32)(auto-migration
on startup).
Image
ghcr.io/gonzalez8/checktime:1.8.1 / :1.8 / :latest
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.8.1/' stack.env
docker compose pull app
docker compose up -d appSchema migration is automatic. No new env vars.
v1.8.0 — Captcha-relay via Telegram (LLM-ready)
CheckJC added a captcha. We bypass it.
Between v1.7.x deploys and v1.8.0, CheckJC introduced a new gate after
login: /portal/employee/verification, with a 6-digit distorted
captcha and an on-screen keypad that reshuffles after every click.
None of the previous code paths could pass it, so 100% of automatic
fichajes were broken.
The key discovery
Each keypad button carries a stable per-session data-value letter
(e.g. N, L, X...). The letter ↔ digit mapping is fixed for the
entire session. Only the physical positions of the buttons reshuffle
after each click.
Validated end-to-end twice against the real CheckJC: read the
keypad once at the start, OCR the 10 clean digit PNGs (Tesseract with
psm=10), and then drive every click by letter — re-reading the DOM
positions between clicks since the buttons move, but never the
letter↔digit map.
For the distorted captcha itself we go through a human-in-the-loop:
the scheduler captures the image, ships it via sendPhoto to the
user's Telegram chat, and waits for the 6-digit reply.
What ships
CaptchaSolverinterface plus two implementations:TelegramHumanSolver— production today.LLMVisionSolver— placeholder for v1.9+. Plugging in an LLM with
vision is a one-line swap inservice.py, nothing else changes.
PendingCaptchaSQLAlchemy model (newpending_captchatable,
created automatically bydb.create_all, no manual migration).keypad_reader.py— Tesseract OCR for the keypad's 10 clean digit
PNGs (psm=10, digit whitelist).CheckJCClient._solve_verification— orchestrates: capture
captcha → capture keypad → OCR mapping → solver → click loop with
per-click DOM re-read → submit → verify landing on /portal/employee.
One retry on wrong reply before giving up.- Bot listener: intercepts 6-digit replies before normal command
handling and writes them back to the pending row; periodic sweeper
expires stale rows after their TTL. - New
CheckJCCaptchaFailedtyped error with its own Telegram
formatting (🧩) so users see something actionable.
What you'll see in production
09:00:00 Scheduler picks up the fichaje
09:00:05 Login OK → lands on /verification
09:00:06 Bot sends captcha image to user's Telegram with caption
09:00:30 User replies "578599"
09:00:31 Bot writes ANSWERED to the pending row
09:00:32 Scheduler exits the polling, translates digits → letters,
clicks 6 letters (re-reading DOM each click), submits
09:00:35 Lands on /portal/employee, clicks btn-check
09:00:36 Telegram: "🟢 Check in completed successfully"
Captcha-relay TTL is 5 minutes. If the user is slow, we send
⌛ Tiempo agotado and skip the fichaje — they need to do it manually
in checkjc.com.
Image
ghcr.io/gonzalez8/checktime:1.8.0 / :1.8 / :latest
Image is slightly larger (~10 MB) because it now ships
tesseract-ocr. Python deps gain pytesseract and Pillow.
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.8.0/' stack.env
docker compose pull app
docker compose up -d appNo new mandatory env vars. Database migration is automatic on first
boot (new table only).
Design rationale and the failure-mode notes for CheckJCCaptchaFailed
live in docs/migrations/captcha-relay.md.
v1.7.4 — Try login form before deciding the body is lite
Fix: avoid unnecessary retries on small bodies that actually work
v1.7.3 introduced lite-variant retries based on body size: any response
under 20 KB was assumed to be the anti-bot variant where the form is
unusable, so login() went straight to sleep+retry without trying.
Production showed this was too aggressive. On 2026-05-18 18:47–18:51,
Jose's check-in went through this sequence:
| Attempt | Time | Body | Tried to find form? | Result |
|---|---|---|---|---|
| 1 | 18:47 | 7778 B | skipped | 120 s sleep |
| 2 | 18:49 | 7098 B | skipped | 120 s sleep |
| 3 | 18:51 | 7608 B | tried (last attempt fallthrough) | found, login OK |
The third attempt's body was the same size as the previous two but the
form WAS usable. Most likely cause: the same BrowserContext keeps
cookies and asset cache across retries, so once Stencil's JS bundle is
cached the components hydrate even when CheckJC keeps returning the
stripped HTML.
Change
login() now always calls _find_login_elements right after the
goto, regardless of body size. Only enters the sleep+retry branch if
the search actually fails AND the body was under 20 KB. With Jose's
18:47 scenario this would have succeeded on attempt 1 instead of
waiting 4 minutes.
Body normal + form not found still propagates immediately — that
signals a real DOM change, which isn't fixed by waiting.
Image
ghcr.io/gonzalez8/checktime:1.7.4 / :1.7 / :latest
No schema changes. Same env vars as v1.7.3.
Upgrade
sed -i 's/^CHECK_TIME_VERSION=.*/CHECK_TIME_VERSION=1.7.4/' stack.env
docker compose pull app
docker compose up -d app