feat(desktop): add Tauri updater (GitHub Releases), updater UI button, hide PowerShell windows; fix Windows inventory arrays and activation; improve metrics parsing; branding rename to Raven across app; avoid localhost fallback in auth-server; inject APP_URL/AUTH_URL in stack

This commit is contained in:
Esdras Renan 2025-10-10 20:39:39 -03:00
parent eb5f39100f
commit 418599ef62
18 changed files with 127 additions and 34 deletions

View file

@ -5,7 +5,7 @@
<meta name="color-scheme" content="light" /> <meta name="color-scheme" content="light" />
<link rel="stylesheet" href="/src/index.css" /> <link rel="stylesheet" href="/src/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sistema de Chamados — Agente Desktop</title> <title>Raven — Agente Desktop</title>
<script type="module" src="/src/main.tsx" defer></script> <script type="module" src="/src/main.tsx" defer></script>
</head> </head>
<body> <body>

View file

@ -21,6 +21,8 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = ["wry"] } tauri = { version = "2", features = ["wry"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-store = "2.4" tauri-plugin-store = "2.4"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] } sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] }

View file

@ -11,6 +11,8 @@
"store:allow-set", "store:allow-set",
"store:allow-get", "store:allow-get",
"store:allow-save", "store:allow-save",
"store:allow-delete" "store:allow-delete",
"updater:default",
"process:default"
] ]
} }

View file

