refactor: 优化电量监控系统主题与统计逻辑

- 添加深色模式适配,跟随系统主题自动切换
- 替换硬编码样式为CSS变量实现主题统一管理
- 重构后端统计计算逻辑,优化当日用电估算方式
- 更新页面标题与站点图标,调整登录页样式
- 移除冗余的data目录拷贝配置
This commit is contained in:
EchoZenith
2026-05-23 01:38:49 +08:00
parent e1b4a0ec5d
commit 224233421d
8 changed files with 175 additions and 115 deletions

View File

@@ -30,7 +30,6 @@ ENV TZ=Asia/Shanghai
COPY --from=builder /app/node_modules ./node_modules
COPY server.js .
COPY --from=client-builder /app/dist ./client/dist
COPY data ./data
EXPOSE 3000

View File

@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>电费监控系统</title>
<link rel="icon" type="image/svg+xml" href="/lightning.svg" />
<title>智能电量监控</title>
</head>
<body>
<div id="root"></div>

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="40" height="40" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 4H37L26 18H41L17 44L22 25H8L19 4Z" fill="#4a90e2" stroke="#4a90e2" stroke-width="4" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -4,6 +4,53 @@ import Dashboard from './pages/Dashboard';
import Login from './pages/Login';
import { checkAuth } from './api';
const cssVars = `
[data-theme="light"], [data-theme="dark"] {
--bg-body: #f5f7fa;
--bg-card: #ffffff;
--bg-chart: #fafbfc;
--bg-item: #f8f9fa;
--bg-item-blue: #e8f4fd;
--bg-item-purple: #f0e8fd;
--bg-item-green: #e8fdf4;
--text-primary: #1a1a1a;
--text-secondary: #666;
--text-tertiary: #999;
--text-placeholder: #999;
--border-light: #e8e8e8;
--shadow-card: 0 2px 8px rgba(0,0,0,0.08);
--color-blue: #4a90e2;
--color-orange: #f39c12;
--color-purple: #9b59b6;
--color-green: #27ae60;
--tab-bg: #f0f2f5;
--tab-bg-active: #333;
--tab-text: #666;
--tab-text-active: white;
}
[data-theme="dark"] {
--bg-body: #141414;
--bg-card: #1f1f1f;
--bg-chart: #2a2a2a;
--bg-item: #2a2a2a;
--bg-item-blue: rgba(74, 144, 226, 0.15);
--bg-item-purple: rgba(155, 89, 182, 0.15);
--bg-item-green: rgba(39, 174, 96, 0.15);
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-tertiary: #808080;
--border-light: #333;
--shadow-card: none;
--tab-bg: #2a2a2a;
--tab-bg-active: #4a90e2;
--tab-text: #808080;
--tab-text-active: white;
}
body { margin: 0; background: var(--bg-body); }
`;
export default function App() {
const [authed, setAuthed] = useState(null);
@@ -13,11 +60,17 @@ export default function App() {
if (authed === null) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: 'var(--bg-body)' }}>
<style>{cssVars}</style>
<Spin size="large" />
</div>
);
}
return authed ? <Dashboard onLogout={() => setAuthed(false)} /> : <Login onLogin={() => setAuthed(true)} />;
return (
<>
<style>{cssVars}</style>
{authed ? <Dashboard onLogout={() => setAuthed(false)} /> : <Login onLogin={() => setAuthed(true)} />}
</>
);
}

View File

