El 96% de las entradas caian en solo dos minutos (08:58 / 08:59). El historial
real del usuario tiene 4 valores repartidos, o sea que el bot resultaba MAS
regular que una persona.
Tres cosas estaban mal:
- La deriva de la costumbre se aplicaba SUMANDO segundos. Cuando salia negativa
hundia el valor contra el suelo y amontonaba medio año en el mismo minuto,
justo lo contrario de lo que pretendia. Ahora va en escala logaritmica:
multiplica el centro en vez de sumarse. Solo con eso la moda baja del 50% al
39%, sin coste ninguno.
- La distribucion diaria era uniforme. Una lognormal da mas minutos distintos
por menos presencia extra, porque la mayoria de los dias caen baratos cerca de
la hora y solo unos pocos se van lejos. Ademas es la forma real de la llegada
de alguien que intenta ser puntual.
- El tope recortaba con min(), asi que un 3% de los dias caia exactamente en el
limite y se veia como un muro en el histograma. Ahora resortea.
El compromiso de fondo no tiene truco: solo se puede variar hacia atras, y
alejarse cuesta presencia. Perfiles medidos sobre los 226 dias laborables del
año, los tres con saldo 0:
discreto 2,8 min/dia entrada 2 minutos, moda 75%
natural 4,2 min/dia entrada 5 minutos, moda 51% <- por defecto
camuflado 5,5 min/dia entrada 4 minutos, moda 43%
Ademas, /proximos mostraba una banda estimada que ocultaba la cola. Ahora se
planifican 21 dias por adelantado y se muestran las horas exactas, con lo que
la prevision es real y se puede decidir sobre ella. El plan guardado se
revalida contra el calendario de Timenet y se regenera si cambia el tipo de
jornada de un dia. Google Calendar se consulta con una sola autenticacion para
todo el horizonte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Muestra que dias se fichara y cuales no (festivo, vacaciones, override o
ausencia de Google Calendar), con el saldo acumulado al final.
Para los dias futuros no se inventa una hora exacta: el instante se sortea esa
madrugada y la costumbre deriva de un dia al siguiente, asi que se muestra el
RANGO en que caera cada fichaje, que si es exacto. Para hoy se muestra el plan
real con su estado.
De paso, nombres de dia en castellano en /hoy y /proximos: dependian del locale
del sistema y salian en ingles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pasa de un cron ciego a un servicio en segundo plano. El killswitch lo exige:
para poder cancelar un fichaje pendiente desde el movil tiene que haber un
proceso escuchando durante la espera.
- daemon: bucle propio que duerme exactamente hasta el siguiente evento, con
precision de decimas. No usa JobQueue de PTB porque APScheduler 3.6 importa
pkg_resources, que ya no existe en Python 3.13; ademas asi no se añade una
dependencia. Recupera fichajes perdidos si el equipo estuvo apagado.
- bot: /hoy /saldo /plan /fichar /desfichar /off /on /pausa /reanudar /log.
Los comandos se registran con setMyCommands para que salgan en el menu del
chat. Solo atiende al chat_id configurado.
- killswitch: aviso 12 min antes con teclado inline (cancelar / +15 min /
fichar ya).
- Google Calendar queda solo para ausencias personales; festivos y vacaciones
salen del calendario de Timenet.
- deploy/autoficher.service: unidad de usuario endurecida (ProtectSystem
strict, escritura solo en el propio directorio), Restart=always.
- requirements: python-telegram-bot >= 22. La 20.5 tiene un bug de __slots__
en Updater que rompe el polling en Python 3.13.
- .gitignore: data/, *.db, .env, __pycache__ en cualquier nivel.
tools/test_daemon.py cubre con reloj simulado y sin red: plan, aviso previo,
fichaje a su hora, killswitch, retraso de 15 min, pausa, recuperacion tras
apagon, fallo de red y dia no laborable. 19 comprobaciones.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Timenet recorta el tiempo a la ventana teorica del horario:
trabajado = min(salida, fin_teorico) - max(entrada, inicio_teorico)
Verificado sobre 6 meses (104 dias laborables, 376 fichajes). La API guarda dos
horas por fichaje: HR (la que fichas) y H (la que cuenta). De 376 fichajes, 136
llevaban ajuste: todos salidas, todos recortados hacia atras. Ninguna entrada
ajustada jamas. Resultado: una entrada tardia se debita al minuto y una salida
tardia no acredita nada, asi que el jitter aleatorio positivo que habia perdia
unos 3,2 min/dia (-776 min acumulados, casi 13 horas).
Y acabar el mes en positivo es imposible: 0 dias con saldo positivo en 108, y
timeExtraHours=false en todos los tipos de jornada. El objetivo es 0 clavado.
- timenet_api: cliente limpio. Documenta que Tn-D y dStr son obligatorias (sin
ellas el servidor responde 403 hasta en endpoints publicos).
- schedule: calendario laboral desde cp/calendar. Festivos, vacaciones y tramos
horarios los da Timenet, no hace falta adivinarlos.
- planner + jitter: entradas siempre ANTES de la hora, salidas siempre DESPUES,
con costumbre que deriva lenta y cola larga hacia el lado seguro. Invariantes
que fallan en duro si una entrada se planifica tarde.
- jitter_geo: las coordenadas del historico son identicas al sexto decimal en
los 376 fichajes; ahora se desplazan dentro de un disco.
- store: SQLite con plan diario, overrides, auditoria y estado del drift.
- settings: config sin el acoplamiento a argparse del config_parser original.
tools/dryrun.py simula el año entero: saldo +0 en 226 dias laborables, con
~3,5 min/dia de presencia extra.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- google_calendar: RefreshError se referenciaba como
google.oauth2.credentials.exceptions.RefreshError, ruta que no existe. Al
fallar un refresh de token saltaba un AttributeError que ocultaba el error
real y rompia el re-auth. Ahora se importa de google.auth.exceptions.
- google_calendar: getEvent() comprobaba si habia un evento activo en ese
instante. El daemon planifica de madrugada, asi que una ausencia de 09:00 a
18:00 no se detectaba y se fichaba igual. Ahora acepta un dia y comprueba
solapamiento. Sin argumento mantiene el comportamiento anterior.
- google_calendar: datetime.utcnow() esta deprecado en Python 3.12+.
- timenet_manager: "args.basedtime and not args.type" ignoraba -t 0 (Entrada)
al combinarlo con -bt, porque not 0 es True. Ahora es "args.type is None".
- timenet_manager: updateVersion() asignaba el objeto Response al header Tn-V
en vez de su texto.
- timenet_manager: geo.accuracy se parseaba y se podia sobrescribir por CLI
pero nunca se enviaba en el payload de cp/checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Agregado logging estructurado en todos los módulos
- Implementados try-catches en todos los métodos críticos
- Errores ahora se envían automáticamente por Telegram
- Mejorado manejo de excepciones en requests HTTP
- Agregado try-catch global en main.py para errores no manejados
- Logging detallado en timenet_manager, telegram_bot, google_calendar y config_parser