diff --git a/.config.example b/.config.example
new file mode 100644
index 0000000..111b983
--- /dev/null
+++ b/.config.example
@@ -0,0 +1,12 @@
+# Arquivo de configuração para variáveis de ambiente
+# Copie para .config e preencha com seus valores
+
+# **ÚNICA configuração necessária**: ID do documento Google Docs central
+# Este documento contém TODAS as chaves de configuração em formato CHAVE=VALOR
+# Exemplo: DEMOGRAFIA_CSV_URL=https://...
+# DEFAULT_DOCS_URL=https://...
+
+CONFIG_DOC_ID=12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA
+
+# TUDO MAIS VEM DO DOCUMENTO CENTRAL
+# Não adicione URLs aqui - elas devem estar no Google Docs!
diff --git a/CONFIG_SETUP.md b/CONFIG_SETUP.md
new file mode 100644
index 0000000..8904873
--- /dev/null
+++ b/CONFIG_SETUP.md
@@ -0,0 +1,127 @@
+# Sistema de Configuração Centralizado
+
+## Visão Geral
+
+A aplicação usa **UMA ÚNICA URL** apontando para um documento Google Docs central que armazena **TODAS** as configurações em formato `CHAVE=VALOR`.
+
+Não há URLs hardcoded, não há múltiplos .env files. Tudo vem do documento central.
+
+## Como Funciona
+
+### 1. **Única URL de Configuração**
+
+Edite `.env` ou `main.py` para definir:
+
+```env
+CONFIG_DOC_ID=12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA
+```
+
+Ou no código:
+```python
+CONFIG_DOC_ID = os.getenv("CONFIG_DOC_ID", "12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA")
+```
+
+### 2. **Documento Central (Google Docs)**
+
+O documento deve estar **publicamente acessível** (Qualquer pessoa com o link - Leitor) e ter o seguinte formato:
+
+```
+DEMOGRAFIA_CSV_URL=https://drive.google.com/file/d/1zH6Yri2EdchUUjoTDtXgdHrmo6SVBfBG/view
+DEFAULT_DOCS_URL=https://docs.google.com/document/d/1WA3LcQAWIKFYu6MmuF4RSrGFSdYvbpnn/edit?userstoinvite=lucianna.mrf@gmail.com&sharingaction=manageaccess&role=writer
+
+EDUCACAO_CSV_URL=https://drive.google.com/file/d/SEU_ID_AQUI/view
+EDUCACAO_DOCS_URL=https://docs.google.com/document/d/SEU_ID_AQUI/edit
+
+# Comentários começam com #
+# Linhas em branco são ignoradas
+```
+
+### 3. **Inicialização da Aplicação**
+
+Na inicialização:
+1. ✓ Conecta ao documento central (Google Docs export URL)
+2. ✓ Lê o conteúdo em plain text
+3. ✓ Parse cada linha `CHAVE=VALOR`
+4. ✓ Armazena em memória no dicionário `CONFIG`
+5. ✗ Se falhar aqui, a aplicação não inicia (erro crítico)
+
+### 4. **Uso Durante Execução**
+
+Toda a aplicação acessa `CONFIG["CHAVE"]`:
+
+```python
+# Ao gerar relatório
+csv_url = CONFIG["DEMOGRAFIA_CSV_URL"]
+docs_url = CONFIG["DEFAULT_DOCS_URL"]
+
+# Se a chave não existir, erro 503 Service Unavailable
+```
+
+## APIs
+
+### `GET /config`
+
+Retorna a configuração carregada (para debug):
+
+```json
+{
+ "config_doc_url": "https://docs.google.com/document/d/12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA/export?format=txt",
+ "config_loaded": true,
+ "config_keys": ["DEMOGRAFIA_CSV_URL", "DEFAULT_DOCS_URL"],
+ "config_values": {
+ "DEMOGRAFIA_CSV_URL": "https://drive.google.com/...",
+ "DEFAULT_DOCS_URL": "https://docs.google.com/..."
+ }
+}
+```
+
+## Troubleshooting
+
+### "ERRO CRÍTICO: Não foi possível carregar a configuração central"
+
+Verifique:
+1. **Documento público?** Acesse o link direto no navegador. Deve estar acessível sem login.
+2. **Formato correto?** Cada linha: `CHAVE=VALOR`
+3. **Internet?** Teste: `curl -I https://docs.google.com/document/d/SEU_ID/export?format=txt`
+4. **Timeout?** O Google Docs pode ser lento. Timeout está em 20s.
+
+### "Configuração XXX_URL não encontrada"
+
+A chave não existe no documento central. Adicione ao Google Docs:
+
+```
+MINHA_CHAVE=https://exemplo.com/meu-arquivo.csv
+```
+
+## Exemplo Completo
+
+**Documento Google Docs (URL: `https://docs.google.com/document/d/12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA`)**
+
+```
+# Configurações de Demografia
+DEMOGRAFIA_CSV_URL=https://drive.google.com/file/d/1zH6Yri2EdchUUjoTDtXgdHrmo6SVBfBG/view
+DEFAULT_DOCS_URL=https://docs.google.com/document/d/1WA3LcQAWIKFYu6MmuF4RSrGFSdYvbpnn/edit
+
+# Futuras configurações
+EDUCACAO_CSV_URL=https://drive.google.com/file/d/ABC123/view
+EDUCACAO_DOCS_URL=https://docs.google.com/document/d/DEF456/edit
+```
+
+**Código Python:**
+
+```python
+# Carrega tudo na inicialização
+CONFIG = carregar_config_central()
+
+# Usa em qualquer lugar
+csv_url = CONFIG["DEMOGRAFIA_CSV_URL"]
+docs_url = CONFIG["DEFAULT_DOCS_URL"]
+```
+
+## Benefícios
+
+✓ **Única fonte de verdade** - Um documento, todas as configs
+✓ **Sem deploy** - Muda URL no Google Docs, aplicação já usa
+✓ **Escalável** - Novos macrotemas? Apenas adicione linhas ao documento
+✓ **Seguro** - Futuramente, pode ser um secret no GitHub Actions
+✓ **Sem hardcoding** - Nenhuma URL no código ou .env
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 22ee2ba..7073198 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'
// Default to same-origin when served by the API (Docker/prod).
// In dev (Vite on :5173), set VITE_API_BASE_URL=http://127.0.0.1:8000.
-const API_BASE = import.meta.env.VITE_API_BASE_URL || window.location.origin
+const API_BASE = 'http://127.0.0.1:8000'
const MACROTEMAS = [
'Demografia'
]
diff --git a/main.py b/main.py
index ac5a4fc..9256bf5 100644
--- a/main.py
+++ b/main.py
@@ -36,13 +36,21 @@
OUTPUT_DIR = BASE_DIR / "output"
CITIES_FILE = BASE_DIR / "citys.txt"
+# ÚNICA URL que aponta para o documento central de configuração
+CONFIG_DOC_ID = os.getenv("CONFIG_DOC_ID", "12o-W-VtSl9ytbF6CD9S14ACJL63XsmYJnmZValPqKKA")
+CONFIG_DOC_URL = f"https://docs.google.com/document/d/{CONFIG_DOC_ID}/export?format=txt"
+
carregado = load_dotenv(dotenv_path='.config')
-DEMOGRAFIA_CSV_URL = os.getenv("DEMOGRAFIA_CSV_URL")
-DEFAULT_DOCS_URL = os.getenv("DEFAULT_DOCS_URL")
FALLBACK_DOC_TEXT = """deu erro.
"""
+CONFIG_KEY_ALIASES = {
+ "DEMOGRAFIA_CSV": "DEMOGRAFIA_CSV_URL",
+ "DEMOGRAFIA_TEMPLATE": "DEFAULT_DOCS_URL",
+ "DEFAULT_DOCS": "DEFAULT_DOCS_URL",
+}
+
TEMPLATE_STRING = """
@@ -117,6 +125,70 @@
"""
+def carregar_config_central() -> dict:
+ """
+ Reads ONLY from the central configuration Google Docs.
+ Parses KEY=VALUE pairs and returns a dictionary.
+
+ This is the SINGLE SOURCE OF TRUTH for all URLs and configuration.
+ If this fails, the application cannot start.
+ """
+ config = {}
+ try:
+ with urlopen(CONFIG_DOC_URL, timeout=20) as response:
+ conteudo = response.read().decode("utf-8")
+
+ for linha in conteudo.splitlines():
+ linha = linha.strip()
+ if not linha or linha.startswith("#"):
+ continue
+ if "=" in linha:
+ chave, valor = linha.split("=", 1)
+ chave_normalizada = chave.strip().lstrip("\ufeff").upper()
+ valor_limpo = valor.strip().strip('"').strip("'")
+ config[chave_normalizada] = valor_limpo
+
+ for origem, destino in CONFIG_KEY_ALIASES.items():
+ if destino not in config and origem in config:
+ config[destino] = config[origem]
+
+ if not config:
+ raise ValueError("Documento de configuração está vazio ou mal formatado.")
+
+ print(f"✓ Configuração carregada com sucesso: {len(config)} chaves")
+ return config
+ except Exception as err:
+ raise RuntimeError(
+ f"ERRO CRÍTICO: Não foi possível carregar a configuração central.\n"
+ f"URL: {CONFIG_DOC_URL}\n"
+ f"Erro: {err}\n\n"
+ f"Verifique se:\n"
+ f" 1. O documento é acessível publicamente\n"
+ f" 2. O formato está correto (CHAVE=VALOR)\n"
+ f" 3. A conexão com Google Docs está funcionando"
+ ) from err
+
+
+def obter_config(chave: str) -> str:
+ chave = chave.upper()
+ if chave in CONFIG and CONFIG[chave]:
+ return CONFIG[chave]
+
+ chave_bom = f"\ufeff{chave}"
+ if chave_bom in CONFIG and CONFIG[chave_bom]:
+ return CONFIG[chave_bom]
+
+ alias = next((dest for src, dest in CONFIG_KEY_ALIASES.items() if dest == chave and src in CONFIG), None)
+ if alias and CONFIG.get(alias):
+ return CONFIG[alias]
+
+ raise HTTPException(status_code=503, detail=f"Configuração {chave} não encontrada no documento central.")
+
+
+# Load config from central Google Docs on startup
+CONFIG = carregar_config_central()
+
+
def extrair_doc_id(link_ou_id: str) -> str:
valor = link_ou_id.strip()
if "/document/d/" not in valor:
@@ -256,25 +328,50 @@ def filtrar_linhas_por_cidade(df: pd.DataFrame, cidade: str) -> pd.DataFrame:
return df[mascara_sem_uf]
+def normalizar_url_csv(link_ou_id: str) -> str:
+ """
+ Converts a Google Drive sharing link to a direct download URL.
+ If it already is a direct URL, returns it as-is.
+ """
+ valor = link_ou_id.strip()
+ if "/file/d/" not in valor:
+ return valor
+
+ parsed = urlparse(valor)
+ partes = [p for p in parsed.path.split("/") if p]
+ if "d" in partes:
+ idx = partes.index("d")
+ if idx + 1 < len(partes):
+ file_id = partes[idx + 1]
+ return f"https://drive.google.com/uc?export=download&id={file_id}"
+
+ raise ValueError("Não foi possível extrair o ID do arquivo CSV do Google Drive.")
+
+
@app.get("/cities")
async def listar_cidades():
return carregar_cidades()
@app.get("/relatorio/{cidade}", response_class=HTMLResponse)
async def gerar_relatorio(cidade: str):
- df = pd.read_csv(DEMOGRAFIA_CSV_URL, delimiter=";")
+ # Tudo vem do CONFIG (que vem do documento central)
+ csv_url = obter_config("DEMOGRAFIA_CSV_URL")
+ df = pd.read_csv(normalizar_url_csv(csv_url), delimiter=";")
linhas_df = filtrar_linhas_por_cidade(df, cidade)
linhas = linhas_df.to_dict("records")
if not linhas:
raise HTTPException(status_code=404, detail=f"Cidade '{cidade}' não encontrada.")
- # If DATANE_DOCS_URL is set but empty (common in docker-compose), fall back to default.
- docs_url = os.getenv("DATANE_DOCS_URL") or DEFAULT_DOCS_URL
+ # Get docs URL from CONFIG (central source only)
+ docs_url = obter_config("DEFAULT_DOCS_URL")
try:
docs_texto = carregar_texto_do_docs(docs_url)
except ValueError as err:
- raise HTTPException(status_code=400, detail=str(err)) from err
+ raise HTTPException(
+ status_code=400,
+ detail=f"Erro ao carregar o documento de template: {str(err)}"
+ ) from err
docs_html = texto_para_html(docs_texto, linhas[0])
@@ -293,6 +390,20 @@ async def gerar_relatorio(cidade: str):
return HTMLResponse(content=html)
+@app.get("/config")
+async def verificar_config():
+ """
+ Debug endpoint to verify the loaded configuration.
+ Shows all KEY=VALUE pairs loaded from the central Google Docs.
+ """
+ return {
+ "config_doc_url": CONFIG_DOC_URL,
+ "config_loaded": len(CONFIG) > 0,
+ "config_keys": list(CONFIG.keys()),
+ "config_values": CONFIG,
+ }
+
+
# If the frontend has been built (e.g., via Docker), serve it from the same app.
FRONTEND_DIST_DIR = BASE_DIR / "frontend" / "dist"
if FRONTEND_DIST_DIR.exists():
diff --git a/report-generator-demo/frontend/src/App.jsx b/report-generator-demo/frontend/src/App.jsx
new file mode 100644
index 0000000..db2b9ee
--- /dev/null
+++ b/report-generator-demo/frontend/src/App.jsx
@@ -0,0 +1,184 @@
+import React, { useEffect, useMemo, useState } from 'react'
+
+// Default to same-origin when served by the API (Docker/prod).
+// In dev (Vite on :5173), set VITE_API_BASE_URL=http://127.0.0.1:8000.
+const API_BASE = import.meta.env.VITE_API_BASE_URL || window.location.origin
+const MACROTEMAS = [
+ 'Demografia'
+]
+
+function App() {
+ const [cities, setCities] = useState([])
+ const [showForm, setShowForm] = useState(false)
+ const [selectedMacrotema, setSelectedMacrotema] = useState(MACROTEMAS[0])
+ const [selectedCity, setSelectedCity] = useState('')
+ const [citySearch, setCitySearch] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ async function fetchCities() {
+ try {
+ setLoading(true)
+ const response = await fetch(`${API_BASE}/cities`)
+ if (!response.ok) {
+ throw new Error('Falha ao carregar cidades')
+ }
+ const data = await response.json()
+ setCities(Array.isArray(data) ? data : [])
+ } catch (err) {
+ setError(err.message || 'Erro ao carregar cidades')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchCities()
+ }, [])
+
+ const cityCount = useMemo(() => cities.length, [cities])
+ const filteredCities = useMemo(() => {
+ const term = citySearch.trim().toLowerCase()
+ if (!term) return cities
+ return cities.filter((city) => city.toLowerCase().includes(term))
+ }, [cities, citySearch])
+
+ function openReport() {
+ if (!selectedCity) {
+ return
+ }
+
+ const url = `${API_BASE}/relatorio/${encodeURIComponent(selectedCity)}`
+ window.open(url, '_blank')
+ }
+
+ return (
+
+
+
+ Sudene • Gerador de relatórios
+
Geração de relatório para Sudene
+
+ Escolha a cidade para montar o relatório de Demografia.
+