403Webshell
Server IP : 54.37.205.81  /  Your IP : 216.73.216.76
Web Server : nginx/1.22.1
System : Linux vps-249481fa 6.1.0-50-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.176-1 (2026-07-02) x86_64
User : debian ( 1000)
PHP Version : 8.2.32
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/app.copyrightchain.it/copyrightchain_app/components/AdminDashboard/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/app.copyrightchain.it/copyrightchain_app/components/AdminDashboard/index.tsx
import React, { useEffect, useMemo, useState } from 'react';
import {
  Activity, CreditCard, KeyRound, Loader2, Rocket, Save, Settings, Share2, Sparkles, Image as ImageIcon, Copy, RefreshCw, Users, Search, Filter, PlusCircle
} from 'lucide-react';
import type { Certificate, Language, AppUser } from '../../types';
import { useNotification } from '../../App';
import { usePersistedState } from '../../hooks/usePersistedState';
import { apiFetch } from '../../services/apiClient';
import { aiGenerateImage, aiGenerateText, aiHealth, aiSetKeys, type AiProvider } from '../../services/aiGatewayService';

interface AdminDashboardProps {
  certificates: Certificate[];
  lang: Language;
  users: AppUser[];
  onUpdateUser: (user: AppUser) => void;
}

type AdminTab = 'stats' | 'growth' | 'systems';

type TowerUser = {
  id: string;
  email: string;
  name: string;
  token: string;
  wallet: string;
  plan: string;
  balance_abo: number;
  cert_count: number;
  created_at: string;
  updated_at: string;
};


const LS_AI_ADMIN_TOKEN = 'ccp_ai_admin_token';

