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/patch/frontend/components/AdminDashboard/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/app.copyrightchain.it/patch/frontend/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
} 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';

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 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);
    }
  }

  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>
  );

  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="bg-white p-10 rounded-[3rem] border border-slate-100 shadow-xl">
          <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 className="mt-6 grid grid-cols-1 md:grid-cols-3 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">Works</div>
              <div className="text-2xl font-black">{props.certificates?.length ?? 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">Users</div>
              <div className="text-2xl font-black">{props.users?.length ?? 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">AI Gateway</div>
              <div className="text-2xl font-black">{aiStatus?.ok ? 'OK' : '—'}</div>
            </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: <b>{aiStatus?.ok ? 'OK' : '—'}</b><br/>
                OpenAI key: <b>{aiStatus?.has_openai_key ? 'OK' : 'NO'}</b><br/>
                Gemini key: <b>{aiStatus?.has_gemini_key ? 'OK' : 'NO'}</b>
              </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 vengono salvate sul VPS nel gateway (non nel browser).</li>
                  <li>La pagina Growth usa OpenAI per i testi e Gemini per le immagini (se configurati).</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