@@ -1,17 +1,36 @@
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { ConfigProvider, theme } from 'antd';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<ConfigProvider
theme={{
token: {
colorPrimary: '#4a90e2',
borderRadius: 8,
fontSize: 14,
},
}}
>
<App />
</ConfigProvider>
);
function Root() {
const [isDark, setIsDark] = useState(() =>
window.matchMedia('(prefers-color-scheme: dark)').matches
);
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => setIsDark(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return (
<ConfigProvider
theme={{
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: '#4a90e2',
borderRadius: 8,
fontSize: 14,
},
}}
>
<div data-theme={isDark ? 'dark' : 'light'}>
<App />
</div>
</ConfigProvider>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<Root />);

View File

@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Button, Spin, message } from 'antd';
import { LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
import { BugOutlined } from '@ant-design/icons';
import { LogoutOutlined, ReloadOutlined, BugOutlined } from '@ant-design/icons';
import { Lightning, ChartLine, ChartHistogram, Timer } from '@icon-park/react';
import { Chart, registerables } from 'chart.js';
import { fetchCurrent, fetchHistory, triggerCollect, logout } from '../api';
@@ -278,7 +277,7 @@ export default function Dashboard({ onLogout }) {
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: '#f5f7fa' }}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: 'var(--bg-body)' }}>
<Spin size="large" />
</div>
);
@@ -288,21 +287,21 @@ export default function Dashboard({ onLogout }) {
const stats = historyData?.stats || {};
const statKey = days === 7 ? '7' : days === 15 ? '15' : '30';
const avgDaily = stats[`avgDaily${statKey}`];
const avgPower = stats[`avgPower${statKey}`];
const estimatedDays = stats[`estimatedDays${statKey}`];
const todayUsage = currentData?.todayUsage ?? 0;
const todayCost = currentData?.todayCost ?? 0;
const todayAvgPower = currentData?.todayAvgPower ?? null;
return (
<div className="d-body" style={{ background: '#f5f7fa', minHeight: '100vh', padding: 20 }}>
<div className="d-body" style={{ background: 'var(--bg-body)', minHeight: '100vh', padding: 20 }}>
<style>{`${styles}
body { margin: 0; }
.d-container { border-radius: 12px !important; box-shadow: none !important; }
`}</style>
<div className="d-container" style={{ maxWidth: 1200, margin: '0 auto', background: 'white', padding: 32, borderRadius: 12 }}>
<div className="d-container" style={{ maxWidth: 1200, margin: '0 auto', background: 'var(--bg-card)', padding: 32, borderRadius: 12 }}>
<div className="d-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 24, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 24, fontWeight: 600, color: 'var(--text-primary)' }}>
<Lightning theme="filled" size="28" fill="#4a90e2" style={{ display: 'flex' }} />
智能电量监控
</div>
@@ -313,9 +312,9 @@ body { margin: 0; }
loading={collecting}
size="small"
style={{
border: '1px solid #d9d9d9',
border: '1px solid var(--border-light)',
borderRadius: 20,
color: '#666',
color: 'var(--text-secondary)',
fontSize: 13,
}}
>
@@ -326,9 +325,9 @@ body { margin: 0; }
onClick={handleTestNotify}
size="small"
style={{
border: '1px solid #d9d9d9',
border: '1px solid var(--border-light)',
borderRadius: 20,
color: '#666',
color: 'var(--text-secondary)',
fontSize: 13,
}}
>
@@ -339,9 +338,9 @@ body { margin: 0; }
onClick={handleLogout}
size="small"
style={{
border: '1px solid #d9d9d9',
border: '1px solid var(--border-light)',
borderRadius: 20,
color: '#666',
color: 'var(--text-secondary)',
fontSize: 13,
}}
>
@@ -352,28 +351,28 @@ body { margin: 0; }
<div className="d-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24, marginBottom: 32 }}>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>剩余电量</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a', display: 'flex', alignItems: 'baseline', gap: 6 }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>剩余电量</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: 6 }}>
{current?.surplus?.toFixed(1) ?? '--'}
<span style={{ fontSize: 16, fontWeight: 400, color: '#999' }}>kWh</span>
<span style={{ fontSize: 16, fontWeight: 400, color: 'var(--text-tertiary)' }}>kWh</span>
</div>
</div>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>剩余余额</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a' }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>剩余余额</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)' }}>
¥{current?.amount?.toFixed(2) ?? '--'}
</div>
</div>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>今日用电</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a', display: 'flex', alignItems: 'baseline', gap: 6 }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>今日用电</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)', display: 'flex', alignItems: 'baseline', gap: 6 }}>
{todayUsage.toFixed(1)}
<span style={{ fontSize: 16, fontWeight: 400, color: '#999' }}>kWh</span>
<span style={{ fontSize: 16, fontWeight: 400, color: 'var(--text-tertiary)' }}>kWh</span>
</div>
</div>
<div className="d-stats-card" style={{ padding: 20 }}>
<div style={{ fontSize: 14, color: '#999', marginBottom: 8 }}>今日电费</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: '#1a1a1a' }}>
<div style={{ fontSize: 14, color: 'var(--text-tertiary)', marginBottom: 8 }}>今日电费</div>
<div className="d-stats-value" style={{ fontSize: 36, fontWeight: 700, color: 'var(--text-primary)' }}>
¥{todayCost.toFixed(2)}
</div>
</div>
@@ -381,15 +380,15 @@ body { margin: 0; }
<div className="d-section-gap" style={{ marginBottom: 32 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>
<ChartLine theme="filled" size="20" fill="#4a90e2" style={{ display: 'flex' }} />
今日用电趋势
<span style={{ fontSize: 13, color: '#999', fontWeight: 400 }}>(每时段)</span>
<span style={{ fontSize: 13, color: 'var(--text-tertiary)', fontWeight: 400 }}>(每时段)</span>
</div>
</div>
<div className="d-chart" style={{ position: 'relative', height: 280, background: '#fafbfc', borderRadius: 8, padding: 20 }}>
<div className="d-chart" style={{ position: 'relative', height: 280, background: 'var(--bg-chart)', borderRadius: 8, padding: 20 }}>
{(!currentData?.todayRecords || currentData.todayRecords.length === 0) ? (
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: '#999' }}>暂无今日数据</div>
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: 'var(--text-tertiary)' }}>暂无今日数据</div>
) : null}
<canvas ref={hourlyChartRef} />
</div>
@@ -397,7 +396,7 @@ body { margin: 0; }
<div className="d-section-gap" style={{ marginBottom: 32 }}>
<div className="d-trend-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' }}>
<ChartHistogram theme="filled" size="20" fill="#4a90e2" style={{ display: 'flex' }} />
用电 & 剩余电量趋势
</div>
@@ -409,8 +408,8 @@ body { margin: 0; }
style={{
padding: '8px 20px',
border: 'none',
background: days === d ? '#333' : '#f0f2f5',
color: days === d ? 'white' : '#666',
background: days === d ? 'var(--tab-bg-active)' : 'var(--tab-bg)',
color: days === d ? 'var(--tab-text-active)' : 'var(--tab-text)',
borderRadius: 20,
cursor: 'pointer',
fontSize: 14,
@@ -422,44 +421,44 @@ body { margin: 0; }
))}
</div>
</div>
<div className="d-chart" style={{ position: 'relative', height: 280, background: '#fafbfc', borderRadius: 8, padding: 20 }}>
<div className="d-chart" style={{ position: 'relative', height: 280, background: 'var(--bg-chart)', borderRadius: 8, padding: 20 }}>
{(!historyData?.dailyRecords || historyData.dailyRecords.length === 0) ? (
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: '#999' }}>暂无历史数据</div>
<div className="d-chart-empty" style={{ textAlign: 'center', paddingTop: 110, color: 'var(--text-tertiary)' }}>暂无历史数据</div>
) : null}
<canvas ref={trendChartRef} />
</div>
</div>
<div className="d-bottom" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: '#e8f4fd' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-item)', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--bg-item-blue)' }}>
<ChartHistogram theme="filled" size="22" fill="#4a90e2" style={{ display: 'flex' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>日均耗电</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>日均耗电</div>
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
{avgDaily != null ? `${avgDaily.toFixed(1)} kWh` : '--'}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: '#f0e8fd' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-item)', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--bg-item-purple)' }}>
<Lightning theme="filled" size="22" fill="#9b59b6" style={{ display: 'flex' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>平均功率</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
{todayAvgPower != null ? `${(todayAvgPower * 1000).toFixed(0)} W` : '--'}
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>平均功率</div>
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
{avgPower != null ? `${(avgPower * 1000).toFixed(0)} W` : '--'}
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8f9fa', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: '#e8fdf4' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-item)', borderRadius: 8 }}>
<div style={{ width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, background: 'var(--bg-item-green)' }}>
<Timer theme="filled" size="22" fill="#27ae60" style={{ display: 'flex' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, color: '#666', marginBottom: 2 }}>预计可用</div>
<div style={{ fontSize: 18, fontWeight: 600, color: '#1a1a1a' }}>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 2 }}>预计可用</div>
<div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)' }}>
{estimatedDays != null ? `${estimatedDays.toFixed(0)}` : '--'}
</div>
</div>

