@extends('adminlte::page') @section('title', $instalacao->nome) @section('content_header') @stop @section('content') @php $statusLabels = [ 'ativa' => 'Ativa', 'inativa' => 'Inativa', 'manutencao' => 'Manutenção', 'erro' => 'Erro', ]; $domainBase = config('gestao.domain_base', 'waygest.com.br'); $frontendUrl = 'https://'.$instalacao->slug.'.'.$domainBase; $apiTokenInstalacao = $instalacao->getConfiguracaoEnv('API_TOKEN'); $podeExecutarArtisanApi = auth()->check() && auth()->user()->can('gestao-write'); $podeSshTailLog = $podeExecutarArtisanApi && config('gestao.ssh_log_tail_enabled'); $modoSuspensaoEfetivo = $instalacao->modoSuspensaoEfetivo(); $modosSuspensaoEtiqueta = \App\Models\Instalacao::modosSuspensaoEtiqueta(); $sshTailMeta = data_get($instalacao->metadados, 'ssh_log_tail'); $sshTailMetaOk = is_array($sshTailMeta) && filled(trim((string) ($sshTailMeta['host'] ?? ''))) && filled(trim((string) ($sshTailMeta['user'] ?? ''))) && filled(trim((string) ($sshTailMeta['remote_deploy_path'] ?? ''))); $stripeDash = \App\Support\Billing\StripeDashboardLinks::baseUrl(); $urlStripeCustomer = \App\Support\Billing\StripeDashboardLinks::customerUrl($instalacao->psp_customer_id); $urlStripeSub = \App\Support\Billing\StripeDashboardLinks::subscriptionUrl($instalacao->psp_subscription_id); $cor1 = $instalacao->cor1 ?? '#6C63FF'; $cor2 = $instalacao->cor2 ?? '#2D3142'; $tenantCronUiEnabled = (bool) config('gestao.tenant_cron.enabled', false) && filled(trim((string) config('gestao.tenant_cron.api_base_path', ''))); $tenantCronMeta = data_get($instalacao->metadados, 'tenant_cron'); $tenantCronRegistado = is_array($tenantCronMeta) && ! empty($tenantCronMeta['registered_at']); @endphp
{{-- Hero --}}

Ficha comercial · Instalação #{{ $instalacao->id }}

{{ $instalacao->nome }}

Operação whitelabel · slug {{ $instalacao->slug }} · criada em {{ $instalacao->created_at->format('d/m/Y') }}

{{ $instalacao->slug }}.{{ $domainBase }} v{{ $instalacao->versao ?? '—' }} {{ $instalacao->plano_label ?: 'Sem plano' }} @if($instalacao->suspensa_comercialmente) Suspensa comercialmente @endif
{{ $statusLabels[$instalacao->status] ?? ucfirst($instalacao->status) }} Abrir frontend @can('gestao-write') Editar @if($podeExecutarArtisanApi) @endif @endcan Voltar
{{-- Flash messages --}} @if(session('success')) @endif @if(session('warning')) @endif @php $passosUi = session('criacao_passos') ?: ($passosProvisionamento ?? []); $temPassoCronErro = collect($passosUi)->contains(fn ($p) => ($p['id'] ?? '') === 'cron_tenant' && ($p['status'] ?? '') === 'erro'); $instalacaoRetomada = (($provisionamentoPendente ?? false) || ($cronPendente ?? false) || $temPassoCronErro) ? $instalacao : null; $validTabs = ['basico', 'psp', 'logs', 'crons', 'operacoes']; $activeTab = in_array((string) request('tab'), $validTabs, true) ? request('tab') : 'basico'; if (! request()->filled('tab')) { if (request()->has('logs_page') || request()->has('logs_per_page')) { $activeTab = 'logs'; } elseif (session('deploy_rotina_resultado') || session('artisan_output_resumo')) { $activeTab = 'operacoes'; } elseif (($cronPendente ?? false) || $temPassoCronErro || session('manual_cron')) { $activeTab = 'crons'; } elseif (session('ssh_tail_output_resumo')) { $activeTab = 'logs'; } } @endphp @if(session('error')) @if(session('manual_cname')) @php $cname = session('manual_cname'); @endphp

Adicionar CNAME manualmente no cPanel

No cPanel, vá em Domínios → Zone Editor (ou Editor de zona), selecione o domínio {{ $cname['zone'] }} e clique em Adicionar registro.

Tipo
CNAME
Nome
{{ $cname['name'] }}
Destino
{{ $cname['value'] }}

Isso fará {{ $cname['full_domain'] }} apontar para a Vercel. Após salvar, aguarde alguns minutos e clique em Refresh no domínio na Vercel.

