Nicchon.
Todos os projetos
SistemaEm produção· 2026

LabClock

Cronômetros regressivos multi-dispositivo com signup público gratuito. Multi-tenant, TV mode, audit log — rodando em shared hosting (PHP + MySQL) sem WebSocket.

Sobre

Pensado pro laboratório químico onde trabalhei como técnico: ensaios paralelos em salas diferentes, cada um com seu cronômetro, ninguém vendo o conjunto. Saí da química mas o problema ficou — então construí. Aplicável a qualquer contexto multi-timing: cozinha profissional, brewery, salões. Sete fases entregues em 18 dias: backend MVP, auth, edição inline + admin, salas/grupos M:N, TV mode, audit log + Service Worker, multi-tenant + landing pública. Signup gratuito aberto — qualquer um cria um lab e tem seu espaço isolado em 30 segundos.

Stack

  • PHP 8
  • MySQL
  • jQuery (self-hosted)
  • Service Workers
  • Notification API
  • Wake Lock API
  • Apache mod_rewrite
  • GitHub Actions (FTP)
  • Hostgator shared

Decisões

  • Polling 3s + cálculo determinístico client-side a 10fps em vez de WebSocket. WebSocket não funciona em shared hosting (Hostgator não permite processo daemon) — escolha forçada pela infra. Cliente recebe started_at_ms e calcula o display localmente, latência de 3s aceita pra ações manuais no contexto de laboratório.
  • Multi-tenant em MySQL puro: tenant_id em todas as 5 tabelas de dado (usuarios, cronometros, grupos, salas, audit) + FK CASCADE. Slugs continuam GLOBAIS (não há tenant na URL) — leitura pública de /c/{slug}/ funciona sem revelar o tenant, ações exigem login e validam ownership por tenant.
  • Email único global em vez de único por tenant. Custo: mesma pessoa em 2 labs precisa de 2 emails. Benefício: login form não precisa de 'qual tenant' — só email + senha e o sistema descobre. Simplifica UX drasticamente.
  • CSP estrita desde o dia 1: script-src 'self' sem CDN. jQuery, qrcodejs e tudo mais é self-hosted. Não tem inline onclick — só addEventListener. Decisão de segurança em sistema PHP shared host vale o pequeno custo de versionamento manual de libs.
  • Slug 8 chars sem caracteres confusos (0/O, 1/I/l): alfabeto abcdefghjkmnpqrstuvwxyz23456789. ~10^11 combinações, URLs digitáveis em celular sem ambiguidade. URL curtas pra compartilhar via WhatsApp.
  • PBKDF2 SHA-256 com 600k iterações pra senha hash. Sem bcrypt — PHP 8 + Hostgator + dependência mínima, hash_pbkdf2 nativo é suficiente e mais previsível em ambiente shared.
  • Audit log append-only com snapshot de usuario_email no momento da ação. Quando admin deleta um técnico, o log sobrevive — o email fica gravado mesmo se a row do user sumir. Filtro inclui tenant_id NULL pra mostrar tentativas anônimas (login.fail) do mesmo email no painel do admin.
  • Notificação no SO via Service Worker (Notification API) — quando timer cruza zero e aba está sem foco, o SO ainda mostra notificação. Web Push real (sem aba aberta) ficou fora de escopo: exige VAPID + lib + cron de servidor, complexidade alta pro ganho marginal em laboratório com TV/desktop sempre ligado.
  • Wake Lock API no TV mode — tela não apaga durante uso. QR code gerado client-side (qrcodejs self-hosted) aponta pra /g/{slug}/ pra parear celular sem digitar URL.
  • Single-tenant inicialmente, multi-tenant via migração idempotente (Fase 7): script PHP que CREATE TABLE IF NOT EXISTS tenants + INSERT default 'kharis' + ALTER ADD COLUMN tenant_id NULL + UPDATE backfill + MODIFY NOT NULL + ADD FK. Roda 1x via browser, deleta depois. Sem ferramenta de migration nativa — é shared host, é PHP puro.

Desafios técnicos

  • Sincronização entre N dispositivos sem WebSocket: precisei me apoiar em timestamp absoluto do servidor (started_at_ms) + offset calculado pelo cliente (server_time_ms vs Date.now). Cada cliente fica consistente sozinho mesmo com clocks levemente diferentes.
  • Migration multi-tenant em produção sem downtime: ADD COLUMN tenant_id NULL primeiro, UPDATE backfill com kharis (tenant default), só DEPOIS MODIFY NOT NULL + FK. Se eu pulasse essa ordem, o ALTER falharia em rows sem tenant.
  • Beep cruzando zero exatamente uma vez (não em loop): flag estado.beepEm[slug] = true setada na primeira passagem por <=0, reset só quando status volta pra PARADO. Sem isso, polling 3s + display 10fps tocaria beep 30x.
  • CSP bloqueava inline scripts e onclick herdados de mini-apps legados do app.nicchon.com — tive que refatorar pra addEventListener em tudo. Trade-off conhecido: CSP estrita melhora segurança e exige disciplina de código.
  • Roteamento amigável /c/{slug}/, /g/{slug}/, /tv/{slug}/ via mod_rewrite + .htaccess. Não tem framework router — é Apache puro mapeando pra cronometro.html?slug=X.
  • Self-service signup atomic: tenant + admin numa transação. Race condition de slug duplicado entre pre-check e INSERT: capturo PDOException 'Duplicate' e respondo 409 explicitamente. Rate limit 3 signups/IP/hora via consulta no próprio audit_log — sem Redis, sem lib externa.

Resultado

Sistema rodando em produção em app.nicchon.com/labclock/ com signup público gratuito. Stack PHP + MySQL + jQuery se justifica pelo deploy zero-config em shared hosting (Hostgator) — sem Docker, sem Node, sem Redis, sem dependências de runtime. CI/CD via GitHub Actions com FTP. Documentação retrospectiva (ADRs, runbooks, onboarding) e auditoria funcional pendentes pra v1.0.0.