export default function AdminDashboard(props: AdminDashboardProps) {
  const { notify } = useNotification();
  const [adminTab, setAdminTab] = useState<AdminTab>('stats');

  // --- GROWTH ---
  const [mktType, setMktType] = useState<'Post'|'Email'|'Blog'|'Ad'>('Post');
  const [mktChannel, setMktChannel] = useState('LinkedIn');
  const [mktAspectRatio, setMktAspectRatio] = useState('1:1');
  const [mktContext, setMktContext] = useState('');
  const [textProvider, setTextProvider] = useState<AiProvider>('openai');
  const [imageProvider, setImageProvider] = useState<AiProvider>('gemini');

  const [isGenText, setIsGenText] = useState(false);
  const [isGenVisual, setIsGenVisual] = useState(false);
  const [genContent, setGenContent] = useState('');
  const [genVisual, setGenVisual] = useState('');

  // --- SYSTEMS ---
  const [aiAdminToken, setAiAdminToken] = useState(localStorage.getItem(LS_AI_ADMIN_TOKEN) || '');
  const [openaiKeyDraft, setOpenaiKeyDraft] = useState('');
  const [geminiKeyDraft, setGeminiKeyDraft] = useState('');
  const [aiStatus, setAiStatus] = useState<any>(null);
  const [isSavingKeys, setIsSavingKeys] = useState(false);
  const [isTestingAi, setIsTestingAi] = useState(false);
  const [towerUsers, setTowerUsers] = useState<TowerUser[]>([]);
  const [towerUsersLoading, setTowerUsersLoading] = useState(false);
  const [userSearch, setUserSearch] = useState('');
  const [userPlanFilter, setUserPlanFilter] = useState('');
  const [hideDirtyUsers, setHideDirtyUsers] = useState(true);
  const [selectedUserForCerts, setSelectedUserForCerts] = useState<any | null>(null);
  const [selectedUserCerts, setSelectedUserCerts] = useState<any[]>([]);
  const [selectedUserCertsLoading, setSelectedUserCertsLoading] = useState(false);

  const onCopy = async (t: string) => {
    try { await navigator.clipboard.writeText(t); notify('Copiato', 'success'); } catch { notify('Copia non riuscita', 'error'); }
  };

  async function refreshAiHealth() {
    try {
      const j = await aiHealth();
      setAiStatus(j);
    } catch {
      setAiStatus({ ok:false });
    }
  }

  useEffect(() => {
    refreshAiHealth();
  }, []);

  const promptBase = useMemo(() => {
    const langLabel = props.lang === 'it' ? 'Italiano' : 'English';
    return `Tipo: ${mktType}\nCanale: ${mktChannel}\nLingua: ${langLabel}\nContesto: ${mktContext}`.trim();
  }, [mktType, mktChannel, props.lang, mktContext]);

  async function handleGenerateCampaign() {
    if (!aiAdminToken) return notify('Imposta il token AI in Systems', 'error');
    setIsGenText(true);
    try {
      const r = await aiGenerateText(aiAdminToken, textProvider, promptBase + "\n\nGenera copy marketing pronto da pubblicare, con CTA e 3 varianti.");
      if (!r.ok) throw new Error(r.error || 'AI error');
      setGenContent(r.text || '');
    } catch (e:any) {
      notify(e?.message || 'Errore generazione testo', 'error');
    } finally {
      setIsGenText(false);
    }
  }

  async function handleGenerateVisual() {
    if (!aiAdminToken) return notify('Imposta il token AI in Systems', 'error');
    setIsGenVisual(true);
    try {
      const r = await aiGenerateImage(aiAdminToken, imageProvider, `Crea un visual marketing professionale. ${promptBase}`, mktAspectRatio);
      if (!r.ok) throw new Error(r.error || 'AI error');
      setGenVisual(r.data_url || '');
    } catch (e:any) {
      notify(e?.message || 'Errore generazione immagine', 'error');
    } finally {
      setIsGenVisual(false);
    }
  }

  async function handleSaveAiKeys() {
    if (!aiAdminToken) return notify('Token AI mancante', 'error');
    setIsSavingKeys(true);
    try {
      localStorage.setItem(LS_AI_ADMIN_TOKEN, aiAdminToken);
      const j = await aiSetKeys(aiAdminToken, openaiKeyDraft || undefined, geminiKeyDraft || undefined);
      if (!j?.ok) throw new Error(j?.error || 'Save keys failed');
      setOpenaiKeyDraft('');
      setGeminiKeyDraft('');
      await refreshAiHealth();
      notify('Chiavi salvate nel gateway', 'success');
    } catch (e:any) {
      notify(e?.message || 'Errore salvataggio', 'error');
    } finally {
      setIsSavingKeys(false);
    }
  }

  async function handleTestAi() {
    if (!aiAdminToken) return notify('Token AI mancante', 'error');
    setIsTestingAi(true);
    try {
      const r = await aiGenerateText(aiAdminToken, 'openai', 'Rispondi solo con OK.');
      if (!r.ok) throw new Error(r.error || 'Test failed');
      notify('AI OK', 'success');
      await refreshAiHealth();
    } catch (e:any) {
      notify(e?.message || 'Test AI fallito', 'error');
    } finally {
      setIsTestingAi(false);
    }
  }

  async function loadTowerUsers() {
    const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
    if (!tokenToUse) {
      notify('Imposta prima il token admin in Systems', 'error');
      return;
    }

    setTowerUsersLoading(true);
    try {
      const res = await fetch('https://app.copyrightchain.it/api/ai/tower/users', {
        method: 'GET',
        headers: {
          'X-CCP-Token': tokenToUse,
        },
      });

      const text = await res.text();
      const json = text ? JSON.parse(text) : null;

      console.log('TOWER USERS RESPONSE', json);

      if (!res.ok || !json?.ok) {
        throw new Error(json?.error || 'Errore caricamento utenti Tower');
      }

      const users = Array.isArray(json.users) ? json.users : [];
      setTowerUsers(users);
    } catch (e: any) {
      console.error('LOAD TOWER USERS ERROR', e);
      notify(e?.message || 'Errore caricamento utenti Tower', 'error');
      setTowerUsers([]);
    } finally {
      setTowerUsersLoading(false);
    }
  }

  async function handleCreditUser(userId: string, userName: string) {
    const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
    if (!tokenToUse) {
      notify('Imposta prima il token admin in Systems', 'error');
      return;
    }

    const amountRaw = window.prompt(`Importo ABO da caricare per ${userName || userId}`, '1');
    if (amountRaw === null) return;

    const amount = Number(String(amountRaw).replace(',', '.'));
    if (!Number.isFinite(amount) || amount === 0) {
      notify('Importo non valido', 'error');
      return;
    }

    const note = window.prompt('Causale operazione', `Ricarica manuale Tower per ${userName || userId}`) || '';

    try {
      const res = await fetch('https://app.copyrightchain.it/api/ai/tower/users/credit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CCP-Token': tokenToUse,
        },
        body: JSON.stringify({
          user_id: userId,
          amount,
          note,
        }),
      });

      const text = await res.text();
      const json = text ? JSON.parse(text) : null;

      if (!res.ok || !json?.ok) {
        throw new Error(json?.error || 'Errore ricarica ABO');
      }

      notify(`Ricarica completata: ${amount}`, 'success');
      await loadTowerUsers();
    } catch (e: any) {
      notify(e?.message || 'Errore ricarica ABO', 'error');
    }
  }

  async function handleResetPassword(userId: string, userName: string) {
    const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
    if (!tokenToUse) {
      notify('Imposta prima il token admin in Systems', 'error');
      return;
    }

    const newPassword = window.prompt(`Nuova password temporanea per ${userName || userId}`, 'TempPass2026!');
    if (newPassword === null) return;
    if (!newPassword || newPassword.length < 8) {
      notify('Password troppo debole', 'error');
      return;
    }

    try {
      const res = await fetch('https://app.copyrightchain.it/api/ai/tower/users/reset-password', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CCP-Token': tokenToUse,
        },
        body: JSON.stringify({
          user_id: userId,
          new_password: newPassword,
        }),
      });

      const text = await res.text();
      const json = text ? JSON.parse(text) : null;

      if (!res.ok || !json?.ok) {
        throw new Error(json?.error || 'Errore reset password');
      }

      notify('Password reimpostata. Cambio obbligatorio al prossimo login.', 'success');
      await loadTowerUsers();
    } catch (e: any) {
      notify(e?.message || 'Errore reset password', 'error');
    }
  }

  async function handleViewUserCertificates(userObj: any) {
    const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
    if (!tokenToUse) {
      notify('Imposta prima il token admin in Systems', 'error');
      return;
    }

    setSelectedUserForCerts(userObj);
    setSelectedUserCerts([]);
    setSelectedUserCertsLoading(true);

    try {
      const res = await fetch(`https://app.copyrightchain.it/api/ai/tower/users/${encodeURIComponent(userObj.id)}/certificates`, {
        method: 'GET',
        headers: {
          'X-CCP-Token': tokenToUse,
        },
      });

      const text = await res.text();
      const json = text ? JSON.parse(text) : null;

      if (!res.ok || !json?.ok) {
        throw new Error(json?.error || 'Errore caricamento certificati utente');
      }

      setSelectedUserCerts(Array.isArray(json.certificates) ? json.certificates : []);
    } catch (e: any) {
      notify(e?.message || 'Errore caricamento certificati utente', 'error');
      setSelectedUserCerts([]);
    } finally {
      setSelectedUserCertsLoading(false);
    }
  }

  function openTowerPdf(path: string) {
    const tokenToUse = localStorage.getItem(LS_AI_ADMIN_TOKEN) || '';
    if (!tokenToUse) {
      notify('Imposta prima il token admin in Systems', 'error');
      return;
    }
    const url = `https://app.copyrightchain.it/api/ai/tower/certificates/file?path=${encodeURIComponent(path)}&token=${encodeURIComponent(tokenToUse)}`;
    window.open(url, '_blank', 'noopener,noreferrer');
  }

  const TabBtn = ({id,label,icon}:{id:AdminTab,label:string,icon:React.ReactNode}) => (
    <button onClick={() => setAdminTab(id)} className={`px-5 py-3 rounded-2xl text-[11px] font-black uppercase tracking-widest flex items-center gap-2 transition-all ${adminTab===id?'bg-slate-900 text-white shadow':'text-slate-500 hover:text-slate-900 hover:bg-slate-50'}`}>
      {icon}{label}
    </button>
  );

  const visibleTowerUsers = useMemo(() => {
    const q = userSearch.trim().toLowerCase();

    return towerUsers.filter((u) => {
      const dirty =
        (!u.email && !u.token) ||
        String(u.id || '').trim() === 'TOK_CLIENTE_1' ||
        (String(u.id || '').trim() === 'u1' && String(u.token || '').trim() === 'TOK_CLIENTE_1');

      if (hideDirtyUsers && dirty) return false;

      if (userPlanFilter && String(u.plan || '').toLowerCase() !== userPlanFilter.toLowerCase()) {
        return false;
      }

      if (!q) return true;

      const hay = [
        u.name || '',
        u.email || '',
        u.id || '',
        u.token || '',
        u.plan || '',
      ].join(' ').toLowerCase();

      return hay.includes(q);
    });
  }, [towerUsers, userSearch, userPlanFilter, hideDirtyUsers]);

  return (
    <div className="p-10 space-y-10">
      <div className="flex flex-wrap gap-3">
        <TabBtn id="stats" label="Stats" icon={<Activity size={16} />} />
        <TabBtn id="growth" label="Growth" icon={<Rocket size={16} />} />
        <TabBtn id="systems" label="Systems" icon={<Settings size={16} />} />
      </div>

      {adminTab === 'stats' && (
        <div className="space-y-8">
          <div className="bg-white p-10 rounded-[3rem] border border-slate-100 shadow-xl">
            <div className="flex flex-wrap items-center justify-between gap-4">
              <div>
                <h3 className="text-2xl font-black uppercase tracking-tight">Tower Overview</h3>
                <p className="text-slate-500 text-sm mt-3">Questa sezione è per amministrazione interna (Tower). Il cliente non la vede.</p>
              </div>
              <button onClick={loadTowerUsers} className="px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-600 font-black uppercase text-[10px] tracking-widest flex items-center gap-2">
                <RefreshCw size={14}/>refresh users
              </button>
            </div>

            <div className="mt-6 grid grid-cols-1 md:grid-cols-4 gap-4">
              <div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
                <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Users</div>
                <div className="text-2xl font-black">{visibleTowerUsers.length}</div>
              </div>
              <div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
                <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Certificates</div>
                <div className="text-2xl font-black">{visibleTowerUsers.reduce((a, u) => a + Number(u.cert_count || 0), 0)}</div>
              </div>
              <div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
                <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">ABO Balance</div>
                <div className="text-2xl font-black">{visibleTowerUsers.reduce((a, u) => a + Number(u.balance_abo || 0), 0).toFixed(2)}</div>
              </div>
              <div className="p-6 bg-slate-50 rounded-3xl border border-slate-100">
                <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">AI Gateway</div>
                <div className="text-2xl font-black">{aiStatus?.ok ? 'OK' : '—'}</div>
              </div>
            </div>
          </div>

          <div className="bg-white rounded-[3rem] border border-slate-100 shadow-xl overflow-hidden">
            <div className="px-8 py-6 border-b border-slate-100 space-y-5">
              <div className="flex items-center justify-between gap-4">
                <div>
                  <h4 className="text-xl font-black uppercase tracking-tight flex items-center gap-3"><Users size={20} className="text-blue-600" /> Utenti registrati</h4>
                  <p className="text-slate-500 text-sm mt-2">Utenti reali letti dal gateway Tower.</p>
                </div>
              </div>

              <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
                <div className="md:col-span-2 relative">
                  <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
                  <input
                    value={userSearch}
                    onChange={e => setUserSearch(e.target.value)}
                    placeholder="Cerca per nome, email, ID, token..."
                    className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl font-bold text-sm outline-none"
                  />
                </div>

                <select
                  value={userPlanFilter}
                  onChange={e => setUserPlanFilter(e.target.value)}
                  className="w-full p-4 bg-slate-50 border border-slate-100 rounded-2xl font-black text-xs uppercase outline-none"
                >
                  <option value="">Tutti i piani</option>
                  <option value="admin">Admin</option>
                  <option value="pro">Pro</option>
                  <option value="free">Free</option>
                </select>

                <label className="flex items-center gap-3 px-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-xs font-black uppercase tracking-widest text-slate-700">
                  <Filter size={14} />
                  <input
                    type="checkbox"
                    checked={hideDirtyUsers}
                    onChange={e => setHideDirtyUsers(e.target.checked)}
                  />
                  Nascondi sporchi
                </label>
              </div>
            </div>

            {towerUsersLoading ? (
              <div className="p-10 flex items-center justify-center gap-3 text-slate-500 font-bold">
                <Loader2 size={18} className="animate-spin" /> Caricamento utenti...
              </div>
            ) : visibleTowerUsers.length === 0 ? (
              <div className="p-10 text-slate-500 font-medium">Nessun utente disponibile con i filtri correnti.</div>
            ) : (
              <div className="overflow-x-auto">
                <table className="w-full min-w-[1100px]">
                  <thead>
                    <tr className="bg-slate-50 border-b border-slate-100">
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Utente</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Email</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">ID</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Token</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Plan</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">ABO</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Certificati</th>
                      <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Azioni</th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-slate-50">
                    {visibleTowerUsers.map((u) => (
                      <tr key={u.id} className="hover:bg-slate-50/40">
                        <td className="px-6 py-5">
                          <div className="font-black text-slate-900">{u.name || '—'}</div>
                        </td>
                        <td className="px-6 py-5">
                          <div className="text-sm font-bold text-slate-700">{u.email || '—'}</div>
                        </td>
                        <td className="px-6 py-5">
                          <div className="text-xs font-mono font-black text-slate-600">{u.id || '—'}</div>
                        </td>
                        <td className="px-6 py-5">
                          <div className="text-xs font-mono font-black text-slate-600 break-all max-w-[260px]">{u.token || '—'}</div>
                        </td>
                        <td className="px-6 py-5">
                          <span className="inline-flex px-3 py-1 rounded-full bg-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">
                            {u.plan || 'free'}
                          </span>
                        </td>
                        <td className="px-6 py-5">
                          <div className="text-sm font-black text-slate-900">{Number(u.balance_abo || 0).toFixed(2)}</div>
                        </td>
                        <td className="px-6 py-5">
                          <div className="text-sm font-black text-slate-900">{Number(u.cert_count || 0)}</div>
                        </td>
                        <td className="px-6 py-5">
                          <div className="flex flex-wrap gap-2">
                            <button
                              onClick={() => handleCreditUser(u.id, u.name || u.email || u.id)}
                              className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-blue-700"
                            >
                              <PlusCircle size={14} />
                              Ricarica ABO
                            </button>
                            <button
                              onClick={() => handleResetPassword(u.id, u.name || u.email || u.id)}
                              className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-slate-800"
                            >
                              Reset PW
                            </button>
                            <button
                              onClick={() => handleViewUserCertificates(u)}
                              className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-emerald-600 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-emerald-700"
                            >
                              Certificati
                            </button>
                          </div>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
          </div>

          {selectedUserForCerts && (
            <div className="bg-white rounded-[3rem] border border-slate-100 shadow-xl overflow-hidden">
              <div className="px-8 py-6 border-b border-slate-100 flex items-center justify-between gap-4">
                <div>
                  <h4 className="text-xl font-black uppercase tracking-tight">Certificati utente</h4>
                  <p className="text-slate-500 text-sm mt-2">
                    {selectedUserForCerts.name || '—'} · {selectedUserForCerts.email || selectedUserForCerts.id}
                  </p>
                </div>
                <button
                  onClick={() => {
                    setSelectedUserForCerts(null);
                    setSelectedUserCerts([]);
                  }}
                  className="px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-600 font-black uppercase text-[10px] tracking-widest"
                >
                  Chiudi
                </button>
              </div>

              {selectedUserCertsLoading ? (
                <div className="p-10 flex items-center justify-center gap-3 text-slate-500 font-bold">
                  <Loader2 size={18} className="animate-spin" /> Caricamento certificati...
                </div>
              ) : selectedUserCerts.length === 0 ? (
                <div className="p-10 text-slate-500 font-medium">
                  Nessun certificato trovato per questo utente.
                </div>
              ) : (
                <div className="overflow-x-auto">
                  <table className="w-full min-w-[1100px]">
                    <thead>
                      <tr className="bg-slate-50 border-b border-slate-100">
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">ID</th>
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Titolo/File</th>
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Tipo</th>
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Stato</th>
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Data</th>
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Hash</th>
                        <th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Azioni</th>
                      </tr>
                    </thead>
                    <tbody className="divide-y divide-slate-50">
                      {selectedUserCerts.map((c: any) => (
                        <tr key={String(c?.cert_id || c?.request_id || Math.random())} className="hover:bg-slate-50/40">
                          <td className="px-6 py-5">
                            <div className="text-xs font-mono font-black text-slate-700">{c?.cert_id || c?.request_id || '—'}</div>
                          </td>
                          <td className="px-6 py-5">
                            <div className="font-black text-slate-900">{c?.title || '—'}</div>
                            <div className="text-xs font-bold text-slate-500 break-all">{c?.original_filename || '—'}</div>
                          </td>
                          <td className="px-6 py-5">
                            <span className="inline-flex px-3 py-1 rounded-full bg-slate-100 text-slate-700 text-[10px] font-black uppercase tracking-widest">
                              {c?.cert_type || 'base'}
                            </span>
                          </td>
                          <td className="px-6 py-5">
                            <span className={`inline-flex px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${
                              (c?.blockchain_status || '') === 'confirmed'
                                ? 'bg-emerald-50 text-emerald-700'
                                : (c?.blockchain_status || '') === 'pending'
                                ? 'bg-amber-50 text-amber-700'
                                : 'bg-slate-100 text-slate-700'
                            }`}>
                              {c?.blockchain_status || (c?.cert_type === 'base' ? 'base' : '—')}
                            </span>
                          </td>
                          <td className="px-6 py-5">
                            <div className="text-sm font-black text-slate-900">{c?.deposit_date || c?.ts || '—'}</div>
                          </td>
                          <td className="px-6 py-5">
                            <div className="text-xs font-mono font-black text-slate-600 break-all max-w-[260px]">{c?.file_hash || '—'}</div>
                          </td>
                          <td className="px-6 py-5">
                            <div className="flex flex-wrap gap-2">
                              {c?.verify_url ? (
                                <a
                                  href={c.verify_url}
                                  target="_blank"
                                  rel="noreferrer"
                                  className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-blue-700"
                                >
                                  Verifica
                                </a>
                              ) : null}

                              {c?.certificate_path ? (
                                <button
                                  onClick={() => openTowerPdf(c.certificate_path)}
                                  className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-900 text-white text-[10px] font-black uppercase tracking-widest shadow hover:bg-slate-800"
                                >
                                  Apri PDF
                                </button>
                              ) : null}
                            </div>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
            </div>
          )}
        </div>
      )}

      {adminTab === 'growth' && (
        <div className="space-y-8">
          <div className="bg-white p-12 rounded-[4rem] border border-slate-100 shadow-xl space-y-10">
            <div className="flex justify-between items-center">
              <h3 className="text-2xl font-black uppercase tracking-tight flex items-center gap-4"><Sparkles size={28} className="text-indigo-600" /> Growth Hub (Marketing)</h3>
              <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400">
                <Share2 size={14}/>solo admin
              </div>
            </div>

            <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
              <div className="space-y-3">
                <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Cosa vuoi promuovere?</label>
                <textarea value={mktContext} onChange={e => setMktContext(e.target.value)} className="w-full p-8 bg-slate-50 border border-slate-100 rounded-[2.5rem] text-sm font-medium h-48 outline-none" placeholder="Es: Feature OTS + prova gratuita 7 giorni..." />
              </div>
              <div className="space-y-6">
                <div className="grid grid-cols-2 gap-4">
                  <div className="space-y-2">
                    <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Formato</label>
                    <select value={mktType} onChange={e => setMktType(e.target.value as any)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
                      <option>Post</option><option>Email</option><option>Blog</option><option>Ad</option>
                    </select>
                  </div>
                  <div className="space-y-2">
                    <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Canale</label>
                    <select value={mktChannel} onChange={e => setMktChannel(e.target.value)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
                      <option value="LinkedIn">LinkedIn</option>
                      <option value="Instagram">Instagram</option>
                      <option value="X (Twitter)">X (Twitter)</option>
                      <option value="Email">Newsletter</option>
                    </select>
                  </div>
                </div>

                <div className="grid grid-cols-2 gap-4">
                  <div className="space-y-2">
                    <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Aspect Ratio</label>
                    <select value={mktAspectRatio} onChange={e => setMktAspectRatio(e.target.value)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
                      <option value="1:1">1:1</option>
                      <option value="16:9">16:9</option>
                      <option value="9:16">9:16</option>
                    </select>
                  </div>
                  <div className="space-y-2">
                    <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Provider</label>
                    <div className="grid grid-cols-2 gap-2">
                      <select value={textProvider} onChange={e => setTextProvider(e.target.value as AiProvider)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
                        <option value="openai">Text: OpenAI</option>
                        <option value="gemini">Text: Gemini</option>
                      </select>
                      <select value={imageProvider} onChange={e => setImageProvider(e.target.value as AiProvider)} className="w-full p-5 bg-slate-50 border border-slate-100 rounded-3xl font-black text-xs uppercase outline-none">
                        <option value="gemini">Image: Gemini</option>
                      </select>
                    </div>
                  </div>
                </div>

                <div className="grid grid-cols-2 gap-4">
                  <button onClick={handleGenerateCampaign} disabled={isGenText || !mktContext} className="py-6 bg-slate-900 text-white rounded-[2rem] font-black uppercase text-[10px] tracking-widest shadow-2xl flex items-center justify-center gap-3">
                    {isGenText ? <Loader2 size={18} className="animate-spin" /> : <><Rocket size={18} /> Genera Testo</>}
                  </button>
                  <button onClick={handleGenerateVisual} disabled={isGenVisual || !mktContext} className="py-6 bg-indigo-600 text-white rounded-[2rem] font-black uppercase text-[10px] tracking-widest shadow-2xl flex items-center justify-center gap-3">
                    {isGenVisual ? <Loader2 size={18} className="animate-spin" /> : <><ImageIcon size={18} /> Genera Immagine</>}
                  </button>
                </div>
              </div>
            </div>
          </div>

          {(genContent || genVisual) && (
            <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
              <div className="bg-slate-900 text-white p-10 rounded-[3rem] shadow-2xl flex flex-col">
                <div className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-6">Testo generato</div>
                <pre className="text-sm whitespace-pre-wrap flex-1">{genContent}</pre>
                <button onClick={() => onCopy(genContent)} className="w-full py-4 mt-6 bg-white/5 border border-white/10 rounded-2xl font-black uppercase text-[10px] tracking-widest flex items-center justify-center gap-2 hover:bg-white/10">
                  <Copy size={14}/>Copia
                </button>
              </div>
              <div className="bg-white p-3 rounded-[3rem] shadow-2xl border border-slate-100 overflow-hidden">
                {genVisual ? <img src={genVisual} className="w-full h-full object-cover rounded-[2.6rem]" alt="AI Visual" /> : null}
              </div>
            </div>
          )}
        </div>
      )}

      {adminTab === 'systems' && (
        <div className="bg-white p-12 rounded-[4rem] border border-slate-100 shadow-xl space-y-8">
          <div className="flex justify-between items-center">
            <h3 className="text-2xl font-black uppercase tracking-tight flex items-center gap-4"><Settings size={28} className="text-blue-600" /> Systems (Tecnico)</h3>
            <button onClick={refreshAiHealth} className="px-4 py-3 rounded-2xl bg-slate-50 border border-slate-100 text-slate-600 font-black uppercase text-[10px] tracking-widest flex items-center gap-2">
              <RefreshCw size={14}/>refresh
            </button>
          </div>

          <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
            <div className="p-8 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-5">
              <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">AI Gateway</div>

              <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Admin Token</label>
              <input value={aiAdminToken} onChange={e => setAiAdminToken(e.target.value)} className="w-full p-5 bg-white border border-slate-100 rounded-3xl font-black text-xs outline-none" placeholder="CCP_AI_ADMIN_TOKEN" />

              <div className="grid grid-cols-1 gap-3">
                <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">OpenAI API Key (opzionale)</label>
                <input value={openaiKeyDraft} onChange={e => setOpenaiKeyDraft(e.target.value)} className="w-full p-5 bg-white border border-slate-100 rounded-3xl font-black text-xs outline-none" placeholder="sk-..." />
                <label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Gemini API Key (opzionale)</label>
                <input value={geminiKeyDraft} onChange={e => setGeminiKeyDraft(e.target.value)} className="w-full p-5 bg-white border border-slate-100 rounded-3xl font-black text-xs outline-none" placeholder="AIza..." />
              </div>

              <div className="grid grid-cols-2 gap-3">
                <button onClick={handleSaveAiKeys} disabled={isSavingKeys} className="py-5 bg-slate-900 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest shadow-xl flex items-center justify-center gap-2">
                  {isSavingKeys ? <Loader2 size={16} className="animate-spin" /> : <><Save size={16}/>Salva</>}
                </button>
                <button onClick={handleTestAi} disabled={isTestingAi} className="py-5 bg-blue-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest shadow-xl flex items-center justify-center gap-2">
                  {isTestingAi ? <Loader2 size={16} className="animate-spin" /> : <><KeyRound size={16}/>Test</>}
                </button>
              </div>

              <div className="text-xs text-slate-600 leading-relaxed">
                Stato gateway: <b>{aiStatus?.ok ? 'OK' : '—'}</b><br/>
                OpenAI key nel gateway: <b>{aiStatus?.has_openai_key ? 'OK' : 'NO'}</b><br/>
                Gemini key nel gateway: <b>{aiStatus?.has_gemini_key ? 'OK' : 'NO'}</b>
              </div>
              <div className="p-4 bg-blue-50 border border-blue-100 rounded-2xl text-xs text-blue-900 leading-relaxed">
                Le chiavi AI di produzione sono già caricate sul VPS nel gateway. Questa sezione va considerata come pannello tecnico di override/test e non come configurazione obbligatoria per il funzionamento quotidiano.
              </div>
            </div>

            <div className="p-8 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-5">
              <div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Note</div>
              <div className="text-sm text-slate-600 leading-relaxed">
                <ul className="list-disc pl-5 space-y-2">
                  <li>Questa sezione è solo per Tower (admin).</li>
                  <li>Le chiavi di produzione sono già salvate sul VPS nel gateway.</li>
                  <li>I campi qui servono solo per override tecnico o test manuali.</li>
                </ul>
              </div>
              <div className="p-5 bg-white rounded-2xl border border-slate-100 text-xs text-slate-600">
                Se il gateway non risponde: controlla systemd <code>sudo systemctl status ccp-ai-gateway</code> e NGINX <code>sudo nginx -t</code>.
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Youez - 2016 - github.com/yon3zu
LinuXploit