@endif @endif {{-- KPIs rápidos --}}

{{ $statusLabels[$instalacao->status] ?? ucfirst($instalacao->status) }}

Status operacional

{{ $instalacao->versao ?? '—' }}

Versão do produto

{{ $instalacao->plano_label ?? '—' }}

Plano comercial

{{ $instalacao->suspensa_comercialmente ? 'Suspensa' : 'Activa' }}

Situação comercial

Billing overview Clientes com PSP / Stripe Custos extras Repasse IA e outros Eventos comerciais Churn, suspensões, notas Logs da instalação Erros e alertas recentes

Informações básicas

ID #{{ $instalacao->id }}
Status {{ $statusLabels[$instalacao->status] ?? ucfirst($instalacao->status) }}
Nome comercial {{ $instalacao->nome }}
Slug {{ $instalacao->slug }}
Subdomínio API {{ $instalacao->subdominio }}
Criado em {{ $instalacao->created_at->format('d/m/Y H:i') }}
Atualizado em {{ $instalacao->updated_at->format('d/m/Y H:i') }}

Identidade visual

{{ $cor1 }} {{ $cor2 }}

Banco de dados

Nome da base {{ $instalacao->database_name }}
Usuário {{ $instalacao->database_username }}
Senha ••••••••

Token de API

Usado como Authorization: Bearer com X-Instalacao-Id: {{ $instalacao->id }}. Corresponde a LOGS_GESTAO_TOKEN em env/.env.{{ $instalacao->slug }}.

@if($apiTokenInstalacao)
@can('gestao-write')
@csrf
@endcan @else

Nenhum API_TOKEN configurado.

@can('gestao-write')
@csrf
@endcan
@endif

Estado comercial (PSP / billing)

@if(($instalacao->psp_provider ?? '') === 'stripe') Stripe @endif

Sincronizado via webhooks quando configurado; metadata instalacao_id na subscrição.

PSP
{{ $instalacao->psp_provider ?? '—' }}
Customer ID
{{ $instalacao->psp_customer_id ? \Illuminate\Support\Str::limit($instalacao->psp_customer_id, 48) : '—' }}
Subscription ID
{{ $instalacao->psp_subscription_id ? \Illuminate\Support\Str::limit($instalacao->psp_subscription_id, 48) : '—' }}
Plano
{{ $instalacao->plano_label ?? '—' }}
Fim do período
{{ $instalacao->assinatura_periodo_fim ? $instalacao->assinatura_periodo_fim->format('d/m/Y') : '—' }}
Suspensa comercialmente
@if($instalacao->suspensa_comercialmente) Suspensa @else Activa @endif
Modo de suspensão
{{ $modosSuspensaoEtiqueta[$modoSuspensaoEfetivo] ?? $modoSuspensaoEfetivo }} @if($instalacao->modo_suspensao_comercial) (override nesta instalação) @else (default global: GESTAO_SUSPENSAO_DEFAULT_MODO) @endif
Sync tenant
@php $syncStatus = $instalacao->suspensao_comercial_sync_status; @endphp @if($syncStatus === 'ok') OK @elseif($syncStatus === 'pendente') Pendente @elseif($syncStatus === 'erro') Erro @else — @endif @if($instalacao->suspensao_comercial_synced_at) {{ $instalacao->suspensao_comercial_synced_at->format('d/m/Y H:i') }} @endif @if($instalacao->suspensao_comercial_sync_detail)
{{ $instalacao->suspensao_comercial_sync_detail }}
@endif @can('gestao-write')
@csrf
@endcan
@can('gestao-write')

Alterar estado comercial (manual)

Webhooks PSP só alteram a flag de suspensão — nunca o modo. Toda alteração manual exige motivo e gera evento em eventos comerciais.

@csrf
modo_suspensao_comercial === null ? '1' : '0') === '1')>
@error('motivo')
{{ $message }}
@enderror
@if($instalacao->suspensa_comercialmente)
@csrf
@else
@csrf
@endif
@endcan @if(($instalacao->psp_provider ?? '') === 'stripe')

Stripe (operador)

@if($stripeDash !== '') @if($urlStripeCustomer) Cliente @endif @if($urlStripeSub) Subscrição @endif @if(!$urlStripeCustomer && !$urlStripeSub) Sem customer/subscription ID sincronizado. @endif @else Defina GESTAO_STRIPE_DASHBOARD_BASE para atalhos ao Dashboard. @endif @can('gestao-write') @if(config('gestao.psp.stripe_secret_key') && $instalacao->psp_customer_id)
@csrf
@endif @endcan
@can('gestao-write') @if(!config('gestao.psp.stripe_secret_key'))

