feat: add frontend, nginx reverse proxy and monitoring/reports extras

- apps/frontend: Next.js 15 (App Router) + Tailwind v4 + componentes estilo
  shadcn/ui sobre Radix UI + TanStack Query. Tema light/dark, logo
  processada. Menu completo (secao 52) com gating por permissao real.
  Todas as telas do checklist de aceite (secao 90) conectadas a endpoints
  reais (nao mockup): login, usuarios, perfis/permissoes, ramais/troncos,
  dialplan, filas/agentes, console do agente, campanhas (CPS/CSV/
  iniciar/pausar), monitoramento ao vivo (polling, nao WebSocket real),
  TME/TMA, busca/export de chamadas, administracao do Asterisk, auditoria
- infrastructure/nginx: reverse proxy colocando frontend+API na mesma
  origem (porta 80), antecipado da Fase 9 pois a API nao publica porta
  propria
- apps/api: GET /api/monitoring/agents (estado corrente real via
  agent_state_events em aberto) e filtro queueId em GET /api/reports/calls

Pendencia registrada: tela de Callbacks nao implementada (schema existe
desde a Fase 6, mas nunca houve controller/service — construir a tela sem
API real seria mockup). Verificacao visual em navegador nao foi possivel
neste ambiente headless; validado via tsc/eslint/next build limpos + curl
reproduzindo as chamadas do navegador (middleware de auth, 24 paginas
protegidas via Nginx, endpoints de dados com cookie de sessao).
This commit is contained in:
2026-08-27 17:35:34 -03:00
parent 0a8b830e2c
commit 6273b32214
100 changed files with 12280 additions and 17 deletions

View File

@@ -0,0 +1,62 @@
'use client';
import * as React from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { authService } from '@/services/auth';
import { ApiError } from '@/lib/api-client';
import type { CurrentUser } from '@/types';
import type { Permission } from '@/lib/permissions';
import { hasPermission, hasAnyPermission } from '@/lib/permissions';
interface AuthContextValue {
user: CurrentUser | undefined;
isLoading: boolean;
isAuthenticated: boolean;
can: (permission: Permission | Permission[]) => boolean;
canAny: (permissions: Permission[]) => boolean;
refetch: () => void;
}
const AuthContext = React.createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const { data, isLoading, refetch } = useQuery({
queryKey: ['auth', 'me'],
queryFn: async () => {
try {
return await authService.me();
} catch (err) {
if (err instanceof ApiError && err.status === 401) return null;
throw err;
}
},
retry: false,
staleTime: 60_000,
});
const value: AuthContextValue = {
user: data ?? undefined,
isLoading,
isAuthenticated: Boolean(data),
can: (permission) => hasPermission(data?.permissions, permission),
canAny: (permissions) => hasAnyPermission(data?.permissions, permissions),
refetch,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = React.useContext(AuthContext);
if (!ctx) throw new Error('useAuth deve ser usado dentro de AuthProvider');
return ctx;
}
export function useLogout() {
const queryClient = useQueryClient();
return async () => {
await authService.logout();
queryClient.clear();
window.location.href = '/login';
};
}