File size: 12,253 Bytes
879d451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5fe13e
879d451
 
 
 
 
 
 
e5fe13e
 
879d451
 
e5fe13e
3da59ab
 
 
 
 
879d451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3da59ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5fe13e
3da59ab
 
 
 
e5fe13e
3da59ab
 
 
 
e5fe13e
3da59ab
 
 
 
 
 
e5fe13e
3da59ab
 
e5fe13e
3da59ab
 
 
e5fe13e
 
3da59ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5fe13e
3da59ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
{% extends "base.html" %}
{% block title %}Admin - English Helper{% endblock %}
{% block content %}
<style>

    .spinner {

        border: 3px solid #f3f3f3;

        border-top: 3px solid #3498db;

        border-radius: 50%;

        width: 20px;

        height: 20px;

        animation: spin 1s linear infinite;

        display: inline-block;

        margin-right: 10px;

    }

    @keyframes spin {

        0% { transform: rotate(0deg); }

        100% { transform: rotate(360deg); }

    }

    .table-container {

        max-height: 600px;

        overflow-y: auto;

    }

</style>

<div id="adminInterface">
    <!-- Header -->
    <header class="bg-white shadow-md">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between items-center py-4">
                <div class="flex items-center">
                    <h1 class="text-2xl font-bold text-gray-900">πŸŽ“ English Helper Admin</h1>
                </div>
            </div>
        </div>
    </header>

    <!-- Floating Save Data Button -->
    <div class="fixed top-4 right-4 z-50">
        <button id="saveDataBtn" class="px-4 py-2 bg-blue-600 text-white rounded shadow">Salvar dados</button>
    </div>

    <!-- Navigation Tabs -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
        <div class="border-b border-gray-200">
            <nav class="-mb-px flex space-x-8">
                <button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
                    πŸ“Š Dashboard
                </button>
                <button onclick="showTab('users')" class="admin-tab" data-tab="users">
                    πŸ‘₯ Users
                </button>
                <button onclick="showTab('database')" class="admin-tab" data-tab="database">
                    πŸ—„οΈ Database
                </button>
                <button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
                    πŸ’³ Token Usage
                </button>
                <button onclick="showTab('system')" class="admin-tab" data-tab="system">
                    🩺 System Health
                </button>
            </nav>
        </div>
    </div>

    <!-- Tab Content -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
        <!-- Dashboard Tab -->
        <div id="dashboardTab" class="tab-content">
            <!-- ...existing dashboard content... -->
        </div>

        <!-- Users Tab -->
        <div id="usersTab" class="tab-content hidden">
            <!-- ...existing users content... -->
        </div>

        <!-- Database Tab -->
        <div id="databaseTab" class="tab-content hidden">
            <!-- ...existing database content... -->
        </div>

        <!-- Token Usage Tab -->
        <div id="tokensTab" class="tab-content hidden">
            <!-- ...existing token usage content... -->
        </div>

        <!-- System Health Tab -->
        <div id="systemTab" class="tab-content hidden">
            <!-- ...existing system health content... -->
        </div>
    </div>
</div>

<!-- User Detail Modal -->
<div id="userDetailModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
    <div class="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-screen overflow-y-auto">
        <div class="flex justify-between items-center mb-4">
            <!-- ...existing modal header... -->
        </div>
        <div id="userDetailContent">
            <!-- ...existing modal content... -->
        </div>
    </div>
</div>

<!-- Toast Notifications -->
<div id="toastContainer" class="fixed top-4 right-4 z-50"></div>

<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>

// Minimal, consolidated admin UI JS that talks to existing backend endpoints

let currentPage = 1;

let usersData = [];



document.addEventListener('DOMContentLoaded', () => {

    showTab('dashboard');

    loadDashboard();

    loadUsers();

    loadDatabaseSchema();

    loadTokenUsage();

    loadSystemHealth();

});



function showTab(tabName) {

    document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));

    const active = document.getElementById(tabName + 'Tab');

    if (active) active.classList.remove('hidden');



    document.querySelectorAll('.admin-tab').forEach(btn => btn.classList.remove('active'));

    const btn = document.querySelector(`.admin-tab[data-tab="${tabName}"]`);

    if (btn) btn.classList.add('active');

}



async function loadDashboard() {

    try {

        const res = await fetch('/admin/stats');

        const data = await res.json();



        const container = document.getElementById('dashboardTab');

        if (!container) return;



        container.innerHTML = `

            <h2 class="text-xl font-semibold mb-4">System Dashboard</h2>

            <div class="grid grid-cols-1 md:grid-cols-4 gap-6">

                <div class="p-4 bg-white rounded shadow"> <div class="text-sm text-gray-500">Total Users</div> <div class="text-2xl font-bold">${data.total_users ?? '-'}</div></div>

                <div class="p-4 bg-white rounded shadow"> <div class="text-sm text-gray-500">Study Sessions</div> <div class="text-2xl font-bold">-</div></div>

                <div class="p-4 bg-white rounded shadow"> <div class="text-sm text-gray-500">API Tokens</div> <div class="text-2xl font-bold">-</div></div>

                <div class="p-4 bg-white rounded shadow"> <div class="text-sm text-gray-500">Content Items</div> <div class="text-2xl font-bold">-</div></div>

            </div>

            <div class="mt-6">

                <canvas id="userGrowthChart" height="120"></canvas>

            </div>

        `;



        // simple user growth sample chart

        const ctx = document.getElementById('userGrowthChart')?.getContext('2d');

        if (ctx) {

            new Chart(ctx, { type: 'line', data: { labels: ['-','-','-'], datasets:[{label:'Users', data:[0,0, data.total_users || 0], borderColor:'rgb(59,130,246)', fill:false}]}, options:{responsive:true}});

        }

    } catch (e) {

        console.error('Dashboard load error', e);

    }

}