Portal: defina GESTAO_STRIPE_SECRET_KEY e configure o Customer Portal na Stripe.

@elseif(!$instalacao->psp_customer_id)

Portal: aguarde sincronização do psp_customer_id (webhook).

@endif @endcan
@endif @if(!empty($stripeInvoicesQueried))

Faturas recentes (Stripe)

@if(!empty($stripeInvoices['fetch_failed']))

Não foi possível carregar faturas. Verifique a chave e os logs.

@elseif(empty($stripeInvoices['invoices']))

Nenhuma fatura encontrada para este cliente.

@else
@foreach($stripeInvoices['invoices'] as $inv) @php $created = !empty($inv['created']) ? \Carbon\Carbon::createFromTimestamp((int) $inv['created']) : null; $cur = strtoupper((string) ($inv['currency'] ?? '')); $paid = isset($inv['amount_paid']) ? (int) $inv['amount_paid'] : 0; $paidFmt = $cur !== '' ? number_format($paid / 100, 2, ',', ' ').' '.$cur : (string) $paid; @endphp @endforeach
Fatura Data Estado Pago
{{ \Illuminate\Support\Str::limit((string) ($inv['number'] ?? $inv['id'] ?? ''), 32) }} {{ $created ? $created->format('d/m/Y H:i') : '—' }} {{ $inv['status'] ?? '—' }} {{ $paidFmt }} @if(!empty($inv['hosted_invoice_url'])) Ver @endif @if(!empty($inv['invoice_pdf'])) PDF @endif

Últimas {{ count($stripeInvoices['invoices']) }} faturas via API Stripe.

@endif
@endif
@if(session('ssh_tail_output_resumo') !== null && session('ssh_tail_output_resumo') !== '')

Saída SSH (tail laravel.log)

{{ session('ssh_tail_output_resumo') }}
@endif

Logs desta instalação

{{ $logsInstalacao->total() }} registro(s) Log Reader Atualizar
@if($logsInstalacao->count() > 0)
@foreach($logsInstalacao as $log) @endforeach
Data / hora Tipo Canal Mensagem
@php $dt = $log->timestamp ?? $log->created_at; @endphp {{ $dt ? $dt->format('d/m/Y H:i:s') : '—' }} {{ $log->tipo }} {{ \Illuminate\Support\Str::limit($log->canal ?? '—', 24) }} {{ \Illuminate\Support\Str::limit($log->mensagem_viewer, 120) }}
{{ $logsInstalacao->firstItem() }}–{{ $logsInstalacao->lastItem() }} de {{ $logsInstalacao->total() }}
Por página: @foreach([10, 20, 50] as $n) {{ $n }} @endforeach
{{ $logsInstalacao->appends(array_merge(['tab' => 'logs'], request()->only(['logs_per_page'])))->onEachSide(1)->links('pagination::bootstrap-4') }}
@else

Nenhum registro em LogInstalacao para esta instalação.

Os envios da API aparecem aqui quando LOGS_GESTAO_* estiver configurado.

@endif
{{-- SSH fallback --}} @if($podeSshTailLog)

Fallback — laravel.log no servidor (SSH)

Leitura das últimas linhas de storage/logs/laravel.log via SSH só leitura quando o envio para a API não estiver a funcionar.

@unless($sshTailMetaOk)
Configure metadados["ssh_log_tail"] com host, user e remote_deploy_path.
@endunless @can('gestao-write')
@csrf
@endcan
@endif
@include('instalacoes.partials.provisionamento-passos', [ 'passos' => $passosUi, 'instalacaoProvisionamento' => $instalacaoRetomada, 'mostrarDetalhes' => (bool) session('mostrar_detalhes_provisionamento', ($cronPendente ?? false)), ]) @php $manualCron = session('manual_cron') ?: (($cronPendente ?? false) ? ($comandosCronManual ?? null) : null); @endphp @if(is_array($manualCron) && filled($manualCron['schedule'] ?? null))

Cron manual (cPanel → Cron Jobs)

Crie duas tarefas cron com periodicidade * * * * * (cada minuto) e cole cada comando no campo Command.

1. schedule:run (tenant {{ $instalacao->slug }})

{{ $manualCron['schedule'] }}

2. queue:work (fila Redis {{ $instalacao->slug }})

{{ $manualCron['worker'] }}
@endif @if($tenantCronUiEnabled) @can('gestao-write')

Cron tenant (API)

Regista no cPanel schedule:run e queue:work com --env={{ $instalacao->slug }} e fila Redis {{ $instalacao->slug }} (padrão produção Waygest).

