190 lines
4.5 KiB
TypeScript
190 lines
4.5 KiB
TypeScript
import { supabase } from './supabase';
|
|
|
|
// Types
|
|
|
|
export interface HistoryRow {
|
|
id?: number;
|
|
created_at?: string;
|
|
v_batt: number;
|
|
battery_pct: number;
|
|
v_in?: number;
|
|
v_out?: number;
|
|
line_state: number;
|
|
power_on: boolean;
|
|
}
|
|
|
|
export interface EventRow {
|
|
id?: number;
|
|
created_at?: string;
|
|
message: string;
|
|
severity: 'info' | 'success' | 'warning' | 'error';
|
|
}
|
|
|
|
let dbUnavailable = false;
|
|
let warnedUnavailable = false;
|
|
let retryTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function isNetworkError(error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
return /failed to fetch|fetch failed|networkerror|load failed/i.test(message);
|
|
}
|
|
|
|
function logDbIssue(operation: string, error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error ?? 'Unknown error');
|
|
|
|
if (isNetworkError(error)) {
|
|
dbUnavailable = true;
|
|
|
|
if (!warnedUnavailable) {
|
|
warnedUnavailable = true;
|
|
console.warn(
|
|
`[DB] Supabase tidak bisa dijangkau (${message}). ` +
|
|
'Dashboard tetap berjalan tanpa sinkronisasi database. ' +
|
|
'Periksa NEXT_PUBLIC_SUPABASE_URL dan koneksi jaringan.'
|
|
);
|
|
}
|
|
// Auto-retry after 60 seconds
|
|
if (!retryTimeoutId) {
|
|
retryTimeoutId = setTimeout(() => {
|
|
dbUnavailable = false;
|
|
warnedUnavailable = false;
|
|
retryTimeoutId = null;
|
|
console.info('[DB] Retrying Supabase connection...');
|
|
}, 60000);
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.warn(`[DB] ${operation} error:`, message);
|
|
}
|
|
|
|
function shouldSkipDb() {
|
|
return dbUnavailable;
|
|
}
|
|
|
|
// History
|
|
|
|
/**
|
|
* Simpan satu titik data history ke Supabase.
|
|
*/
|
|
export async function insertHistoryPoint(row: HistoryRow) {
|
|
if (shouldSkipDb()) return;
|
|
|
|
try {
|
|
const { error } = await supabase.from('ups_history').insert([row]);
|
|
if (error) logDbIssue('insertHistoryPoint', error);
|
|
} catch (error) {
|
|
logDbIssue('insertHistoryPoint', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ambil N data history terakhir dari Supabase, diurutkan dari terlama ke terbaru.
|
|
*/
|
|
export async function fetchHistory(limit = 100): Promise<HistoryRow[]> {
|
|
if (shouldSkipDb()) return [];
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('ups_history')
|
|
.select('*')
|
|
.order('created_at', { ascending: false })
|
|
.limit(limit);
|
|
|
|
if (error) {
|
|
logDbIssue('fetchHistory', error);
|
|
return [];
|
|
}
|
|
return (data as HistoryRow[]).reverse();
|
|
} catch (error) {
|
|
logDbIssue('fetchHistory', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ambil data history dari Supabase dalam rentang tanggal tertentu.
|
|
* @param from ISO string (misal "2024-05-01T00:00:00.000Z")
|
|
* @param to ISO string (misal "2024-05-07T23:59:59.999Z")
|
|
* @param limit maksimum jumlah baris yang diambil
|
|
*/
|
|
export async function fetchHistoryByRange(from: string, to: string, limit = 2000): Promise<HistoryRow[]> {
|
|
if (shouldSkipDb()) return [];
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('ups_history')
|
|
.select('*')
|
|
.gte('created_at', from)
|
|
.lte('created_at', to)
|
|
.order('created_at', { ascending: true })
|
|
.limit(limit);
|
|
|
|
if (error) {
|
|
logDbIssue('fetchHistoryByRange', error);
|
|
return [];
|
|
}
|
|
return data as HistoryRow[];
|
|
} catch (error) {
|
|
logDbIssue('fetchHistoryByRange', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Events
|
|
|
|
/**
|
|
* Simpan satu event log ke Supabase.
|
|
*/
|
|
export async function insertEvent(row: EventRow) {
|
|
if (shouldSkipDb()) return;
|
|
|
|
try {
|
|
const { error } = await supabase.from('ups_events').insert([row]);
|
|
if (error) logDbIssue('insertEvent', error);
|
|
} catch (error) {
|
|
logDbIssue('insertEvent', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ambil N event log terakhir dari Supabase, diurutkan dari terbaru ke terlama.
|
|
*/
|
|
export async function fetchEvents(limit = 200): Promise<EventRow[]> {
|
|
if (shouldSkipDb()) return [];
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('ups_events')
|
|
.select('*')
|
|
.order('created_at', { ascending: false })
|
|
.limit(limit);
|
|
|
|
if (error) {
|
|
logDbIssue('fetchEvents', error);
|
|
return [];
|
|
}
|
|
return data as EventRow[];
|
|
} catch (error) {
|
|
logDbIssue('fetchEvents', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hapus semua event log dari Supabase.
|
|
*/
|
|
export async function clearAllEvents() {
|
|
if (shouldSkipDb()) return;
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('ups_events')
|
|
.delete()
|
|
.gte('id', 0); // delete all rows
|
|
if (error) logDbIssue('clearAllEvents', error);
|
|
} catch (error) {
|
|
logDbIssue('clearAllEvents', error);
|
|
}
|
|
}
|