@ -469,13 +469,18 @@ fn collect_linux_extended() -> serde_json::Value {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn collect_windows_extended() -> serde_json::Value { fn collect_windows_extended() -> serde_json::Value {
use std::process::Command; use std::process::Command;
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
fn ps(cmd: &str) -> Option<serde_json::Value> { fn ps(cmd: &str) -> Option<serde_json::Value> {
let ps_cmd = format!( let ps_cmd = format!(
"$ErrorActionPreference='SilentlyContinue'; {} | ConvertTo-Json -Depth 4 -Compress", "$ErrorActionPreference='SilentlyContinue'; {} | ConvertTo-Json -Depth 4 -Compress",
cmd cmd
); );
let out = Command::new("powershell") let out = Command::new("powershell")
.creation_flags(CREATE_NO_WINDOW)
.arg("-NoProfile") .arg("-NoProfile")
.arg("-WindowStyle").arg("Hidden")
.arg("-NoLogo")
.arg("-Command") .arg("-Command")
.arg(ps_cmd) .arg(ps_cmd)
.output() .output()
@ -486,23 +491,24 @@ fn collect_windows_extended() -> serde_json::Value {
let software = ps(r#"@(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher"#) let software = ps(r#"@(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher"#)
.unwrap_or_else(|| json!([])); .unwrap_or_else(|| json!([]));
let services = ps("Get-Service | Select-Object Name,Status,DisplayName").unwrap_or_else(|| json!([])); let services = ps("@(Get-Service | Select-Object Name,Status,DisplayName)").unwrap_or_else(|| json!([]));
let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({})); let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({}));
let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([])); let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([]));
// Informações de build/edição e ativação // Informações de build/edição e ativação
let os_info = ps(r#" let os_info = ps(r#"
$cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'; $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion';
$ls = (Get-CimInstance -Query "SELECT LicenseStatus FROM SoftwareLicensingProduct WHERE PartialProductKey IS NOT NULL" | Select-Object -First 1).LicenseStatus; $ls = Get-CimInstance -Query "SELECT Name, LicenseStatus FROM SoftwareLicensingProduct WHERE PartialProductKey IS NOT NULL" | Where-Object { $_.Name -like 'Windows*' } | Select-Object -First 1;
$lsCode = if ($ls -and $ls.LicenseStatus -ne $null) { [int]$ls.LicenseStatus } else { 0 };
[PSCustomObject]@{ [PSCustomObject]@{
ProductName = $cv.ProductName ProductName = $cv.ProductName
CurrentBuild = $cv.CurrentBuild CurrentBuild = $cv.CurrentBuild
CurrentBuildNumber = $cv.CurrentBuildNumber CurrentBuildNumber = $cv.CurrentBuildNumber
DisplayVersion = $cv.DisplayVersion DisplayVersion = $cv.DisplayVersion
ReleaseId = $cv.ReleaseId ReleaseId = $cv.ReleaseId
EditionID = $cv.EditionID EditionID = $cv.EditionID
LicenseStatus = $ls LicenseStatus = $lsCode
IsActivated = ($ls -eq 1) IsActivated = ($lsCode -eq 1)
} }
"#).unwrap_or_else(|| json!({})); "#).unwrap_or_else(|| json!({}));
@ -510,9 +516,9 @@ fn collect_windows_extended() -> serde_json::Value {
let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({})); let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({}));
let baseboard = ps("Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version").unwrap_or_else(|| json!({})); let baseboard = ps("Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version").unwrap_or_else(|| json!({}));
let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({})); let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({}));
let memory = ps("Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage").unwrap_or_else(|| json!([])); let memory = ps("@(Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage)").unwrap_or_else(|| json!([]));
let video = ps("Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID").unwrap_or_else(|| json!([])); let video = ps("@(Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID)").unwrap_or_else(|| json!([]));
let disks = ps("Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType").unwrap_or_else(|| json!([])); let disks = ps("@(Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType)").unwrap_or_else(|| json!([]));
json!({ json!({
"windows": { "windows": {

View file

@ -38,6 +38,8 @@ pub fn run() {
.manage(AgentRuntime::new()) .manage(AgentRuntime::new())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(StorePluginBuilder::default().build()) .plugin(StorePluginBuilder::default().build())
.plugin(tauri_plugin_updater::init())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
collect_machine_profile, collect_machine_profile,
collect_machine_inventory, collect_machine_inventory,

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Sistema de Chamados Desktop", "productName": "Raven",
"version": "0.1.0", "version": "0.1.0",
"identifier": "br.com.esdrasrenan.sistemadechamados", "identifier": "br.com.esdrasrenan.sistemadechamados",
"build": { "build": {
@ -13,7 +13,7 @@
"withGlobalTauri": true, "withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "Sistema de Chamados", "title": "Raven",
"width": 1100, "width": 1100,
"height": 720, "height": 720,
"resizable": true "resizable": true
@ -23,6 +23,14 @@
"csp": null "csp": null
} }
}, },
"updater": {
"active": true,
"endpoints": [
"https://github.com/esdrasrenan/sistema-de-chamados/releases/latest/download/latest.json"
],
"dialog": true,
"pubkey": "REPLACE_WITH_TAURI_PUBLIC_KEY"
},
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",

View file

@ -2,7 +2,7 @@ import { useEffect, useState } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { invoke } from "@tauri-apps/api/core" import { invoke } from "@tauri-apps/api/core"
import { Store } from "@tauri-apps/plugin-store" import { Store } from "@tauri-apps/plugin-store"
import { ExternalLink, Eye, EyeOff } from "lucide-react" import { ExternalLink, Eye, EyeOff, GalleryVerticalEnd, RefreshCw } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"
import { cn } from "./lib/utils" import { cn } from "./lib/utils"
@ -127,6 +127,7 @@ function App() {
const [company, setCompany] = useState("") const [company, setCompany] = useState("")
const [collabEmail, setCollabEmail] = useState("") const [collabEmail, setCollabEmail] = useState("")
const [collabName, setCollabName] = useState("") const [collabName, setCollabName] = useState("")
const [updating, setUpdating] = useState(false)
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -241,13 +242,38 @@ function App() {
} }
} }
async function checkForUpdates() {
try {
setUpdating(true)
const { check } = await import("@tauri-apps/plugin-updater")
const update = await check()
if (update && (update as any).available) {
// download and install then relaunch
await (update as any).downloadAndInstall()
const { relaunch } = await import("@tauri-apps/plugin-process")
await relaunch()
} else {
alert("Nenhuma atualização disponível.")
}
} catch (error) {
console.error("Falha ao verificar atualizações", error)
alert("Falha ao verificar atualizações.")
} finally {
setUpdating(false)
}
}
return ( return (
<div className="min-h-screen grid place-items-center p-6"> <div className="min-h-screen grid place-items-center p-6">
<div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> <div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<div className="flex items-start justify-between gap-3"> <div className="mb-2 flex items-center justify-between gap-3">
<h1 className="text-xl font-semibold">Sistema de Chamados Agente Desktop</h1> <div className="flex items-center gap-2 text-xl font-semibold text-neutral-900">
<span className="flex size-6 items-center justify-center rounded-md bg-black text-white"><GalleryVerticalEnd className="size-4" /></span>
Sistema de chamados
</div>
<StatusBadge status={status} /> <StatusBadge status={status} />
</div> </div>
<h1 className="text-base font-semibold text-slate-800">Agente Desktop</h1>
{error ? <p className="mt-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700">{error}</p> : null} {error ? <p className="mt-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700">{error}</p> : null}
{!token ? ( {!token ? (
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
@ -263,7 +289,7 @@ function App() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm font-medium">Empresa (slug opcional)</label> <label className="text-sm font-medium">Empresa (slug opcional)</label>
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="ex.: tenant-atlas" value={company} onChange={(e)=>setCompany(e.target.value)} /> <input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="ex.: atlas-engenharia" value={company} onChange={(e)=>setCompany(e.target.value)} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm font-medium">Colaborador (e-mail)</label> <label className="text-sm font-medium">Colaborador (e-mail)</label>
@ -360,11 +386,21 @@ function App() {
<label className="label">Nome do colaborador (opcional)</label> <label className="label">Nome do colaborador (opcional)</label>
<input className="input" placeholder="Nome completo" value={collabName} onChange={(e)=>setCollabName(e.target.value)} /> <input className="input" placeholder="Nome completo" value={collabName} onChange={(e)=>setCollabName(e.target.value)} />
</div> </div>
<div className="grid gap-2">
<label className="label">Atualizações</label>
<button onClick={checkForUpdates} disabled={updating} className={cn("btn btn-outline inline-flex items-center gap-2", updating && "opacity-60")}>
<RefreshCw className="size-4" /> Verificar atualizações
</button>
</div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
)} )}
</div> </div>
<div className="mt-6 flex justify-center">
<img src={`${appUrl}/rever-8.png`} alt="Logotipo Rever Tecnologia" width={110} height={110} className="h-[3.45rem] w-auto" />
</div>
</div>
</div> </div>
) )
} }

View file

@ -103,6 +103,15 @@ export async function POST(request: Request) {
} catch (error) { } catch (error) {
console.error("[machines.register] Falha no provisionamento", error) console.error("[machines.register] Falha no provisionamento", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao provisionar máquina", details }, 500, request.headers.get("origin"), CORS_METHODS) const msg = details.toLowerCase()
// Mapear alguns erros "esperados" para códigos adequados
// - empresa inválida → 404
// - segredo inválido → 401
// - demais ConvexError → 400
const isCompanyNotFound = msg.includes("empresa não encontrada")
const isInvalidSecret = msg.includes("código de provisionamento inválido")
const isConvexError = msg.includes("convexerror")
const status = isCompanyNotFound ? 404 : isInvalidSecret ? 401 : isConvexError ? 400 : 500
return jsonWithCors({ error: "Falha ao provisionar máquina", details }, status, request.headers.get("origin"), CORS_METHODS)
} }
} }

View file

@ -29,7 +29,7 @@ export default async function InvitePage({ params }: { params: Promise<{ token:
<CardHeader className="space-y-2 text-center"> <CardHeader className="space-y-2 text-center">
<CardTitle className="text-2xl font-semibold text-neutral-900">Aceitar convite</CardTitle> <CardTitle className="text-2xl font-semibold text-neutral-900">Aceitar convite</CardTitle>
<CardDescription className="text-sm text-neutral-600"> <CardDescription className="text-sm text-neutral-600">
Conclua seu cadastro para acessar a plataforma Sistema de chamados. Conclua seu cadastro para acessar a plataforma Raven.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View file

@ -18,8 +18,8 @@ const jetBrainsMono = JetBrains_Mono({
}) })
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sistema de chamados", title: "Raven",
description: "Plataforma de chamados da Rever", description: "Plataforma Raven da Rever",
icons: { icons: {
icon: "/rever-8.png", icon: "/rever-8.png",
}, },

View file

@ -43,7 +43,7 @@ export function LoginPageClient() {
<div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md"> <div className="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
<GalleryVerticalEnd className="size-4" /> <GalleryVerticalEnd className="size-4" />
</div> </div>
Sistema de chamados Raven
</Link> </Link>
</div> </div>
<div className="flex flex-1 items-center justify-center"> <div className="flex flex-1 items-center justify-center">

View file

@ -23,7 +23,7 @@ const ERROR_TEMPLATE = `
<h1>Não foi possível autenticar esta máquina</h1> <h1>Não foi possível autenticar esta máquina</h1>
<p>O token informado é inválido, expirou ou não está mais associado a uma máquina ativa.</p> <p>O token informado é inválido, expirou ou não está mais associado a uma máquina ativa.</p>
<p>Volte ao agente desktop, gere um novo token ou realize o provisionamento novamente.</p> <p>Volte ao agente desktop, gere um novo token ou realize o provisionamento novamente.</p>
<a href="/">Voltar para o Sistema de Chamados</a> <a href="/">Voltar para o Raven</a>
</main> </main>
</body> </body>
</html> </html>
@ -61,4 +61,3 @@ export async function GET(request: NextRequest) {
}) })
} }
} }

View file

@ -1354,8 +1354,27 @@ function DetailLine({ label, value, classNameValue }: DetailLineProps) {
function MetricsGrid({ metrics }: { metrics: MachineMetrics }) { function MetricsGrid({ metrics }: { metrics: MachineMetrics }) {
const data = (metrics ?? {}) as Record<string, unknown> const data = (metrics ?? {}) as Record<string, unknown>
const cpu = Number(data.cpuUsage ?? data.cpu ?? data.cpu_percent ?? NaN) // Compat: aceitar chaves do agente desktop (cpuUsagePercent, memoryUsedBytes, memoryTotalBytes)
const memory = Number(data.memoryBytes ?? data.memory ?? data.memory_used ?? NaN) const cpu = (() => {
const v = Number(
data.cpuUsage ?? data.cpu ?? data.cpu_percent ?? data.cpuUsagePercent ?? NaN
)
return v
})()
const memory = (() => {
// valor absoluto em bytes, se disponível
const memBytes = Number(
data.memoryBytes ?? data.memory ?? data.memory_used ?? data.memoryUsedBytes ?? NaN
)
if (Number.isFinite(memBytes)) return memBytes
// tentar derivar a partir de percentuais do agente
const usedPct = Number(data.memoryUsedPercent ?? NaN)
const totalBytes = Number(data.memoryTotalBytes ?? NaN)
if (Number.isFinite(usedPct) && Number.isFinite(totalBytes)) {
return Math.max(0, Math.min(1, usedPct > 1 ? usedPct / 100 : usedPct)) * totalBytes
}
return NaN
})()
const disk = Number(data.diskUsage ?? data.disk ?? NaN) const disk = Number(data.diskUsage ?? data.disk ?? NaN)
return ( return (

View file

@ -164,7 +164,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<Sidebar {...props}> <Sidebar {...props}>
<SidebarHeader className="gap-3"> <SidebarHeader className="gap-3">
<VersionSwitcher <VersionSwitcher
label="Sistema de chamados" label="Raven"
versions={[...navigation.versions]} versions={[...navigation.versions]}
defaultVersion={navigation.versions[0]} defaultVersion={navigation.versions[0]}
/> />

View file

@ -59,7 +59,7 @@ export function PortalShell({ children }: PortalShellProps) {
<span className="text-xs font-semibold uppercase tracking-[0.28em] text-neutral-500"> <span className="text-xs font-semibold uppercase tracking-[0.28em] text-neutral-500">
Portal do cliente Portal do cliente
</span> </span>
<span className="text-lg font-semibold text-neutral-900">Sistema de chamados</span> <span className="text-lg font-semibold text-neutral-900">Raven</span>
</div> </div>
<nav className="flex items-center gap-3 text-sm font-medium"> <nav className="flex items-center gap-3 text-sm font-medium">
{navItems.map((item) => { {navItems.map((item) => {
@ -112,7 +112,7 @@ export function PortalShell({ children }: PortalShellProps) {
</main> </main>
<footer className="border-t border-slate-200 bg-white/70"> <footer className="border-t border-slate-200 bg-white/70">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4 text-xs text-neutral-500"> <div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4 text-xs text-neutral-500">
<span>&copy; {new Date().getFullYear()} Sistema de chamados</span> <span>&copy; {new Date().getFullYear()} Raven</span>
<span>Suporte: suporte@sistema.dev</span> <span>Suporte: suporte@sistema.dev</span>
</div> </div>
</footer> </footer>

View file

@ -54,7 +54,7 @@ export function BackgroundPaperShaders({ className }: { className?: string }) {
<div className="relative h-[420px] w-[420px] overflow-hidden rounded-full"> <div className="relative h-[420px] w-[420px] overflow-hidden rounded-full">
<ShaderVisual /> <ShaderVisual />
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center text-white"> <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center text-white">
<div className="text-sm uppercase tracking-[0.32em] text-white/50">Sistema de Chamados</div> <div className="text-sm uppercase tracking-[0.32em] text-white/50">Raven</div>
<h2 className="mt-4 text-xl font-semibold md:text-2xl">Atendimento moderno e colaborativo</h2> <h2 className="mt-4 text-xl font-semibold md:text-2xl">Atendimento moderno e colaborativo</h2>
<p className="mt-3 text-sm text-white/70"> <p className="mt-3 text-sm text-white/70">
Tenha visão unificada de todos os canais, monitore SLAs em tempo real e mantenha os clientes informados Tenha visão unificada de todos os canais, monitore SLAs em tempo real e mantenha os clientes informados

View file

@ -24,7 +24,14 @@ async function buildRequest() {
headerList.get("x-client-ip") || headerList.get("x-client-ip") ||
undefined undefined
return new Request(env.BETTER_AUTH_URL ?? "http://localhost:3000", { // Evitar fallback para localhost em produção: tenta BETTER_AUTH_URL,
// depois NEXT_PUBLIC_APP_URL e, por fim, o host do próprio request.
const forwardedProto = headerList.get("x-forwarded-proto")
const forwardedHost = headerList.get("x-forwarded-host") ?? headerList.get("host")
const requestOrigin = forwardedProto && forwardedHost ? `${forwardedProto}://${forwardedHost}` : undefined
const baseUrl = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_APP_URL || requestOrigin || "http://localhost:3000"
return new Request(baseUrl, {
headers: { headers: {
cookie: cookieHeader, cookie: cookieHeader,
"user-agent": userAgent, "user-agent": userAgent,

View file

@ -20,6 +20,9 @@ services:
NPM_CONFIG_PRODUCTION: "false" NPM_CONFIG_PRODUCTION: "false"
# Use service-to-service no overlay para o Convex # Use service-to-service no overlay para o Convex
NEXT_PUBLIC_CONVEX_URL: "http://sistema_convex_backend:3210" NEXT_PUBLIC_CONVEX_URL: "http://sistema_convex_backend:3210"
# URLs públicas do app (evita fallback para localhost)
NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}"
BETTER_AUTH_URL: "${BETTER_AUTH_URL}"
# Mantém o SQLite fora do repositório # Mantém o SQLite fora do repositório
DATABASE_URL: "file:/app/data/db.sqlite" DATABASE_URL: "file:/app/data/db.sqlite"
# Usado para forçar novo rollout a cada deploy (setado pelo CI) # Usado para forçar novo rollout a cada deploy (setado pelo CI)