@if($tenantCronRegistado)
Cron registrado em {{ \Illuminate\Support\Carbon::parse($tenantCronMeta['registered_at'])->format('d/m/Y H:i') }}.
@elseif($cronPendente ?? false)
Cron ainda não registrado para este tenant. Os demais passos do provisionamento podem já estar OK — use Repetir só cron no painel acima ou o botão abaixo (não reexecuta banco nem Vercel).
@endif
@csrf
@csrf

Base API: {{ config('gestao.tenant_cron.api_base_path') }} · PHP: {{ config('gestao.tenant_cron.php_binary') ?: (config('gestao.deploy_homologacao.php_binary') ?: '/usr/local/bin/ea-php83') }}

@endcan @endif
@if(session('artisan_output_resumo'))

Resumo da saída (artisan)

{{ session('artisan_output_resumo') }}
@endif @if(session('deploy_rotina_resultado')) @php $rotina = session('deploy_rotina_resultado'); @endphp

@if(($rotina['profile'] ?? '') === 'atualizar_api' || ($rotina['profile'] ?? '') === 'api_unificada') Atualizar API @else Rotina pós-deploy @endif @if($rotina['ok'] ?? false) OK @else Com erros @endif

Perfil {{ $rotina['profile'] ?? '—' }} · --env={{ $rotina['env'] ?? '—' }} @if(!empty($rotina['base_path'])) · pasta {{ $rotina['base_path'] }} @endif

@foreach($rotina['steps'] ?? [] as $step)
php artisan {{ $step['command'] ?? '?' }} --env={{ $rotina['env'] ?? '?' }} @if($step['ok'] ?? false) — OK @else — Erro (exit {{ $step['exit_code'] ?? '?' }}) @endif
{{ $step['output'] ?? '' }}
@endforeach
@endif {{-- Migrations / Seeder / Rotina pós-deploy --}} @if($podeExecutarArtisanApi) @can('gestao-write') @php $deployHomologConfig = config('gestao.deploy_homologacao', []); $deployProdConfig = config('gestao.deploy_producao', []); $atualizarApiConfig = config('gestao.atualizar_api', []); @endphp

Atualizar API (este tenant)

Executa cache clear, migrations do core e addons:migrate-installed na API compartilhada com --env={{ $instalacao->slug }} (só muda o env do tenant), sem git pull. Pasta: API_PATH ou GESTAO_API_CRON_BASE_PATH (ex.: /home/waygest/api.waygest.com.br). Equivalente ao terminal no servidor cPanel (PHP 8.3).

    @foreach($atualizarApiConfig['artisan_commands'] ?? [] as $cmd)
  • php artisan {{ $cmd }} --env={{ $instalacao->slug }}
  • @endforeach

Em lote (vários tenants em sequência): Atualizar APIs (tenants) · Homologação (pasta dedicada): Atualizar API Homologação

Rotina pós-deploy (este tenant)

Executa a sequência de cache/clear e migrations na API desta instalação, sem git pull. Equivalente ao terminal, com saída por comando abaixo.

Homologação (--env={{ $deployHomologConfig['artisan_env'] ?? 'homologacao' }})

    @foreach($deployHomologConfig['artisan_commands'] ?? [] as $cmd)
  • php artisan {{ $cmd }}
  • @endforeach

Produção (--env={{ $deployProdConfig['artisan_env'] ?? 'producao' }})

    @foreach($deployProdConfig['artisan_commands'] ?? [] as $cmd)
  • php artisan {{ $cmd }}
  • @endforeach

Deploy em lote (git pull + rotina por tenant): Homologação · Produção · Atualizar APIs (cache/migrate/addons por slug): tenants em sequência · API Homologação

Migrations

Executa na API, com --env={{ $instalacao->slug }}: optimize:clear, config:clear, cache:clear, config:cache, route:clear e migrate --force.

Seeder de produção

Executa db:seed --force com a classe em GESTAO_INSTALACAO_SEED_CLASS. Pode alterar dados existentes.

@endcan @elseif(!auth()->user()?->can('gestao-write'))

Ações de migrations/seeder/deploy exigem papel super_admin (permissão gestao-write).

@endif @can('gestao-write')

Operações

Editar instalação @if(!$apiTokenInstalacao)
@csrf
@endif
@csrf
@csrf
@csrf @method('DELETE')
@endcan
@if($podeExecutarArtisanApi) @can('gestao-write') @php $deployHomologConfig = $deployHomologConfig ?? config('gestao.deploy_homologacao', []); $deployProdConfig = $deployProdConfig ?? config('gestao.deploy_producao', []); @endphp @endcan @endif
@stop @section('css') @stop @section('js') @if($apiTokenInstalacao) @endif @stop