async function loadUsers() {

    try {

        const res = await fetch('/users');

        const data = await res.json();

        const users = data.users || [];

        usersData = users;



        const container = document.getElementById('usersTab');

        if (!container) return;



        if (users.length === 0) {

            container.innerHTML = '<div class="text-center py-8">No users found</div>';

            return;

        }



        container.innerHTML = `

            <div class="overflow-x-auto bg-white rounded shadow">

                <table class="w-full text-left">

                    <thead class="bg-gray-50"><tr><th class="p-3">User ID</th><th class="p-3">Stats</th><th class="p-3">Actions</th></tr></thead>

                    <tbody id="usersTableBody">${users.map(u=>`<tr class="border-b"><td class="p-3">${u}</td><td class="p-3">-</td><td class="p-3"><button onclick="viewUser('${u}')" class="text-blue-600">View</button></td></tr>`).join('')}</tbody>

                </table>

            </div>

        `;

    } catch (e) {

        console.error('loadUsers error', e);

        showToast('Failed to load users', 'error');

    }

}



async function viewUser(userId) {

    try {

        const res = await fetch(`/user/${encodeURIComponent(userId)}/analytics`);

        const data = await res.json();

        const modal = document.getElementById('userDetailModal');

        const content = document.getElementById('userDetailContent');

        content.innerHTML = `

            <h3 class="text-lg font-semibold mb-2">User: ${userId}</h3>

            <div class="text-sm">Total flashcards: ${data.total_flashcards ?? 0}</div>

            <div class="text-sm">Total conversations: ${data.total_conversations ?? 0}</div>

            <div class="text-sm">Total sessions: ${data.total_sessions ?? 0}</div>

            <div class="mt-4">

                <h4 class="font-medium">Recent activity</h4>

                <div class="max-h-48 overflow-y-auto text-sm">${(data.recent_activity||[]).map(a=>`<div class="py-1 border-b">${a.timestamp || ''} - ${a.get || a.action || JSON.stringify(a)}</div>`).join('')}</div>

            </div>

        `;

        modal.classList.remove('hidden');

    } catch (e) {

        console.error('viewUser error', e);

        showToast('Failed to load user details', 'error');

    }

}



async function loadDatabaseSchema() {

    try {

        // no DB backend available in HF simplified; show placeholder

        const container = document.getElementById('databaseTab');

        if (!container) return;

        container.innerHTML = '<div class="p-4 bg-white rounded shadow">No database schema available in file-based HF mode.</div>';

    } catch (e) {

        console.error(e);

    }

}



async function loadTokenUsage() {

    try {

        const container = document.getElementById('tokensTab');

        if (!container) return;

        container.innerHTML = `

            <div class="grid grid-cols-1 md:grid-cols-2 gap-4">

                <canvas id="providerUsageChart"></canvas>

                <canvas id="costBreakdownChart"></canvas>

            </div>

        `;

        // create sample charts

        const pctx = document.getElementById('providerUsageChart')?.getContext('2d');

        if (pctx) new Chart(pctx, {type:'bar', data:{labels:['Groq','Gemini'], datasets:[{label:'Input Tokens', data:[150000,85000], backgroundColor:['#3b82f6','#9333ea']}]}, options:{responsive:true}});

        const cctx = document.getElementById('costBreakdownChart')?.getContext('2d');

        if (cctx) new Chart(cctx, {type:'pie', data:{labels:['Conversation','Analysis','Planning'], datasets:[{data:[40,30,30], backgroundColor:['#22c55e','#3b82f6','#f97316']}]}, options:{responsive:true}});

    } catch (e) { console.error(e); }

}



async function loadSystemHealth() {

    try {

        const res = await fetch('/system/status');

        const data = await res.json();

        const container = document.getElementById('systemTab');

        if (!container) return;

        container.innerHTML = `<div class="p-4 bg-white rounded shadow"><pre class="text-sm">${JSON.stringify(data, null, 2)}</pre></div>`;

    } catch (e) {

        console.error('system health error', e);

    }

}



function showToast(message, type='info'){

    const toast = document.createElement('div');

    toast.className = `fixed top-4 right-4 p-3 rounded shadow z-50 ${type==='success'?'bg-green-600':type==='error'?'bg-red-600':'bg-blue-600'} text-white`;

    toast.textContent = message; document.body.appendChild(toast);

    setTimeout(()=>toast.remove(),4000);

}



// Save data button

document.addEventListener('click', (e) => {

    if (e.target && e.target.id === 'saveDataBtn') {

        e.target.disabled = true;

        e.target.textContent = 'Preparando...';

        fetch('/admin/export/all')

            .then(res => {

                if (!res.ok) throw new Error('Export failed');

                return res.blob();

            })

            .then(blob => {

                const url = window.URL.createObjectURL(blob);

                const a = document.createElement('a');

                a.href = url;

                a.download = 'hf_data_export.zip';

                document.body.appendChild(a);

                a.click();

                a.remove();

                window.URL.revokeObjectURL(url);

                showToast('Download iniciado', 'success');

            })

            .catch(err => {

                console.error('Export all error', err);

                showToast('Falha ao exportar dados', 'error');

            })

            .finally(() => {

                e.target.disabled = false;

                e.target.textContent = 'Salvar dados';

            });

    }

});

</script>
{% endblock %}