import { createClient } from '@supabase/supabase-js'; import crypto from 'node:crypto'; const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL || process.env.PUBLIC_SUPABASE_URL; const supabaseAnonKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY || process.env.PUBLIC_SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { console.warn("Supabase credentials missing! Please configure PUBLIC_SUPABASE_URL and PUBLIC_SUPABASE_ANON_KEY in your .env file."); } export const supabase = createClient(supabaseUrl, supabaseAnonKey); // Kiểm tra kết nối Supabase và in log ra Terminal CLI if (supabaseUrl && supabaseAnonKey) { supabase.from('settings').select('key').limit(1) .then(({ error }) => { if (error) { console.log("\x1b[41m\x1b[37m[Supabase]\x1b[0m \x1b[31m❌ KẾT NỐI THẤT BẠI: " + error.message + "\x1b[0m"); } else { console.log("\x1b[42m\x1b[30m[Supabase]\x1b[0m \x1b[32m✅ KẾT NỐI THÀNH CÔNG tới " + supabaseUrl + "\x1b[0m"); } }) .catch(err => { console.log("\x1b[41m\x1b[37m[Supabase]\x1b[0m \x1b[31m❌ LỖI KẾT NỐI: " + err.message + "\x1b[0m"); }); } export class txasupabase { static get client() { return supabase; } static get supabase() { return supabase; } // --- MOVIES --- static async getLocalMovies() { const { data, error } = await supabase .from('movies') .select('slug, movie, episodes'); if (error) { console.error('Error fetching movies from Supabase:', error); return {}; } const moviesObj = {}; (data || []).forEach(row => { moviesObj[row.slug] = { movie: row.movie, episodes: row.episodes }; }); return moviesObj; } static async saveLocalMovie(slug, movieData) { const existing = await this.getMovieBySlug(slug); const updatedMovie = { ...existing?.movie, ...movieData.movie, slug, modified: { time: new Date().toISOString() } }; const updatedEpisodes = movieData.episodes || existing?.episodes || []; // So sánh xem có tập mới không! let newEpisodesAdded = []; if (existing && existing.episodes && movieData.episodes) { for (const newServer of movieData.episodes) { const extServer = existing.episodes.find(s => s.server_name === newServer.server_name); if (!extServer) { newEpisodesAdded.push(...(newServer.server_data || [])); } else { for (const newEp of (newServer.server_data || [])) { const hasEp = extServer.server_data.some(e => e.name === newEp.name); if (!hasEp) { newEpisodesAdded.push(newEp); } } } } } const { data, error } = await supabase .from('movies') .upsert({ slug, movie: updatedMovie, episodes: updatedEpisodes, updated_at: new Date().toISOString() }) .select() .single(); if (error) { console.error(`Error saving movie ${slug} to Supabase:`, error); throw error; } if (newEpisodesAdded.length > 0) { this.notifyNewEpisodes(slug, updatedMovie.name, newEpisodesAdded).catch(err => { console.error('[Notification Trigger Error]:', err); }); } return { movie: data.movie, episodes: data.episodes }; } static async getMovieBySlug(slug) { const { data, error } = await supabase .from('movies') .select('slug, movie, episodes') .eq('slug', slug) .maybeSingle(); if (error) { console.error(`Error fetching movie ${slug} from Supabase:`, error); return null; } return data ? { movie: data.movie, episodes: data.episodes } : null; } static async resetLocalMovie(slug) { const { error } = await supabase .from('movies') .delete() .eq('slug', slug); if (error) { console.error(`Error resetting movie ${slug} on Supabase:`, error); return false; } return true; } // --- DELETED MOVIES --- static async getDeletedMovies() { const { data, error } = await supabase .from('deleted_movies') .select('slug'); if (error) { console.error('Error fetching deleted movies from Supabase:', error); return []; } return (data || []).map(row => row.slug); } static async deleteLocalMovie(slug) { // 1. Nếu là phim cục bộ tự tạo, xóa khỏi movies const movieObj = await this.getMovieBySlug(slug); let isLocalOnly = false; if (movieObj && movieObj.movie.isLocal) { const { error } = await supabase .from('movies') .delete() .eq('slug', slug); if (error) console.error('Error deleting local movie from Supabase:', error); isLocalOnly = true; } // 2. Thêm vào deleted_movies để ẩn khỏi API const { error: delError } = await supabase .from('deleted_movies') .upsert({ slug }) .select(); if (delError) { console.error('Error adding movie to deleted_movies on Supabase:', delError); } return { success: true, isLocalOnly }; } static async restoreLocalMovie(slug) { const { error } = await supabase .from('deleted_movies') .delete() .eq('slug', slug); if (error) { console.error(`Error restoring movie ${slug} from Supabase:`, error); return false; } return true; } // --- GENRES --- static async getGenres() { const { data, error } = await supabase .from('genres') .select('slug, name') .order('name'); if (error) { console.error('Error fetching genres from Supabase:', error); return []; } return data || []; } static async saveGenre(slug, name) { const { error } = await supabase .from('genres') .upsert({ slug, name }); if (error) { console.error(`Error saving genre ${slug} to Supabase:`, error); } return await this.getGenres(); } static async deleteGenre(slug) { const { error } = await supabase .from('genres') .delete() .eq('slug', slug); if (error) { console.error(`Error deleting genre ${slug} from Supabase:`, error); } return await this.getGenres(); } static async bulkDeleteGenres(slugs) { const { error } = await supabase .from('genres') .delete() .in('slug', slugs); if (error) { console.error('Error bulk deleting genres from Supabase:', error); } return await this.getGenres(); } // --- COUNTRIES --- static async getCountries() { const { data, error } = await supabase .from('countries') .select('slug, name') .order('name'); if (error) { console.error('Error fetching countries from Supabase:', error); return []; } return data || []; } static async saveCountry(slug, name) { const { error } = await supabase .from('countries') .upsert({ slug, name }); if (error) { console.error(`Error saving country ${slug} to Supabase:`, error); } return await this.getCountries(); } static async deleteCountry(slug) { const { error } = await supabase .from('countries') .delete() .eq('slug', slug); if (error) { console.error(`Error deleting country ${slug} from Supabase:`, error); } return await this.getCountries(); } static async bulkDeleteCountries(slugs) { const { error } = await supabase .from('countries') .delete() .in('slug', slugs); if (error) { console.error('Error bulk deleting countries from Supabase:', error); } return await this.getCountries(); } // --- ACTORS --- static async getActors() { const { data, error } = await supabase .from('actors') .select('name') .order('name'); if (error) { console.error('Error fetching actors from Supabase:', error); return []; } return data || []; } static async saveActor(name, originalName = null) { const cleanName = name.trim(); if (originalName && originalName.trim().toLowerCase() !== cleanName.toLowerCase()) { const { error: delError } = await supabase .from('actors') .delete() .eq('name', originalName.trim()); if (delError) console.error('Error deleting renamed actor on Supabase:', delError); } const { error } = await supabase .from('actors') .upsert({ name: cleanName }); if (error) { console.error(`Error saving actor ${cleanName} to Supabase:`, error); } return await this.getActors(); } static async deleteActor(name) { const { error } = await supabase .from('actors') .delete() .eq('name', name); if (error) { console.error(`Error deleting actor ${name} from Supabase:`, error); } return await this.getActors(); } static async bulkDeleteActors(names) { const { error } = await supabase .from('actors') .delete() .in('name', names); if (error) { console.error('Error bulk deleting actors from Supabase:', error); } return await this.getActors(); } // --- SERVERS --- static async getServers() { const { data, error } = await supabase .from('servers') .select('name') .order('name'); if (error) { console.error('Error fetching servers from Supabase:', error); return []; } return data || []; } static async saveServer(name, originalName = null) { const cleanName = name.trim(); if (originalName && originalName.trim().toLowerCase() !== cleanName.toLowerCase()) { const { error: delError } = await supabase .from('servers') .delete() .eq('name', originalName.trim()); if (delError) console.error('Error deleting renamed server on Supabase:', delError); } const { error } = await supabase .from('servers') .upsert({ name: cleanName }); if (error) { console.error(`Error saving server ${cleanName} to Supabase:`, error); } return await this.getServers(); } static async deleteServer(name) { const { error } = await supabase .from('servers') .delete() .eq('name', name); if (error) { console.error(`Error deleting server ${name} from Supabase:`, error); } return await this.getServers(); } static async bulkDeleteServers(names) { const { error } = await supabase .from('servers') .delete() .in('name', names); if (error) { console.error('Error bulk deleting servers from Supabase:', error); } return await this.getServers(); } // --- AUTOMATIC SYNC FROM MOVIES DATA --- static async syncFromMovies(type) { const moviesObj = await this.getLocalMovies(); const allMovies = Object.values(moviesObj); if (type === 'servers') { const serversList = await this.getServers(); const serverSet = new Set(serversList.map(s => s.name.toLowerCase())); const extracted = new Set(); for (const item of allMovies) { if (item.episodes) { for (const epGroup of item.episodes) { if (epGroup.server_name) { extracted.add(epGroup.server_name.trim()); } } } } const newServers = []; for (const sName of extracted) { if (!serverSet.has(sName.toLowerCase())) { newServers.push({ name: sName }); serverSet.add(sName.toLowerCase()); } } if (newServers.length > 0) { const { error } = await supabase.from('servers').insert(newServers); if (error) console.error('Error syncing servers to Supabase:', error); } return await this.getServers(); } if (type === 'genres') { const genresList = await this.getGenres(); const genreMap = new Map(genresList.map(g => [g.slug, g.name])); const newGenres = []; for (const item of allMovies) { const cats = item.movie.category || []; for (const cat of cats) { if (cat.slug && cat.name) { if (!genreMap.has(cat.slug)) { newGenres.push({ slug: cat.slug, name: cat.name }); genreMap.set(cat.slug, cat.name); } } } } if (newGenres.length > 0) { const { error } = await supabase.from('genres').insert(newGenres); if (error) console.error('Error syncing genres to Supabase:', error); } return await this.getGenres(); } if (type === 'countries') { const countriesList = await this.getCountries(); const countryMap = new Map(countriesList.map(c => [c.slug, c.name])); const newCountries = []; for (const item of allMovies) { const countries = item.movie.country || []; for (const c of countries) { if (c.slug && c.name) { if (!countryMap.has(c.slug)) { newCountries.push({ slug: c.slug, name: c.name }); countryMap.set(c.slug, c.name); } } } } if (newCountries.length > 0) { const { error } = await supabase.from('countries').insert(newCountries); if (error) console.error('Error syncing countries to Supabase:', error); } return await this.getCountries(); } if (type === 'actors') { const actorsList = await this.getActors(); const actorSet = new Set(actorsList.map(a => a.name.toLowerCase())); const extracted = new Set(); for (const item of allMovies) { const actors = item.movie.actor || []; for (const actor of actors) { if (actor && typeof actor === 'string') { extracted.add(actor.trim()); } } } const newActors = []; for (const actorName of extracted) { if (!actorSet.has(actorName.toLowerCase())) { newActors.push({ name: actorName }); actorSet.add(actorName.toLowerCase()); } } if (newActors.length > 0) { const { error } = await supabase.from('actors').insert(newActors); if (error) console.error('Error syncing actors to Supabase:', error); } return await this.getActors(); } return []; } // --- USERS --- static async getUsers() { const { data, error } = await supabase .from('users') .select('*'); if (error) { console.error('Error fetching users from Supabase:', error); return []; } return data || []; } static async getUserByUsername(username) { const { data, error } = await supabase .from('users') .select('*') .eq('username', username) .maybeSingle(); if (error) { console.error(`Error fetching user ${username} from Supabase:`, error); return null; } return data; } static async getUserById(id) { const { data, error } = await supabase .from('users') .select('*') .eq('id', id) .maybeSingle(); if (error) { console.error(`Error fetching user by ID ${id} from Supabase:`, error); return null; } return data; } static getGravatarUrl(email) { if (!email) return 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp'; const hash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex'); return `https://www.gravatar.com/avatar/${hash}?d=mp`; } static async saveUser(user) { const defaultAvatar = user.email ? this.getGravatarUrl(user.email) : 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp'; const { data, error } = await supabase .from('users') .upsert({ username: user.username, id: user.id, email: user.email, password: user.password, province: user.province || '', ward: user.ward || '', role: user.role || 'user', favorites: user.favorites || [], history: user.history || [], avatar_url: user.avatar_url || defaultAvatar, created_at: user.created_at || user.createdAt || new Date().toISOString() }) .select() .single(); if (error) { console.error(`Error saving user ${user.username} to Supabase:`, error); throw error; } return data; } // --- RATINGS --- static async getRatingBySlug(slug) { const { data, error } = await supabase .from('ratings') .select('*') .eq('slug', slug) .maybeSingle(); if (error) { console.error(`Error fetching rating for ${slug}:`, error); return null; } return data; } static async saveRating(slug, count, totalScore, userRatings, ipRatings = {}) { const { data, error } = await supabase .from('ratings') .upsert({ slug, count, total_score: totalScore, user_ratings: userRatings, ip_ratings: ipRatings, updated_at: new Date().toISOString() }) .select() .single(); if (error) { console.error(`Error saving rating for ${slug}:`, error); throw error; } return data; } // --- MOVIE VIEWS & SUGGESTIONS --- static async incrementViews(slug, ip = '127.0.0.1') { try { const { data: existing, error: fetchError } = await supabase .from('movie_views') .select('views, ip_log') .eq('slug', slug) .maybeSingle(); if (fetchError) { console.error(`Error fetching views for ${slug}:`, fetchError); return null; } let views = existing?.views || 0; let ipLog = existing?.ip_log || {}; const now = new Date(); const lastViewTimeStr = ipLog[ip]; if (lastViewTimeStr) { const lastViewTime = new Date(lastViewTimeStr); const diffMs = now.getTime() - lastViewTime.getTime(); const diffHours = diffMs / (1000 * 60 * 60); // Nếu IP này đã xem phim này trong vòng 24 giờ qua, không tính thêm lượt xem if (diffHours < 24) { return existing; } } // Tăng lượt xem lên 1 views += 1; // Ghi nhận thời gian xem của IP này ipLog[ip] = now.toISOString(); // Dọn dẹp ipLog: chỉ giữ lại các địa chỉ IP đã xem trong vòng 24 giờ qua để tiết kiệm dung lượng JSON const cleanIpLog = {}; const limitMs = 24 * 60 * 60 * 1000; Object.entries(ipLog).forEach(([key, val]) => { if (typeof val === 'string') { const t = new Date(val).getTime(); if (now.getTime() - t < limitMs) { cleanIpLog[key] = val; } } }); const { data, error } = await supabase .from('movie_views') .upsert({ slug, views, ip_log: cleanIpLog, updated_at: now.toISOString() }) .select() .single(); if (error) { console.error(`Error incrementing views for ${slug}:`, error); return null; } return data; } catch (e) { console.error(`Error in incrementViews for ${slug}:`, e); return null; } } static async getTopViewedMovies(limit = 10) { try { const { data, error } = await supabase .from('movie_views') .select('slug, views') .order('views', { ascending: false }) .limit(limit); if (error) { console.error('Error fetching top viewed movies:', error); return []; } if (!data || !data.length) return []; const slugs = data.map(d => d.slug); const { data: movies, error: moviesError } = await supabase .from('movies') .select('slug, movie') .in('slug', slugs); if (moviesError) { console.error('Error fetching movie details for top viewed:', moviesError); return []; } return data .map(v => { const m = movies.find(x => x.slug === v.slug); if (!m) return null; return { ...m.movie, views: v.views }; }) .filter(Boolean); } catch (e) { console.error('Error in getTopViewedMovies:', e); return []; } } static async getTopRatedMovies(limit = 10) { try { const { data, error } = await supabase .from('ratings') .select('slug, count, total_score'); if (error) { console.error('Error fetching ratings for top rated:', error); return []; } if (!data || !data.length) return []; const sorted = data .map(r => ({ slug: r.slug, avg: r.count > 0 ? (Number(r.total_score) / r.count) : 0, count: r.count })) .sort((a, b) => b.avg - a.avg || b.count - a.count) .slice(0, limit); const slugs = sorted.map(s => s.slug); const { data: movies, error: moviesError } = await supabase .from('movies') .select('slug, movie') .in('slug', slugs); if (moviesError) { console.error('Error fetching movie details for top rated:', moviesError); return []; } return sorted .map(s => { const m = movies.find(x => x.slug === s.slug); if (!m) return null; return { ...m.movie, rating: s.avg.toFixed(1), vote_count: s.count }; }) .filter(Boolean); } catch (e) { console.error('Error in getTopRatedMovies:', e); return []; } } static async getMostFavoritedMovies(limit = 10) { try { const { data: users, error } = await supabase .from('users') .select('favorites'); if (error) { console.error('Error fetching users for favorites count:', error); return []; } const counts = {}; users?.forEach(u => { const favs = u.favorites || []; if (Array.isArray(favs)) { favs.forEach(f => { if (f.slug) { counts[f.slug] = (counts[f.slug] || 0) + 1; } }); } }); const sortedSlugs = Object.entries(counts) .sort((a, b) => b[1] - a[1]) .slice(0, limit) .map(entry => entry[0]); if (sortedSlugs.length === 0) return []; const { data: movies, error: moviesError } = await supabase .from('movies') .select('slug, movie') .in('slug', sortedSlugs); if (moviesError) { console.error('Error fetching movie details for most favorited:', moviesError); return []; } return sortedSlugs .map(slug => { const m = movies.find(x => x.slug === slug); if (!m) return null; return { ...m.movie, favorite_count: counts[slug] }; }) .filter(Boolean); } catch (e) { console.error('Error in getMostFavoritedMovies:', e); return []; } } // --- SETTINGS & EMAIL LOGS --- static async getSiteUrl(request = null) { let url = await this.getSetting('site_url'); if (!url) { if (process.env.SITE_URL) { url = process.env.SITE_URL; } else if (process.env.VERCEL_URL) { url = `https://${process.env.VERCEL_URL}`; } else if (request) { try { const host = request.headers.get('host') || request.url ? new URL(request.url).host : null; const proto = request.headers.get('x-forwarded-proto') || 'http'; url = host ? `${proto}://${host}` : 'http://localhost:4321'; } catch (e) { url = 'http://localhost:4321'; } } else { url = 'http://localhost:4321'; } } if (url.endsWith('/')) { url = url.slice(0, -1); } return url; } static async getSettings() { const { data, error } = await supabase .from('settings') .select('*'); if (error) { console.error('Error fetching settings:', error); return {}; } const settings = {}; (data || []).forEach(item => { settings[item.key] = item.value; }); return settings; } static async getSetting(key) { const { data, error } = await supabase .from('settings') .select('value') .eq('key', key) .maybeSingle(); if (error) { console.error(`Error fetching setting ${key}:`, error); return null; } return data ? data.value : null; } static async saveSetting(key, value) { const { data, error } = await supabase .from('settings') .upsert({ key, value, updated_at: new Date().toISOString() }) .select() .single(); if (error) { console.error(`Error saving setting ${key}:`, error); throw error; } return data; } static async saveSettings(settingsObj) { const upsertData = Object.entries(settingsObj).map(([key, value]) => ({ key, value, updated_at: new Date().toISOString() })); const { data, error } = await supabase .from('settings') .upsert(upsertData); if (error) { console.error('Error saving multiple settings:', error); throw error; } return data; } static async getEmailLogs() { const { data, error } = await supabase .from('email_logs') .select('*') .order('created_at', { ascending: false }); if (error) { console.error('Error fetching email logs:', error); return []; } return data; } static async saveEmailLog(toEmail, subject, body, templateName, status, errorMessage = null) { const { data, error } = await supabase .from('email_logs') .insert({ to_email: toEmail, subject, body, template_name: templateName, status, error_message: errorMessage, created_at: new Date().toISOString() }) .select() .single(); if (error) { console.error('Error saving email log:', error); throw error; } return data; } static async notifyNewEpisodes(slug, movieName, newEpisodes = []) { try { const { data: users, error } = await supabase .from('users') .select('id, username, email, favorites'); if (error) throw error; const interestedUsers = (users || []).filter(u => Array.isArray(u.favorites) && u.favorites.some(f => f.slug === slug) ); if (interestedUsers.length === 0) return; const settings = await this.getSettings(); if (!settings.smtp_user || !settings.smtp_pass) { console.log(`[Notification Engine]: SMTP not configured. Skipping notifications for ${movieName}.`); return; } console.log(`[Notification Engine]: Sending new episode notifications for ${movieName} to ${interestedUsers.length} users...`); const episodeListHtml = newEpisodes.map(ep => `
Welcome to TPHIMX.
`; } // Replace placeholders Object.entries(placeholders).forEach(([key, val]) => { const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g'); htmlContent = htmlContent.replace(regex, val); }); // Also dynamically replace site_url, site_name and copyright_year from configurations if present const siteUrl = settings.site_url || 'http://localhost:4321'; const siteName = settings.site_name || 'TPHIMX'; const currentYear = new Date().getFullYear(); const copyrightYear = currentYear > 2026 ? `2026 - ${currentYear}` : '2026'; htmlContent = htmlContent.replace(/{{\s*site_url\s*}}/g, siteUrl); htmlContent = htmlContent.replace(/{{\s*site_name\s*}}/g, siteName); htmlContent = htmlContent.replace(/{{\s*copyright_year\s*}}/g, copyrightYear); // Create transporter const transporter = nodemailer.default.createTransport({ host, port, secure, auth: { user, pass, }, tls: { rejectUnauthorized: false } }); // Send mail const info = await transporter.sendMail({ from: `"${fromName}" <${fromEmail}>`, to, subject, html: htmlContent }); console.log('Message sent: %s', info.messageId); // Log success to DB await this.saveEmailLog(to, subject, htmlContent, templateName, 'sent'); return { success: true, messageId: info.messageId }; } catch (error) { console.error('Error sending email:', error); // Log failure to DB try { await this.saveEmailLog(to, subject, `[Gửi thất bại] ${error.message}`, templateName, 'failed', error.message); } catch (logErr) { console.error('Failed to log email failure:', logErr); } throw error; } } // --- ZALO ACCESS --- static async getZaloAccessByToken(token) { const { data, error } = await supabase .from('zalo_access') .select('*') .eq('browser_token', token) .maybeSingle(); if (error) { console.error(`Error fetching Zalo access for token ${token}:`, error); return null; } return data; } static async createZaloAccessRequest(token, zaloNickname) { const { data, error } = await supabase .from('zalo_access') .upsert({ browser_token: token, zalo_nickname: zaloNickname, status: 'pending', updated_at: new Date().toISOString() }, { onConflict: 'browser_token' }) .select() .single(); if (error) { console.error(`Error creating Zalo access request for token ${token}:`, error); throw error; } return data; } static async updateZaloAccessStatus(id, status) { const { data, error } = await supabase .from('zalo_access') .update({ status, updated_at: new Date().toISOString() }) .eq('id', id) .select() .single(); if (error) { console.error(`Error updating Zalo access status for ${id}:`, error); throw error; } return data; } static async getAllZaloAccessRequests() { const { data, error } = await supabase .from('zalo_access') .select('*') .order('created_at', { ascending: false }); if (error) { console.error('Error fetching Zalo access requests:', error); return []; } return data; } static async getZaloAccessRequests() { return await this.getAllZaloAccessRequests(); } // --- REPORTS --- static async createReport(movieSlug, movieName, episodeName, errorType, description) { const { data, error } = await supabase .from('reports') .insert({ movie_slug: movieSlug, movie_name: movieName, episode_name: episodeName, error_type: errorType, description, status: 'pending', created_at: new Date().toISOString() }) .select() .single(); if (error) { console.error('Error creating report:', error); throw error; } return data; } static async getAllReports() { const { data, error } = await supabase .from('reports') .select('*') .order('created_at', { ascending: false }); if (error) { console.error('Error fetching reports:', error); return []; } return data; } static async getReports() { return await this.getAllReports(); } static async updateReportStatus(id, status) { const { data, error } = await supabase .from('reports') .update({ status }) .eq('id', id) .select() .single(); if (error) { console.error(`Error updating report status for ${id}:`, error); throw error; } return data; } // --- LOGS --- static async createLog(action, level, message, details = null) { const { data, error } = await supabase .from('logs') .insert({ action, level, message, details: typeof details === 'object' ? JSON.stringify(details) : details, created_at: new Date().toISOString() }) .select() .single(); if (error) { console.error('Error creating log:', error); } return data; } static async getAllLogs() { const { data, error } = await supabase .from('logs') .select('*') .order('created_at', { ascending: false }); if (error) { console.error('Error fetching logs:', error); return []; } return data; } }