feat: node management enhancements and UX improvements (#297)

* feat(nodes): add renewal cycle and auto-advance scheduling

- Add renewal_cycle field to nodes for tracking paid vs free cycles
- Implement auto-advance scheduling when renewal is processed
- Add migration test for renewal_cycle column
- Update dashboard to show renewal cycle count
- Add renewal status display on node detail page

* fix(frontend): show rule count in compact view mode for forward pages

* style(frontend): format code and improve node card layout

Entire-Checkpoint: 08b760b76538

* fix: restore renewal.ts from main to resolve build errors
This commit is contained in:
sagit
2026-03-09 10:01:55 +08:00
committed by GitHub
parent 413081f72a
commit de9ab51def
50 changed files with 26959 additions and 371 deletions
@@ -231,14 +231,20 @@ const normalizeNodeExpiryReminders = (items: NodeApiItem[]) => {
}))
.filter((item) => {
if (!item.expiryTime || !item.renewalCycle) return false;
const snapshot = getNodeRenewalSnapshot(item.expiryTime, item.renewalCycle);
const snapshot = getNodeRenewalSnapshot(
item.expiryTime,
item.renewalCycle,
);
if (!snapshot.nextDueTime) return false;
return snapshot.nextDueTime <= now + warningWindowMs;
})
.sort((a, b) => {
const aDue = getNodeRenewalSnapshot(a.expiryTime, a.renewalCycle).nextDueTime || 0;
const bDue = getNodeRenewalSnapshot(b.expiryTime, b.renewalCycle).nextDueTime || 0;
const aDue =
getNodeRenewalSnapshot(a.expiryTime, a.renewalCycle).nextDueTime || 0;
const bDue =
getNodeRenewalSnapshot(b.expiryTime, b.renewalCycle).nextDueTime || 0;
return aDue - bDue;
});
@@ -265,31 +271,34 @@ export const useDashboardData = (): DashboardDataState => {
const packageRequestInFlightRef = useRef(false);
const nodeExpiryRequestInFlightRef = useRef(false);
const applyPackageData = useCallback((data: {
userInfo?: DashboardUserInfo;
tunnelPermissions?: DashboardUserTunnel[];
forwards?: ForwardApiItem[];
statisticsFlows?: DashboardStatisticsFlow[];
}) => {
const normalizedTunnelPermissions = normalizeTunnelPermissions(
data.tunnelPermissions || [],
);
const normalizedForwards = normalizeForwards(data.forwards || []);
const applyPackageData = useCallback(
(data: {
userInfo?: DashboardUserInfo;
tunnelPermissions?: DashboardUserTunnel[];
forwards?: ForwardApiItem[];
statisticsFlows?: DashboardStatisticsFlow[];
}) => {
const normalizedTunnelPermissions = normalizeTunnelPermissions(
data.tunnelPermissions || [],
);
const normalizedForwards = normalizeForwards(data.forwards || []);
if (!isMountedRef.current) {
return;
}
if (!isMountedRef.current) {
return;
}
setUserInfo(data.userInfo || ({} as DashboardUserInfo));
setUserTunnels(normalizedTunnelPermissions);
setForwardList(normalizedForwards);
setStatisticsFlows(data.statisticsFlows || []);
setUserInfo(data.userInfo || ({} as DashboardUserInfo));
setUserTunnels(normalizedTunnelPermissions);
setForwardList(normalizedForwards);
setStatisticsFlows(data.statisticsFlows || []);
checkExpirationNotifications(
data.userInfo || ({} as DashboardUserInfo),
normalizedTunnelPermissions,
);
}, []);
checkExpirationNotifications(
data.userInfo || ({} as DashboardUserInfo),
normalizedTunnelPermissions,
);
},
[],
);
const loadPackageData = useCallback(
async ({ silent = false, notifyOnError = false } = {}) => {