View File

@@ -29,7 +29,7 @@ export default function Login({ onLogin }) {
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: '#f5f7fa',
background: 'var(--bg-body)',
padding: 0,
}}>
<Card
@@ -37,7 +37,6 @@ export default function Login({ onLogin }) {
width: '100%',
maxWidth: 400,
borderRadius: 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}}
styles={{ body: { padding: '48px 40px', textAlign: 'center' } }}
>
@@ -47,11 +46,11 @@ export default function Login({ onLogin }) {
margin: 0,
fontSize: 24,
fontWeight: 600,
color: '#1a1a1a',
color: 'var(--text-primary)',
}}>
智能电量监控
</h2>
<p style={{ color: '#999', fontSize: 14, marginTop: 8, marginBottom: 0 }}>
<p style={{ color: 'var(--text-tertiary)', fontSize: 14, marginTop: 8, marginBottom: 0 }}>
请登录后查看用电数据
</p>
</div>
@@ -66,7 +65,7 @@ export default function Login({ onLogin }) {
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined style={{ color: '#999' }} />}
prefix={<UserOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="用户名"
size="large"
/>
@@ -77,7 +76,7 @@ export default function Login({ onLogin }) {
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#999' }} />}
prefix={<LockOutlined style={{ color: 'var(--text-tertiary)' }} />}
placeholder="密码"
size="large"
/>

View File

@@ -314,63 +314,52 @@ app.get('/api/history', requireAuth, (req, res) => {
const { dailyRecords } = calculateDailyUsage(allRecords);
const latest = allRecords.length > 0 ? allRecords[allRecords.length - 1] : null;
const todayStr = getLocalDateStr(new Date());
const historyDailyRecords = dailyRecords.filter(d => d.date !== todayStr);
const calcStats = (dailyRecords, n) => {
const records = dailyRecords.slice(-n);
if (records.length < 1) return { avgDaily: null, avgPower: null, estimatedDays: null };
const totalUsageLast7Days = historyDailyRecords.slice(-7).reduce((sum, d) => sum + d.usage, 0);
const totalUsageLast15Days = historyDailyRecords.slice(-15).reduce((sum, d) => sum + d.usage, 0);
const totalUsageLast30Days = historyDailyRecords.slice(-30).reduce((sum, d) => sum + d.usage, 0);
const todayStr = getLocalDateStr(new Date());
let totalUsage = 0;
let dayCount = 0;
const totalHours7 = historyDailyRecords.slice(-7).reduce((sum, d) => sum + d.hoursSpan, 0);
const totalHours15 = historyDailyRecords.slice(-15).reduce((sum, d) => sum + d.hoursSpan, 0);
const totalHours30 = historyDailyRecords.slice(-30).reduce((sum, d) => sum + d.hoursSpan, 0);
records.forEach(r => {
if (r.date === todayStr) {
const elapsed = Math.max(1, r.hoursSpan + 1);
const normalized = Math.round((r.usage / elapsed) * 24 * 100) / 100;
totalUsage += normalized;
} else {
totalUsage += r.usage;
}
dayCount++;
});
const days7 = Math.min(7, historyDailyRecords.length);
const days15 = Math.min(15, historyDailyRecords.length);
const days30 = Math.min(30, historyDailyRecords.length);
const avgDaily = Math.round((totalUsage / dayCount) * 100) / 100;
const avgPower = Math.round((avgDaily / 24) * 1000) / 1000;
const estimatedDays = avgDaily > 0 && latest
? Math.round(latest.surplus / avgDaily * 10) / 10
: null;
const avgDaily7 = days7 >= 1 ? Math.round((totalUsageLast7Days / days7) * 100) / 100 : null;
const avgDaily15 = days15 >= 1 ? Math.round((totalUsageLast15Days / days15) * 100) / 100 : null;
const avgDaily30 = days30 >= 1 ? Math.round((totalUsageLast30Days / days30) * 100) / 100 : null;
return { avgDaily, avgPower, estimatedDays };
};
const avgPower7 = totalHours7 >= 1
? Math.round((totalUsageLast7Days / totalHours7) * 1000) / 1000
: null;
const avgPower15 = totalHours15 >= 1
? Math.round((totalUsageLast15Days / totalHours15) * 1000) / 1000
: null;
const avgPower30 = totalHours30 >= 1
? Math.round((totalUsageLast30Days / totalHours30) * 1000) / 1000
: null;
const estimatedDays7 = avgDaily7 && avgDaily7 > 0 && latest
? Math.round(latest.surplus / avgDaily7 * 10) / 10
: null;
const estimatedDays15 = avgDaily15 && avgDaily15 > 0 && latest
? Math.round(latest.surplus / avgDaily15 * 10) / 10
: null;
const estimatedDays30 = avgDaily30 && avgDaily30 > 0 && latest
? Math.round(latest.surplus / avgDaily30 * 10) / 10
: null;
const s7 = calcStats(dailyRecords, 7);
const s15 = calcStats(dailyRecords, 15);
const s30 = calcStats(dailyRecords, 30);
res.json({
success: true,
current: latest,
dailyRecords: dailyRecords.slice(-30),
stats: {
avgDaily7,
avgDaily15,
avgDaily30,
avgPower7,
avgPower15,
avgPower30,
estimatedDays7,
estimatedDays15,
estimatedDays30
avgDaily7: s7.avgDaily,
avgDaily15: s15.avgDaily,
avgDaily30: s30.avgDaily,
avgPower7: s7.avgPower,
avgPower15: s15.avgPower,
avgPower30: s30.avgPower,
estimatedDays7: s7.estimatedDays,
estimatedDays15: s15.estimatedDays,
estimatedDays30: s30.estimatedDays,
}
});
});