Carbon Exchange — Manual
Plataforma de monitoramento ambiental para vinícolas, agro e brigadas de incêndio. Recebe dados de sensores LoRaWAN, classifica risco, alerta brigadistas via WhatsApp e expõe dashboard com mapa e previsão meteorológica.
1. Overview
Carbon Exchange é uma stack de três componentes:
- Webhook/API (
POST /carbon) — recebe leituras de sensores em JSON, persiste em PostgreSQL e arquivo JSONL, calcula risco e dispara mensagens WhatsApp. - WhatsApp instance — cliente Baileys que envia alertas formatados para o grupo administrativo (e, na Fase 2, para grupo do cliente).
- Dashboard — interface web com KPIs, mapa de devices, gráficos históricos e previsão 24 h.
2. Arquitetura
3. Fluxo de dados (request lifecycle)
- Sensor envia POST com JSON para
https://api.danhausch.cloud/carbon. - nginx encaminha para
127.0.0.1:5000(gunicorn → Flaskfunnel_backend.py). - Handler
carbon_webhook():- Persiste body completo em
carbon_webhook_events.jsonl. - Extrai leitura via
_extract_sensor_reading(). - Calcula risco via
_compute_environment_risk(). - Encaminha mensagem formatada para WhatsApp via
_forward_carbon_webhook_to_whatsapp(). - Salva em PostgreSQL (
_carbon_db_save_reading): faz upsert emcarbon.clients+carbon.devices, insere emcarbon.sensor_readings. - Se CO₂ > threshold, dispara alerta de áudio (
_dispatch_carbon_audio_alert).
- Persiste body completo em
- Resposta JSON síncrona em < 2 s (depende do RTT do Open-Meteo se for chamado).
4. Casos de uso
UC-01 · Brigada recebe alerta de CO₂
Sensor detecta CO₂ acima de 800 ppm. POST /carbon recebe payload, classifica risco como vermelho e dispara mensagem WhatsApp para grupo admin com nível de risco, leitura completa, link para mapa e previsão de chuva/vento das próximas 24 h. Brigadista decide ir ao local.
UC-02 · Sensor é movido
Payload chega com movimento: 1. Mensagem WhatsApp recebe linha destacada "Sensor movido do local original!". Dashboard mostra badge laranja no card "Movimento". Operador investiga.
UC-03 · Operador consulta histórico
Acessa /carbon/dashboard, filtra por cliente Vinícola e device mile_07, ajusta janela para 168 h (7 dias). Vê 4 séries temporais (umidade, temp, pressão, CO₂) + última leitura + posição no mapa.
UC-04 · Admin provisiona novo cliente Fase 2
Admin loga com JWT, abre /carbon/admin, cria tenant Fazenda Boa Vista com seu JID de grupo WhatsApp. Adiciona 5 devices. A partir do próximo POST, mensagens vão para o grupo do cliente (e mantém cópia no admin).
UC-05 · Cliente vê apenas seu ambiente Fase 2
Usuário client_admin da Vinícola loga e o dashboard filtra automaticamente todos os queries por seu tenant_id. Não vê devices de outros clientes.
5. Telas
Dashboard
/carbon/dashboard4 KPIs primários (umidade, temp, pressão, CO₂); 4 secundários (cliente, device, bateria, badge de movimento); caixa de risco com cor; mapa Leaflet com markers; painel de previsão 24 h; 4 gráficos históricos (Chart.js).
Refresh automático: dados a cada 5 s; catálogo de clientes/devices a cada 60 s.
WhatsApp Pairing
/carbon/whatsappQR code gerado pelo Baileys; botões "Atualizar QR" / "Reconectar"; envio de mensagem teste para qualquer telefone.
API Docs
/carbon/docsPágina HTML simples com exemplos de payload e endpoints.
Test UI
/carbon/testForm para POST manual de payloads ao webhook.
Login
/carbon/loginEmail + senha → JWT em localStorage. Redireciona para dashboard ou admin conforme role.
Admin
/carbon/adminTabs Tenants / Usuários / Auditoria. CRUD de cliente (incl. JID do grupo WhatsApp via dropdown), criação de usuários, log de alertas com status de replay.
6. Features
- Ingestão idempotente: payload bruto sempre persistido em JSONL antes de qualquer transformação.
- Multi-formato de chave: extrator aceita
temp/temperature/temperatura,pa/pressure/pressao,umid/humidity/umidade,co2/co2_ppmetc. - Risco ambiental: classifica leitura em escala (azul / verde / amarelo / laranja / vermelho) considerando CO₂, temperatura, umidade e pressão.
- WhatsApp formatado: filtra
fCnt,fPort,token,devnameda mensagem; inclui link Google Maps a partir de lat/lon; destaque paramovimento=1. - Mapa de devices: Leaflet com OpenStreetMap (sem API key); fitBounds automático.
- Previsão 24 h: Open-Meteo (free, no key) com cache em memória de 10 min por coordenada arredondada a 4 casas decimais.
- Alerta crítico CO₂: se
co2 > CARBON_CO2_ALERT_THRESHOLD, dispara mensagem "🚨 SIRENE CARBON" para lista de telefones emCARBON_AUDIO_ALERT_PHONES. - Catálogo dinâmico:
/carbon/catalogretorna clientes e devices a partir do PostgreSQL para popular filtros e mapa.
7. API / Endpoints
Base URL pública: https://api.danhausch.cloud · Base local: http://127.0.0.1:5000
| Método | Path | Descrição |
|---|---|---|
| POST | /carbon | Webhook principal: ingestão de leituras, dispara WA, salva em PG. |
| GET | /carbon | Health/info: tamanho do JSONL e path do arquivo. |
| GET | /carbon/latest?limit=20 | Últimos eventos brutos do JSONL (1–200). |
| GET | /carbon/dashboard | Serve dashboard/index.html. |
| GET | /carbon/dashboard/data | JSON com última leitura + risco. Filtros: client_id, device_id. |
| GET | /carbon/readings | Histórico de leituras. Params: limit (1–2000), hours (1–720), client_id, device_id. |
| GET | /carbon/catalog | Lista de clients e devices (incl. lat/lon). |
| GET | /carbon/forecast?lat=&lon= | Previsão 24 h (chuva, vento, rajada) via Open-Meteo, cache 10 min. |
| GET | /carbon/whatsapp | UI de pareamento Baileys. |
| GET | /carbon/whatsapp/status | Estado da sessão (proxy → :8790/wa/status). |
| GET | /carbon/whatsapp/pairing | QR code data URL (proxy → :8790/wa/pairing). |
| POST | /carbon/whatsapp/reconnect | Força reconexão Baileys. |
| POST | /carbon/whatsapp/send-test | Body: {phone, text}. Envia mensagem teste. |
| GET | /carbon/test | UI de teste do webhook. |
| GET | /carbon/docs | Documentação HTML simples. |
| GET | /carbon/manual | Este manual. |
| POST | /carbon/auth/login | Email + senha → access + refresh tokens. |
| POST | /carbon/auth/refresh | {refresh_token} → novo access. |
| POST | /carbon/auth/logout | Revoga refresh. |
| GET | /carbon/auth/me | User + memberships (lê do JWT). |
| GET | /carbon/login | UI de login. |
| GET | /carbon/admin | UI admin (tabs tenants/users/audit). |
| GET | /carbon/admin/tenants | Lista tenants. require_admin. |
| POST | /carbon/admin/tenants | Cria tenant {name, slug?, whatsapp_jid?}. |
| POST | /carbon/admin/tenants/<id> (PUT) | Edita nome, JID, is_active. admin OU client_admin do próprio tenant. |
| GET | /carbon/admin/users | Lista usuários + memberships. |
| POST | /carbon/admin/users | Cria {email, password, name?, is_admin?}. |
| POST | /carbon/admin/tenants/<id>/members | Adiciona/atualiza membership. {email, role} (viewer/client_admin). |
| GET | /carbon/admin/wa-groups | Lista grupos do Baileys (@g.us) para popular dropdown. |
| GET | /carbon/admin/audit | Audit log alert_events; tenant-scoped para client_admin. |
Exemplo de chamada
curl -X POST "https://api.danhausch.cloud/carbon" \
-H "Content-Type: application/json" \
-d '{
"movimento": 0,
"temp": 30.1,
"umid": 47,
"co2": 305,
"pa": 949.5,
"bat": 3.48,
"device_id": "24e124126f099489",
"device_name": "mile_07",
"cliente": "Vinícola",
"latitude": -22.548545,
"longitude": -48.819028,
"fCnt": 144,
"fPort": 85,
"token": "pgpanfd115jpr7yu0dv5",
"devname": "mile-07"
}'
Resposta típica
{
"ok": true,
"saved": true,
"forwarded": true,
"forward_status": "sent",
"reading": {
"temperature": 30.1, "humidity": 47.0, "pressure": 949.5, "co2": 305.0,
"battery": 3.48, "movimento": 0,
"latitude": -22.548545, "longitude": -48.819028
},
"db_saved": true,
"db_status": "db_saved",
"audio_alert": false,
"audio_alert_status": "co2_normal"
}
8. Payload do sensor
| Campo | Tipo | Significado | Uso |
|---|---|---|---|
movimento | 0/1 | 1 = sensor movido do local original | Badge no dashboard, alerta no WA |
temp | float | Temperatura °C (1 casa decimal) | KPI, gráfico, risco |
umid | int | Umidade relativa % | KPI, gráfico, risco |
co2 | int | CO₂ em ppm | KPI, gráfico, risco crítico, alerta áudio |
pa | float | Pressão atmosférica em hPa | KPI, gráfico, risco |
bat | float | Tensão da bateria (V) | KPI |
device_id | string | ID único do device | Chave em carbon.devices |
device_name | string | Nome amigável do cliente (ex.: "caixa d'água") | Exibido no WA + dashboard |
cliente | string | Nome do cliente | Tenant (Fase 1: vira client_id) |
latitude, longitude | float | Coordenadas WGS-84 | Mapa, link Google Maps no WA, previsão |
fCnt, fPort, token, devname | — | Metadados internos LoRa/plataforma | Ignorados na mensagem WA; persistidos só em raw_payload |
9. Risco ambiental
Função _compute_environment_risk(reading) em funnel_backend.py:1129. Decisão atual:
| Condição | Nível | Cor |
|---|---|---|
CO₂ > CARBON_CO2_ALERT_THRESHOLD | alerta crítico | vermelho |
| Umidade 80–100% | risco mínimo | azul |
| Umidade 60–79% | risco baixo | verde |
| Umidade 30–59% & temp > 30 °C | risco médio | amarelo |
| Umidade < 30% & temp > 35 °C | risco alto | laranja |
| Demais combinações | indeterminado | cinza |
10. WhatsApp routing
Backend Baileys
Node.js rodando em 127.0.0.1:8790. Service systemd openclaw-wa-backend.service. Workdir: /root/.openclaw/workspace/apps/openclaw-cockpit/wa-backend.
Endpoints internos do Baileys
GET /wa/statusGET /wa/pairingGET /wa/chatsPOST /wa/send— body{phone, text}POST /wa/reconnect
Forward webhook → grupo
Função _forward_carbon_webhook_to_whatsapp(payload, reading, risk) formata mensagem PT-BR com _format_carbon_whatsapp_message() e envia via _wa_send_to(target, text).
Target controlado por env var CARBON_WEBHOOK_FORWARD_PHONE. Se terminar em @g.us ou @s.whatsapp.net, é tratado como JID e enviado direto; caso contrário, normalize_phone() converte para digits-only.
Atualmente: [email protected] = grupo Carbon Software.
Formato da mensagem
🌿 Carbon Exchange — Vinícola / mile_07
🌡️ Temp: 30.1 °C 💧 Umid: 47.0 %
🫁 CO₂: 305 ppm 🌐 Pressão: 949.5 hPa
🔋 Bateria: 3.48 V
📍 -22.548545, -48.819028 https://maps.google.com/?q=-22.548545,-48.819028
🛰️ Sensor no local original.
🟡 Risco: risco médio — Calor com humidade moderada.
11. Previsão (Open-Meteo)
Endpoint /carbon/forecast?lat=…&lon=… faz proxy de:
https://api.open-meteo.com/v1/forecast
?latitude=&longitude=
&hourly=precipitation,precipitation_probability,wind_speed_10m,wind_gusts_10m,wind_direction_10m
&forecast_hours=24&timezone=auto&wind_speed_unit=kmh
Retorna summary (total chuva mm, máx vento, máx rajada, máx prob. chuva) + hourly[] com 24 pontos. Cache em memória 10 min, chave por coordenada arredondada a 4 casas decimais.
12. Schema PostgreSQL
Schema carbon em goku_games @ 127.0.0.1:5432. Migrações idempotentes via _carbon_db_ensure_schema() rodam a cada save.
carbon.clients (
id BIGSERIAL PK,
client_id TEXT UNIQUE NOT NULL,
name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)
carbon.devices (
id BIGSERIAL PK,
device_id TEXT UNIQUE NOT NULL,
client_id TEXT REFERENCES carbon.clients,
label TEXT, -- = device_name do payload
location TEXT,
active BOOLEAN DEFAULT TRUE,
latitude DOUBLE PRECISION, -- adicionado Fase 1
longitude DOUBLE PRECISION, -- adicionado Fase 1
created_at TIMESTAMPTZ DEFAULT NOW()
)
carbon.sensor_readings (
id BIGSERIAL PK,
device_id TEXT REFERENCES carbon.devices,
client_id TEXT REFERENCES carbon.clients,
humidity NUMERIC(6,2),
temperature NUMERIC(6,2),
pressure NUMERIC(8,2),
co2 NUMERIC(10,2),
battery NUMERIC(5,2), -- adicionado Fase 1
movimento SMALLINT, -- adicionado Fase 1
latitude DOUBLE PRECISION, -- adicionado Fase 1
longitude DOUBLE PRECISION, -- adicionado Fase 1
risk_level TEXT,
raw_payload JSONB NOT NULL,
measured_at TIMESTAMPTZ DEFAULT NOW(),
received_at TIMESTAMPTZ DEFAULT NOW()
)
-- Índices
idx_carbon_readings_received_at ON sensor_readings (received_at DESC)
idx_carbon_readings_device_time ON sensor_readings (device_id, measured_at DESC)
13. Infraestrutura
| Componente | Tecnologia | Localização | Service |
|---|---|---|---|
| Reverse proxy / TLS | nginx + Let's Encrypt | /etc/nginx/sites-enabled/danhausch.cloud.conf | nginx.service |
| API/Webhook | Python 3 + Flask + gunicorn (3 workers) | /root/.openclaw/workspace/funnel_backend.py | funnel_backend.service |
| WhatsApp client | Node.js + Baileys | …/apps/openclaw-cockpit/wa-backend | openclaw-wa-backend.service |
| DB | PostgreSQL | 127.0.0.1:5432 · DB goku_games | — |
| Persistência audit | JSONL append-only | /root/.openclaw/workspace/data/carbon_webhook_events.jsonl | — |
| Frontend dashboard | HTML estático + Chart.js + Leaflet | …/carbon/dashboard/index.html | servido pelo Flask |
| Frontend WA pairing | HTML estático | …/carbon/whatsapp/index.html | servido pelo Flask |
| Forecast externo | Open-Meteo (free) | api.open-meteo.com | — |
Variáveis de ambiente relevantes
| Variável | Default | Função |
|---|---|---|
WA_BACKEND_BASE_URL | http://127.0.0.1:8790 | URL do backend Baileys |
CARBON_WEBHOOK_FORWARD_ENABLED | true | Liga/desliga forward para WhatsApp |
CARBON_WEBHOOK_FORWARD_PHONE | JID configurado | Telefone OU JID de grupo (admin) |
CARBON_AUDIO_ALERT_PHONES | = forward phone | Lista CSV de telefones para alerta CO₂ |
CARBON_CO2_ALERT_THRESHOLD | (numérico) | Limiar CO₂ ppm para alerta crítico |
PG_HOST/PG_PORT/PGDATABASE/PGUSER/PGPASSWORD | — | Conexão PostgreSQL |
CARBON_WEBHOOK_FILE | JSONL path | Audit log local |
14. Deploy / Operação
Restart de serviços
# API/webhook (após editar funnel_backend.py)
sudo systemctl restart funnel_backend.service
# WhatsApp Baileys
sudo systemctl restart openclaw-wa-backend.service
# Logs
sudo journalctl -u funnel_backend.service -f
sudo journalctl -u openclaw-wa-backend.service -f
Configurar grupo WhatsApp (admin)
# Listar grupos
curl -s http://127.0.0.1:8790/wa/chats | jq '.items[] | select(.id | endswith("@g.us"))'
# Setar grupo via systemd drop-in
sudo mkdir -p /etc/systemd/system/funnel_backend.service.d
sudo tee /etc/systemd/system/funnel_backend.service.d/carbon-forward.conf <<EOF
[Service]
Environment="CARBON_WEBHOOK_FORWARD_PHONE=<jid>@g.us"
EOF
sudo systemctl daemon-reload
sudo systemctl restart funnel_backend.service
Smoke test
curl -s -X POST http://127.0.0.1:5000/carbon \
-H 'Content-Type: application/json' \
-d '{"temp":30.1,"umid":47,"co2":305,"pa":949.5,"bat":3.48,"movimento":0,
"device_id":"test-001","device_name":"smoke","cliente":"QA",
"latitude":-22.5,"longitude":-48.8}'
Status check
curl -s http://127.0.0.1:8790/wa/status
curl -s http://127.0.0.1:5000/carbon/dashboard/data | jq
curl -s "http://127.0.0.1:5000/carbon/forecast?lat=-22.5&lon=-48.8" | jq .summary
/root/.openclaw/credentials/whatsapp/. Backup automático via carbon-backup.timer roda diariamente; rotação keep-last-14 em /var/backups/carbon/.14.1 Testing — checklist E2E
Plano de teste para validar todas as features. Substitua $BASE pelo host (http://127.0.0.1:5000 em dev ou https://api.danhausch.cloud em prod).
| # | Passo | Comando / Ação | Esperado |
|---|---|---|---|
| 1 | Health | curl -sI $BASE/carbon | HTTP 200 |
| 2 | Webhook anônimo | POST /carbon com payload do sensor | ok:true, db_saved:true, forward_legs.tenant_id resolvido |
| 3 | Mensagem WhatsApp | Conferir grupo Carbon Software | Linha "🌿 Carbon Exchange — <cliente> / <device>" recebida em < 5 s |
| 4 | Audit row | SELECT * FROM carbon.alert_events ORDER BY id DESC LIMIT 1 | status='sent', attempts=1, reading_id ligado |
| 5 | Login admin | POST /carbon/auth/login {email,password} | Devolve access_token, refresh_token, user.is_admin:true |
| 6 | Listar tenants | GET /carbon/admin/tenants com Bearer | Lista os 4 backfilled |
| 7 | Configurar JID do tenant | UI /carbon/admin → editar tenant 4 → escolher grupo | tenants.whatsapp_jid salvo |
| 8 | Dual-routing | Novo POST /carbon com cliente do tenant 4 | forward_legs.tenant.sent:true + admin.sent:true |
| 9 | Replay (Baileys offline) | systemctl stop openclaw-wa-backend, mandar webhook, religar | Em ≤ 60 s, carbon-replay.timer reenvia; alert_events.status vira sent e attempts=2 |
| 10 | Backup manual | systemctl start carbon-backup.service | Arquivo /var/backups/carbon/db/carbon-*.dump criado |
| 11 | Isolation | Criar viewer ligado ao tenant 4, logar como ele, GET /carbon/catalog | clients.length=1, só vê tenant 4 |
| 12 | Filtro do mapa | Selecionar cliente/device no dashboard | Mapa zoom + somente markers do filtro |
| 13 | Hard enforcement | Setar CARBON_AUTH_ENFORCE=true, restart, GET anônimo | HTTP 401 |
| 14 | Anti-spoof por token | Setar CARBON_TOKEN_ENFORCE=true, POST /carbon com token errado de um device cadastrado | HTTP 401 {error:"invalid_token", reason:"token_mismatch"} |
| 15 | TOFU | POST com novo device_id + token X → segundo POST mesmo device + token Y | Primeiro: 200 OK; segundo: 401 |
14.2 Smoke test rápido
BASE=https://api.danhausch.cloud
# 1. Login
TOKEN=$(curl -s -X POST $BASE/carbon/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"<senha>"}' | jq -r .access_token)
# 2. Webhook + verificar dual-routing
curl -s -X POST $BASE/carbon -H 'Content-Type: application/json' -d '{
"movimento":0,"temp":31.0,"umid":42,"co2":380,"pa":947.2,"bat":3.45,
"device_id":"sensor-test-01","device_name":"caixa-d-agua",
"cliente":"Vinícola Teste",
"latitude":-22.548545,"longitude":-48.819028
}' | jq '{ok,forward_legs,reading}'
# 3. Audit
curl -s "$BASE/carbon/admin/audit?limit=5" \
-H "Authorization: Bearer $TOKEN" | jq '.events[] | {channel,target,status,attempts}'
15. Fase 2 — entregue
Implementação do design em /carbon/phase2-design. Ativada via flag CARBON_AUTH_ENFORCE (default false para migração suave).
15.1 Auth (JWT)
POST /carbon/auth/login— bcrypt verify, devolve access (60 min) + refresh (30 d).POST /carbon/auth/refresh— novo access via refresh token; verifica revogação e expiração.POST /carbon/auth/logout— revoga refresh.GET /carbon/auth/me— devolve user + memberships (lê do JWT).- Decorator
_carbon_require_authimplementa soft-mode: quandoCARBON_AUTH_ENFORCE=false, anônimo passa; quandotrue, retorna 401 sem JWT. - Bootstrap inicial:
scripts/bootstrap_admin.pycom env varsCARBON_BOOTSTRAP_ADMIN_EMAIL/PASSWORD/NAME. Idempotente.
15.2 Multi-tenant
- Tabelas:
tenants,users,tenant_members,auth_refresh_tokens(criadas e populáveis pela admin UI). - Backfill executado: 4 tenants criados a partir dos clients existentes;
clients.tenant_idsetado. - Queries
_carbon_db_latest_reading,_carbon_db_readings_history,_carbon_db_catalogrecebem parâmetrotenant_filter. - Helper
_carbon_tenant_filter_from_g()deriva o filtro do JWT — admin vê tudo, demais usuários ficam restritos aos tenants das suas memberships.
15.3 WhatsApp routing por tenant
_carbon_resolve_tenant_for_device(device_id, cliente)resolve o tenant a partir do device cadastrado ou do nome do cliente._forward_carbon_webhook_to_whatsappagora envia duas mensagens: para o JID do tenant (se houver) + para o JID admin global. Cada leg vira uma linha emalert_events.- Se tenant JID = admin JID, o segundo envio é pulado (status
skipped_same_as_tenant).
15.4 Audit + replay queue
- Toda mensagem WhatsApp (envio, audio alert, retry) gera linha em
alert_events. - Falhas recebem
next_attempt_at = NOW() + 60s; workercarbon-replay.serviceroda a cada 60 s via timer systemd. - Backoff exponencial até 8 tentativas (60 s → 1 h); depois marca
status='dead'. - Admin UI lista o histórico em
/carbon/adminaba Auditoria.
15.5 Backup automático
carbon-backup.timerroda diariamente às 03:30 UTC; chama/opt/carbon-backup/backup.sh.- Faz
pg_dump -Fcda DBcarbon(viadocker exec) + tar.gz das credenciais Baileys em/root/.openclaw/credentials/whatsapp/. - Rotação keep-last-14 em
/var/backups/carbon/{db,wa}/.
15.6 UI admin
/carbon/login— autenticação; persiste tokens emlocalStorage; redireciona admins para/carbon/admine demais para/carbon/dashboard./carbon/admin— três tabs:- Tenants — lista, criar, editar JID via dropdown alimentado por
/carbon/admin/wa-groups, adicionar membro. - Usuários — list global, criar com email + senha + flag is_admin.
- Auditoria — últimos 200
alert_eventscom status, tentativas, próximo retry.
- Tenants — lista, criar, editar JID via dropdown alimentado por
15.7 Anti-spoof: token por device
Cada device tem um platform_token (string opaca ~20 chars) atribuído pela plataforma da Greenbug e enviado no campo token de cada payload. Quando CARBON_TOKEN_ENFORCE=true:
- Webhook compara
payload.tokencomdevices.platform_tokencadastrado. - Bate → 200; não bate → 401
{reason:"token_mismatch"}. - Device desconhecido → primeiro POST registra o token (TOFU); a partir do 2º, qualquer token diferente é rejeitado.
- Em soft mode (default), backend só loga via
token_checksem bloquear.
Os 19 devices conhecidos da Greenbug (iza01–iza10, khomp, mile-01–mile-08) foram seedados via scripts/seed_greenbug_tokens.py.
15.8 Pendente para Fase 3
- Endpoint admin para CRUD de devices com
platform_tokenvisível. - Convite por email (hoje admin define senha temporária ao criar usuário).
- Login rate-limit / lockout.
- Dedup por
(device_id, fCnt). - Detecção de device offline (last-seen alert).
Documento atualizado em 2026-05-10. Carbon Exchange • funnel_backend.py · repo danielharagao/carbon.