-
-
-
+
+
+
-
+
- Nenhum log capturado ainda. Os eventos da agenda aparecerão aqui.
+ Nenhum resultado para "{{ searchQuery }}"
+ Nenhum log capturado ainda. Os eventos aparecerão aqui.
+
{{ formatTime(log.timestamp) }}
{{ log.level }}
[{{ log.source }}]
-
{{ log.message }}
+
+ >{ }
-
+
+
+
+
+
+
@@ -139,13 +225,14 @@ function formatTime (iso) {
font-size: 12px;
}
+/* ── Barra ───────────────────────────────────────────────── */
.support-banner__bar {
display: flex;
align-items: center;
justify-content: space-between;
- background: #b45309;
+ background: #92400e;
color: #fff;
- padding: 6px 16px;
+ padding: 5px 14px;
gap: 12px;
}
@@ -155,38 +242,51 @@ function formatTime (iso) {
gap: 10px;
font-weight: 600;
letter-spacing: 0.05em;
+ min-width: 0;
+ overflow: hidden;
}
.support-banner__bar-right {
display: flex;
align-items: center;
- gap: 6px;
+ gap: 5px;
+ flex-shrink: 0;
}
.support-banner__pulse {
display: inline-block;
- width: 8px;
- height: 8px;
+ width: 7px;
+ height: 7px;
border-radius: 50%;
background: #fcd34d;
+ flex-shrink: 0;
animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
- 50% { opacity: 0.5; transform: scale(1.3); }
+ 50% { opacity: 0.5; transform: scale(1.4); }
}
.support-banner__tenant {
font-weight: 400;
- opacity: 0.75;
+ opacity: 0.7;
font-size: 11px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 260px;
}
-.support-banner__toggle,
-.support-banner__clear,
-.support-banner__close {
- background: rgba(255,255,255,0.15);
+.support-banner__count {
+ font-weight: 400;
+ opacity: 0.55;
+ font-size: 10px;
+ flex-shrink: 0;
+}
+
+.support-banner__toggle {
+ background: rgba(255,255,255,0.14);
border: none;
color: #fff;
cursor: pointer;
@@ -198,36 +298,56 @@ function formatTime (iso) {
gap: 5px;
transition: background 0.15s;
}
-.support-banner__toggle:hover,
-.support-banner__clear:hover,
-.support-banner__close:hover {
- background: rgba(255,255,255,0.28);
+.support-banner__toggle:hover { background: rgba(255,255,255,0.26); }
+
+.support-banner__icon-btn {
+ width: 26px;
+ height: 26px;
+ background: rgba(255,255,255,0.1);
+ border: none;
+ color: #fff;
+ cursor: pointer;
+ border-radius: 4px;
+ display: grid;
+ place-items: center;
+ transition: background 0.15s;
}
+.support-banner__icon-btn:hover { background: rgba(255,255,255,0.25); }
.support-banner__err-badge {
background: #ef4444;
color: #fff;
border-radius: 10px;
- padding: 0 7px;
+ padding: 0 6px;
font-size: 10px;
font-weight: 700;
}
-/* Painel */
+/* ── Painel ──────────────────────────────────────────────── */
.support-banner__panel {
background: #0f172a;
- border-top: 2px solid #b45309;
+ border-top: 2px solid #92400e;
display: flex;
flex-direction: column;
- max-height: 360px;
+ max-height: 380px;
+}
+
+/* ── Toolbar ─────────────────────────────────────────────── */
+.support-banner__toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 10px;
+ border-bottom: 1px solid #1e293b;
+ flex-shrink: 0;
+ flex-wrap: wrap;
}
.support-banner__filters {
display: flex;
- gap: 6px;
- padding: 8px 12px;
- border-bottom: 1px solid #1e293b;
- flex-shrink: 0;
+ gap: 4px;
+ flex-wrap: wrap;
+ flex: 1;
}
.support-banner__filter-btn {
@@ -235,61 +355,116 @@ function formatTime (iso) {
border: 1px solid #334155;
color: #94a3b8;
border-radius: 4px;
- padding: 2px 10px;
+ padding: 2px 9px;
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
- gap: 5px;
- transition: all 0.15s;
+ gap: 4px;
+ transition: all 0.12s;
+ white-space: nowrap;
}
.support-banner__filter-btn--active {
- background: #b45309;
- border-color: #b45309;
+ background: #92400e;
+ border-color: #92400e;
color: #fff;
}
.support-banner__filter-count {
- background: rgba(255,255,255,0.15);
+ background: rgba(255,255,255,0.13);
border-radius: 8px;
- padding: 0 5px;
+ padding: 0 4px;
font-size: 10px;
}
+/* ── Search ──────────────────────────────────────────────── */
+.support-banner__search-wrap {
+ position: relative;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+}
+
+.support-banner__search-icon {
+ position: absolute;
+ left: 7px;
+ color: #475569;
+ font-size: 11px;
+ pointer-events: none;
+}
+
+.support-banner__search {
+ background: #1e293b;
+ border: 1px solid #334155;
+ border-radius: 4px;
+ color: #e2e8f0;
+ font-size: 11px;
+ font-family: inherit;
+ padding: 3px 24px 3px 24px;
+ width: 180px;
+ outline: none;
+ transition: border-color 0.15s;
+}
+.support-banner__search:focus { border-color: #92400e; }
+.support-banner__search::placeholder { color: #475569; }
+
+.support-banner__search-clear {
+ position: absolute;
+ right: 5px;
+ background: none;
+ border: none;
+ color: #475569;
+ cursor: pointer;
+ font-size: 10px;
+ display: grid;
+ place-items: center;
+ padding: 2px;
+}
+.support-banner__search-clear:hover { color: #94a3b8; }
+
+/* ── Logs list ───────────────────────────────────────────── */
.support-banner__logs {
overflow-y: auto;
flex: 1;
- padding: 4px 0;
+ padding: 2px 0;
+ scrollbar-width: thin;
+ scrollbar-color: #1e293b transparent;
}
.support-banner__empty {
color: #475569;
text-align: center;
padding: 20px;
+ font-style: italic;
}
.support-banner__log-entry {
display: flex;
flex-wrap: wrap;
align-items: baseline;
- gap: 6px;
- padding: 3px 12px;
- border-bottom: 1px solid #1e293b;
+ gap: 5px;
+ padding: 2px 12px;
+ border-bottom: 1px solid #0f1829;
transition: background 0.1s;
}
.support-banner__log-entry:hover { background: #1e293b; }
-.support-banner__log-entry--error { border-left: 3px solid #ef4444; }
-.support-banner__log-entry--api { border-left: 3px solid #3b82f6; }
+.support-banner__log-entry--error { border-left: 3px solid #ef4444; }
+.support-banner__log-entry--api { border-left: 3px solid #3b82f6; }
.support-banner__log-entry--recurrence { border-left: 3px solid #8b5cf6; }
-.support-banner__log-entry--guard { border-left: 3px solid #10b981; }
-.support-banner__log-entry--perf { border-left: 3px solid #f59e0b; }
-.support-banner__log-entry--event { border-left: 3px solid #64748b; }
+.support-banner__log-entry--guard { border-left: 3px solid #10b981; }
+.support-banner__log-entry--perf { border-left: 3px solid #f59e0b; }
+.support-banner__log-entry--tenant { border-left: 3px solid #06b6d4; }
+.support-banner__log-entry--menu { border-left: 3px solid #84cc16; }
+.support-banner__log-entry--profile { border-left: 3px solid #ec4899; }
+.support-banner__log-entry--auth { border-left: 3px solid #f97316; }
+.support-banner__log-entry--agenda { border-left: 3px solid #a78bfa; }
+.support-banner__log-entry--event { border-left: 3px solid #475569; }
-.support-banner__log-time { color: #475569; font-size: 10px; flex-shrink: 0; }
+.support-banner__log-time { color: #334155; font-size: 10px; flex-shrink: 0; }
.support-banner__log-level { color: #f59e0b; font-size: 10px; font-weight: 700; text-transform: uppercase; flex-shrink: 0; }
.support-banner__log-source { color: #7c3aed; font-size: 10px; flex-shrink: 0; }
-.support-banner__log-msg { color: #e2e8f0; flex: 1; }
+.support-banner__log-msg { color: #cbd5e1; flex: 1; }
.support-banner__log-expand {
background: #1e293b;
@@ -300,16 +475,17 @@ function formatTime (iso) {
cursor: pointer;
font-size: 10px;
font-family: monospace;
+ flex-shrink: 0;
}
.support-banner__log-expand:hover { color: #e2e8f0; }
.support-banner__log-data {
width: 100%;
- margin: 4px 0 0;
- background: #0f172a;
+ margin: 3px 0 2px;
+ background: #020617;
border: 1px solid #1e293b;
color: #94a3b8;
- padding: 8px;
+ padding: 7px;
border-radius: 4px;
font-size: 10px;
overflow-x: auto;
@@ -317,17 +493,26 @@ function formatTime (iso) {
word-break: break-all;
}
+/* mark de busca */
+:deep(.sbh) {
+ background: rgba(250, 204, 21, 0.3);
+ color: #fde68a;
+ border-radius: 2px;
+ padding: 0 1px;
+}
+
+/* ── Footer ──────────────────────────────────────────────── */
.support-banner__footer {
display: flex;
gap: 8px;
padding: 4px 12px;
- color: #475569;
+ color: #334155;
font-size: 10px;
border-top: 1px solid #1e293b;
flex-shrink: 0;
}
-/* Transition */
+/* ── Transition ──────────────────────────────────────────── */
.support-slide-enter-active,
.support-slide-leave-active { transition: transform 0.25s ease; }
.support-slide-enter-from,
diff --git a/src/support/supportLogger.js b/src/support/supportLogger.js
index d9a3a17..b3d1cea 100644
--- a/src/support/supportLogger.js
+++ b/src/support/supportLogger.js
@@ -11,8 +11,8 @@
* Usar sempre este módulo para logs de diagnóstico.
*
* Uso:
- * import { logEvent, logAPI, logError, logRecurrence } from '@/support/supportLogger'
- * logEvent('useRecurrence', 'loadRules', { ownerId, startISO })
+ * import { logEvent, logAPI, logError, logTenant } from '@/support/supportLogger'
+ * logTenant('loadSessionAndTenant', { tenantId, role })
*/
import { useSupportDebugStore } from './supportDebugStore'
@@ -26,6 +26,11 @@ export const LOG_LEVEL = {
RECURRENCE: 'recurrence',
GUARD: 'guard',
PERF: 'perf',
+ TENANT: 'tenant',
+ MENU: 'menu',
+ PROFILE: 'profile',
+ AUTH: 'auth',
+ AGENDA: 'agenda',
}
// ─── Função base ─────────────────────────────────────────────────────────────
@@ -47,7 +52,6 @@ function _log (level, source, message, data = null) {
store.addLog(entry)
- // Agrupa no console para não poluir — só visível quando debug ativo
const prefix = `[${level.toUpperCase()}][${source}]`
if (level === LOG_LEVEL.ERROR) {
console.error(prefix, message, data ?? '')
@@ -58,24 +62,17 @@ function _log (level, source, message, data = null) {
// ─── API pública ─────────────────────────────────────────────────────────────
-/**
- * Log de evento geral (lifecycle, state changes)
- * Substitui console.log genérico dos composables
- */
+/** Log de evento geral (lifecycle, state changes) */
export function logEvent (source, message, data = null) {
_log(LOG_LEVEL.EVENT, source, message, data)
}
-/**
- * Log de chamada de API (Supabase queries)
- */
+/** Log de chamada de API (Supabase queries) */
export function logAPI (source, message, data = null) {
_log(LOG_LEVEL.API, source, message, data)
}
-/**
- * Log de erro capturado
- */
+/** Log de erro capturado */
export function logError (source, message, error = null) {
const data = error
? { message: error?.message, code: error?.code, details: error?.details }
@@ -83,25 +80,48 @@ export function logError (source, message, error = null) {
_log(LOG_LEVEL.ERROR, source, message, data)
}
-/**
- * Log específico do sistema de recorrência
- * Substitui os console.log de useRecurrence
- */
+/** Log específico do sistema de recorrência */
export function logRecurrence (message, data = null) {
_log(LOG_LEVEL.RECURRENCE, 'useRecurrence', message, data)
}
-/**
- * Log de navegação/guard do router
- * Substitui console.time/timeLog/timeEnd de guards.js
- */
+/** Log de navegação/guard do router */
export function logGuard (message, data = null) {
_log(LOG_LEVEL.GUARD, 'router.guard', message, data)
}
+/** Log de tenant: carregamento de sessão, troca de role, tenant ativo */
+export function logTenant (source, message, data = null) {
+ _log(LOG_LEVEL.TENANT, source, message, data)
+}
+
+/** Log do sistema de menu: build, reset, model changes */
+export function logMenu (message, data = null) {
+ _log(LOG_LEVEL.MENU, 'menuStore', message, data)
+}
+
+/** Log da página de perfil: carregamento, salvamento de settings */
+export function logProfile (message, data = null) {
+ _log(LOG_LEVEL.PROFILE, 'ProfilePage', message, data)
+}
+
+/** Log de autenticação: login, logout, refresh, MFA */
+export function logAuth (message, data = null) {
+ _log(LOG_LEVEL.AUTH, 'auth', message, data)
+}
+
+/** Log da agenda: eventos, slots, appointments, recurrence */
+export function logAgenda (source, message, data = null) {
+ _log(LOG_LEVEL.AGENDA, source, message, data)
+}
+
/**
- * Log de performance (substitui console.time)
- * Retorna uma função que finaliza a medição
+ * Log de performance — retorna função que finaliza a medição.
+ *
+ * Uso:
+ * const end = logPerf('useAgenda', 'loadEvents')
+ * ...await work...
+ * end({ count: events.length })
*/
export function logPerf (source, label) {
let store
diff --git a/src/support/supportSessionService.js b/src/support/supportSessionService.js
index 3cb2324..e80b57f 100644
--- a/src/support/supportSessionService.js
+++ b/src/support/supportSessionService.js
@@ -6,52 +6,112 @@
* Usado apenas pelo painel do admin — nunca pelo terapeuta/paciente.
*
* Fluxo:
- * 1. Admin seleciona tenant
- * 2. createSession(tenantId) → { token, expires_at }
+ * 1. Admin seleciona tenant + TTL + nota opcional
+ * 2. createSupportSession(tenantId, ttlMinutes, note) → { token, expires_at }
* 3. Admin recebe URL pronta para copiar
- * 4. Admin pode listar sessões ativas e revogar
+ * 4. Admin pode listar sessões ativas, histórico e revogar
*/
import { supabase } from '@/lib/supabase/client'
+const TAG = '[supportSessionService]'
+
/**
* Cria uma sessão de suporte para um tenant.
* Requer: usuário autenticado com role saas_admin (validado no RPC).
*
- * @param {string} tenantId - UUID do tenant a ser depurado
- * @param {number} ttlMinutes - TTL em minutos (1–120, default 60)
+ * @param {string} tenantId - UUID do tenant a ser depurado
+ * @param {number} ttlMinutes - TTL em minutos (1–120, default 60)
+ * @param {string} [note] - Nota opcional sobre o motivo do suporte
* @returns {{ token: string, expires_at: string, session_id: string }}
*/
-export async function createSupportSession (tenantId, ttlMinutes = 60) {
+export async function createSupportSession (tenantId, ttlMinutes = 60, note = '') {
if (!tenantId) throw new Error('tenant_id é obrigatório.')
+ console.log(`${TAG} createSupportSession`, { tenantId, ttlMinutes, note: note || '(sem nota)' })
+
const { data, error } = await supabase
.rpc('create_support_session', {
p_tenant_id: tenantId,
p_ttl_minutes: ttlMinutes,
})
- if (error) throw error
+ if (error) {
+ console.error(`${TAG} createSupportSession ERRO`, error)
+ throw error
+ }
if (!data?.token) throw new Error('Resposta inválida do servidor.')
+ // Salva nota localmente associada ao session_id (banco não tem coluna note)
+ if (note?.trim()) {
+ try {
+ const notes = JSON.parse(sessionStorage.getItem('support_notes') || '{}')
+ notes[data.session_id || data.token] = note.trim()
+ sessionStorage.setItem('support_notes', JSON.stringify(notes))
+ } catch {}
+ }
+
+ console.log(`${TAG} sessão criada`, { token: `${data.token.slice(0, 8)}…`, expires_at: data.expires_at })
return data
}
/**
- * Lista sessões de suporte ativas do admin logado.
- * Retorna somente sessões não expiradas.
+ * Lista sessões de suporte ATIVAS (não expiradas).
*
* @returns {Array}
*/
export async function listActiveSupportSessions () {
+ console.log(`${TAG} listActiveSupportSessions`)
+
const { data, error } = await supabase
.from('support_sessions')
.select('id, tenant_id, token, expires_at, created_at')
.gt('expires_at', new Date().toISOString())
.order('created_at', { ascending: false })
- if (error) throw error
- return data || []
+ if (error) {
+ console.error(`${TAG} listActiveSupportSessions ERRO`, error)
+ throw error
+ }
+
+ const sessions = data || []
+ console.log(`${TAG} ${sessions.length} sessão(ões) ativa(s)`)
+
+ // Enriquece com notas salvas localmente
+ const notes = _loadNotes()
+ return sessions.map(s => ({ ...s, _note: notes[s.id] || notes[s.token] || '' }))
+}
+
+/**
+ * Lista histórico de sessões (ativas + expiradas), limitado às últimas N.
+ *
+ * @param {number} limit - quantidade máxima (default 50)
+ * @returns {Array}
+ */
+export async function listSessionHistory (limit = 50) {
+ console.log(`${TAG} listSessionHistory limit=${limit}`)
+
+ const { data, error } = await supabase
+ .from('support_sessions')
+ .select('id, tenant_id, token, expires_at, created_at')
+ .order('created_at', { ascending: false })
+ .limit(limit)
+
+ if (error) {
+ console.error(`${TAG} listSessionHistory ERRO`, error)
+ throw error
+ }
+
+ const sessions = data || []
+ console.log(`${TAG} histórico: ${sessions.length} sessão(ões)`)
+
+ const notes = _loadNotes()
+ const now = new Date()
+ return sessions.map(s => ({
+ ...s,
+ _note: notes[s.id] || notes[s.token] || '',
+ _expired: new Date(s.expires_at) < now,
+ }))
}
/**
@@ -63,10 +123,17 @@ export async function listActiveSupportSessions () {
export async function revokeSupportSession (token) {
if (!token) throw new Error('Token é obrigatório.')
+ console.log(`${TAG} revokeSupportSession token=${token.slice(0, 8)}…`)
+
const { data, error } = await supabase
.rpc('revoke_support_session', { p_token: token })
- if (error) throw error
+ if (error) {
+ console.error(`${TAG} revokeSupportSession ERRO`, error)
+ throw error
+ }
+
+ console.log(`${TAG} sessão revogada:`, !!data)
return !!data
}
@@ -81,3 +148,13 @@ export function buildSupportUrl (token, basePath = '/therapist/agenda') {
const origin = window.location.origin
return `${origin}${basePath}?support=${token}`
}
+
+// ─── Helpers internos ─────────────────────────────────────────────────────────
+
+function _loadNotes () {
+ try {
+ return JSON.parse(sessionStorage.getItem('support_notes') || '{}')
+ } catch {
+ return {}
+ }
+}
diff --git a/src/views/pages/saas/SaasSupportPage.vue b/src/views/pages/saas/SaasSupportPage.vue
index 901e5be..b38649a 100644
--- a/src/views/pages/saas/SaasSupportPage.vue
+++ b/src/views/pages/saas/SaasSupportPage.vue
@@ -1,237 +1,60 @@
-
-
-
-
-
-
-
-
-
-
-
Suporte Técnico
-
Gere links seguros para acessar a agenda de um cliente em modo debug
-
-
-
-
-
-
-
-
- Nova Sessão de Suporte
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- URL de Suporte Gerada
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Expira em: {{ expiresLabel }}
-
-
-
-
-
- {{ tokenPreview }}
-
-
-
-
- Envie este link ao terapeuta ou acesse diretamente para ver os logs da agenda.
- O link expira automaticamente.
-
-
-
-
-
- Nenhuma sessão gerada ainda
-
-
-
-
-
-
-
-
-
- Sessões Ativas
-
-
-
-
-
-
-
- {{ data.tenant_id }}
-
-
-
-
-
- {{ data.token.slice(0, 16) }}…
-
-
-
-
-
-
- {{ formatExpires(data.expires_at) }}
-
-
-
-
-
-
- {{ formatDate(data.created_at) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
Suporte Técnico
+
Gere e gerencie links seguros de acesso em modo debug
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configurar acesso de suporte
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ URL Gerada
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Expira em:
+ {{ expiresLabel }}
+
+
+
+
+ {{ tokenPreview }}
+
+
+
+
+ {{ sessionNote }}
+
+
+
+ Envie este link ao terapeuta ou acesse diretamente para monitorar os logs da agenda em tempo real.
+
+
+
+
+
+ Nenhuma sessão gerada ainda
+
+
+
+
+
+
+
+
+
+ Sessões Ativas
+
+
+
+
+
+
+
+
+ Sessões em vigor
+
+
+
+
+
+
+
+
+ {{ tenantName(data.tenant_id) }}
+ {{ data.tenant_id }}
+
+
+
+
+
+
+ {{ data.token.slice(0, 12) }}…
+
+
+
+
+
+
+ {{ remainingLabel(data.expires_at) }}
+
+
+
+
+
+
+ {{ formatDate(data.created_at) }}
+
+
+
+
+
+ {{ data._note }}
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Últimas 100 sessões
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tenantName(data.tenant_id) }}
+ {{ data.tenant_id }}
+
+
+
+
+
+
+ {{ data.token.slice(0, 12) }}…
+
+
+
+
+
+ {{ formatDate(data.created_at) }}
+
+
+
+
+
+ {{ formatDate(data.expires_at) }}
+
+
+
+
+
+ {{ data._note }}
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+