Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions internal/database/speedtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ package database
import (
"context"
"fmt"

sq "github.com/Masterminds/squirrel"
"time"

"github.com/autobrr/netronome/internal/config"
"github.com/autobrr/netronome/internal/types"
Expand All @@ -26,12 +25,13 @@ func (s *service) SaveSpeedTest(ctx context.Context, result types.SpeedTestResul
"is_scheduled": result.IsScheduled,
}

// Use provided created_at if available, otherwise use current timestamp
if !result.CreatedAt.IsZero() {
data["created_at"] = result.CreatedAt
// Use provided created_at if available, otherwise default to current UTC time
if result.CreatedAt.IsZero() {
result.CreatedAt = time.Now().UTC()
} else {
data["created_at"] = sq.Expr("CURRENT_TIMESTAMP")
result.CreatedAt = result.CreatedAt.UTC()
}
data["created_at"] = result.CreatedAt

var id int64

Expand Down Expand Up @@ -151,6 +151,8 @@ func (s *service) GetSpeedTests(ctx context.Context, timeRange string, page, lim
if err != nil {
return nil, fmt.Errorf("failed to scan speed test result: %w", err)
}

result.CreatedAt = result.CreatedAt.UTC()
results = append(results, result)
}

Expand Down
33 changes: 23 additions & 10 deletions internal/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ func (s *service) Start(ctx context.Context) {
// - For exact times (e.g., "exact:14:00"), finds the next occurrence
// - For durations (e.g., "1h"), schedules from the current time
// - This prevents network flooding after downtime and ensures fresh data

func (s *service) initializeSchedules(ctx context.Context) {
schedules, err := s.db.GetSchedules(ctx)
if err != nil {
log.Error().Err(err).Msg("Error fetching schedules during initialization")
return
}

now := time.Now()
now := time.Now().UTC()
for _, schedule := range schedules {
if !schedule.Enabled {
continue
Expand Down Expand Up @@ -163,21 +164,26 @@ func (s *service) checkAndRunScheduledTests(ctx context.Context) {
return
}

now := time.Now()
now := time.Now().UTC()
for _, schedule := range schedules {
if !schedule.Enabled || schedule.NextRun.After(now) {
continue
}

scheduledStart := schedule.NextRun.UTC()
if schedule.NextRun.IsZero() {
scheduledStart = now
}

log.Info().
Int64("schedule_id", schedule.ID).
Time("next_run", schedule.NextRun).
Time("scheduled_start_utc", scheduledStart).
Str("interval", schedule.Interval).
Bool("is_iperf", schedule.Options.UseIperf).
Msg("Running scheduled test")

testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
go func(schedule types.Schedule, ctx context.Context, cancel context.CancelFunc) {
go func(schedule types.Schedule, scheduledStart time.Time, ctx context.Context, cancel context.CancelFunc) {
defer cancel()
schedule.Options.IsScheduled = true
result, err := s.speedtest.RunTest(ctx, &schedule.Options)
Expand All @@ -195,7 +201,7 @@ func (s *service) checkAndRunScheduledTests(ctx context.Context) {
Float64("upload_speed", result.UploadSpeed).
Msg("Scheduled test completed")

nextRun := s.calculateNextRun(schedule.Interval, now, false)
nextRun := s.calculateNextRun(schedule.Interval, scheduledStart, false)
if nextRun.IsZero() {
log.Error().
Int64("schedule_id", schedule.ID).
Expand All @@ -204,7 +210,13 @@ func (s *service) checkAndRunScheduledTests(ctx context.Context) {
return
}

schedule.LastRun = &now
nowUTC := time.Now().UTC()
if nextRun.Before(nowUTC) {
nextRun = s.calculateNextRun(schedule.Interval, nowUTC, false)
}

lastRun := nowUTC
schedule.LastRun = &lastRun
schedule.NextRun = nextRun

if err := s.db.UpdateSchedule(ctx, schedule); err != nil {
Expand All @@ -213,7 +225,7 @@ func (s *service) checkAndRunScheduledTests(ctx context.Context) {
Int64("schedule_id", schedule.ID).
Msg("Error updating schedule")
}
}(schedule, testCtx, cancel)
}(schedule, scheduledStart, testCtx, cancel)
}
}

Expand Down Expand Up @@ -385,7 +397,7 @@ func (s *service) initializePacketLossMonitors(ctx context.Context) {
return
}

now := time.Now()
now := time.Now().UTC()
for _, monitor := range monitors {
if !monitor.Enabled {
continue
Expand Down Expand Up @@ -559,7 +571,7 @@ func (s *service) checkAndRunPacketLossMonitors(ctx context.Context) {

// UpdateMonitorSchedule updates a monitor's last_run and next_run times after a test completes
func (s *service) UpdateMonitorSchedule(monitorID int64, interval string) error {
now := time.Now()
now := time.Now().UTC()

// Get the monitor to update
monitor, err := s.db.GetPacketLossMonitor(monitorID)
Expand All @@ -578,7 +590,8 @@ func (s *service) UpdateMonitorSchedule(monitorID int64, interval string) error
}

// Update the monitor
monitor.LastRun = &now
lastRun := now
monitor.LastRun = &lastRun
monitor.NextRun = &nextRun

log.Debug().
Expand Down
3 changes: 3 additions & 0 deletions internal/speedtest/result_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func (h *DefaultResultHandler) SaveResult(ctx context.Context, result *Result, t
saveCtx, saveCancel := context.WithTimeout(ctx, 10*time.Second)
defer saveCancel()

createdAt := time.Now().UTC()

dbResult, err := h.db.SaveSpeedTest(saveCtx, types.SpeedTestResult{
ServerName: result.Server,
ServerID: serverID,
Expand All @@ -71,6 +73,7 @@ func (h *DefaultResultHandler) SaveResult(ctx context.Context, result *Result, t
Latency: result.Latency,
Jitter: jitterPtr,
IsScheduled: opts.IsScheduled,
CreatedAt: createdAt,
})
if err != nil {
log.Error().Err(err).
Expand Down
23 changes: 18 additions & 5 deletions web/src/components/speedtest/DashboardTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React, { useState, useEffect, useMemo } from "react";
import { motion } from "motion/react";
import { ColumnDef } from "@tanstack/react-table";
import { SpeedTestResult, TimeRange } from "@/types/types";
import { SpeedHistoryChart } from "./SpeedHistoryChart";
import { MetricCard } from "@/components/common/MetricCard";
Expand All @@ -33,7 +34,7 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { DataTable } from "@/components/ui/data-table";
import { speedTestColumns, speedTestMobileColumns } from "./columns";
import { getSpeedTestColumns, getSpeedTestMobileColumns } from "./columns";
import {
DndContext,
closestCenter,
Expand All @@ -51,7 +52,7 @@ import {
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { formatDateTimeWithSettings } from "@/utils/timeSettings";
import { formatDateTimeWithSettings, useTimeSettings } from "@/utils/timeSettings";

interface DashboardTabProps {
latestTest: SpeedTestResult | null;
Expand Down Expand Up @@ -166,11 +167,17 @@ export const DashboardTab: React.FC<DashboardTabProps> = ({
onNavigateToSpeedTest,
onNavigateToVnstat,
}) => {
const { settings } = useTimeSettings();
const [displayCount, setDisplayCount] = useState(5);
const [isRecentTestsOpen, setIsRecentTestsOpen] = useState(() => {
const saved = localStorage.getItem("recent-tests-open");
return saved === null ? true : saved === "true";
});
const columns = useMemo(() => getSpeedTestColumns(settings), [settings]);
const mobileColumns = useMemo(
() => getSpeedTestMobileColumns(settings),
[settings]
);

// Initialize section order from localStorage or default
const [sectionOrder, setSectionOrder] = useState<string[]>(() => {
Expand Down Expand Up @@ -330,7 +337,7 @@ export const DashboardTab: React.FC<DashboardTabProps> = ({
<div>
Last test run:{" "}
{latestTest?.createdAt
? formatDateTimeWithSettings(latestTest.createdAt)
? formatDateTimeWithSettings(latestTest.createdAt, settings)
: "N/A"}
</div>
</div>
Expand Down Expand Up @@ -440,6 +447,8 @@ export const DashboardTab: React.FC<DashboardTabProps> = ({
setDisplayCount={setDisplayCount}
isRecentTestsOpen={isRecentTestsOpen}
setIsRecentTestsOpen={setIsRecentTestsOpen}
columns={columns}
mobileColumns={mobileColumns}
/>
</SortableItem>
);
Expand All @@ -462,6 +471,8 @@ interface DraggableRecentSpeedtestsProps {
setDisplayCount: (count: number | ((prev: number) => number)) => void;
isRecentTestsOpen: boolean;
setIsRecentTestsOpen: (open: boolean) => void;
columns: ColumnDef<SpeedTestResult>[];
mobileColumns: ColumnDef<SpeedTestResult>[];
dragHandleRef?: (node: HTMLElement | null) => void;
dragHandleListeners?: Record<string, (...args: unknown[]) => unknown>;
dragHandleClassName?: string;
Expand All @@ -474,6 +485,8 @@ const DraggableRecentSpeedtests: React.FC<DraggableRecentSpeedtestsProps> = ({
setDisplayCount,
isRecentTestsOpen,
setIsRecentTestsOpen,
columns,
mobileColumns,
dragHandleRef,
dragHandleListeners,
dragHandleClassName,
Expand Down Expand Up @@ -534,7 +547,7 @@ const DraggableRecentSpeedtests: React.FC<DraggableRecentSpeedtestsProps> = ({
{/* Desktop Table View */}
<div className="hidden md:block">
<DataTable
columns={speedTestColumns}
columns={columns}
data={displayedTests}
showPagination={false}
showColumnVisibility={true}
Expand All @@ -548,7 +561,7 @@ const DraggableRecentSpeedtests: React.FC<DraggableRecentSpeedtestsProps> = ({
{/* Mobile Card View */}
<div className="md:hidden">
<DataTable
columns={speedTestMobileColumns}
columns={mobileColumns}
data={displayedTests}
showPagination={false}
showColumnVisibility={false}
Expand Down
41 changes: 36 additions & 5 deletions web/src/components/speedtest/ScheduleManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,39 @@ const calculateNextRun = (
}
};

const formatExactTimeFromUTC = (time: string): string => {
const trimmed = time.trim();
const [hourStr, minuteStr] = trimmed.split(":");
const hours = Number(hourStr);
const minutes = Number(minuteStr);

if (
Number.isNaN(hours) ||
Number.isNaN(minutes) ||
hours < 0 ||
hours > 23 ||
minutes < 0 ||
minutes > 59
) {
return trimmed;
}

const today = new Date();
const candidate = new Date(
Date.UTC(
today.getUTCFullYear(),
today.getUTCMonth(),
today.getUTCDate(),
hours,
minutes,
0,
0
)
);

return formatTimeWithSettings(candidate);
};

export default function ScheduleManager({
servers,
selectedServers,
Expand Down Expand Up @@ -798,8 +831,8 @@ export default function ScheduleManager({
.substring(6)
.split(",");
if (times.length === 1) {
return formatTimeWithSettings(
`2000-01-01T${times[0]}:00`
return formatExactTimeFromUTC(
times[0]
);
} else {
return `${times.length} times`;
Expand Down Expand Up @@ -850,9 +883,7 @@ export default function ScheduleManager({
.substring(6)
.split(",")
.map((time) =>
formatTimeWithSettings(
`2000-01-01T${time}:00`
)
formatExactTimeFromUTC(time)
)
.join(", ")}
</span>
Expand Down
14 changes: 9 additions & 5 deletions web/src/components/speedtest/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
createRightAlignedSortableHeader,
} from "@/components/ui/data-table";
import { cn } from "@/lib/utils";
import { formatDateTimeWithSettings } from "@/utils/timeSettings";
import { TimeFormatSettings, formatDateTimeWithSettings } from "@/utils/timeSettings";

// Helper function to format speed
const formatSpeed = (speed: number) => {
Expand Down Expand Up @@ -46,15 +46,17 @@ const getTestTypeDisplayName = (testType: string) => {
}
};

export const speedTestColumns: ColumnDef<SpeedTestResult>[] = [
export const getSpeedTestColumns = (
settings?: TimeFormatSettings
): ColumnDef<SpeedTestResult>[] => [
{
accessorKey: "createdAt",
header: createSortableHeader("Date"),
cell: ({ row }) => {
const date = new Date(row.getValue("createdAt"));
return (
<span className="text-gray-700 dark:text-gray-300">
{formatDateTimeWithSettings(date)}
{formatDateTimeWithSettings(date, settings)}
</span>
);
},
Expand Down Expand Up @@ -144,7 +146,9 @@ export const speedTestColumns: ColumnDef<SpeedTestResult>[] = [
];

// Mobile-friendly columns with fewer fields
export const speedTestMobileColumns: ColumnDef<SpeedTestResult>[] = [
export const getSpeedTestMobileColumns = (
settings?: TimeFormatSettings
): ColumnDef<SpeedTestResult>[] => [
{
id: "summary",
cell: ({ row }) => {
Expand All @@ -167,7 +171,7 @@ export const speedTestMobileColumns: ColumnDef<SpeedTestResult>[] = [
</span>
</div>
<div className="text-gray-600 dark:text-gray-400 text-sm">
{formatDateTimeWithSettings(test.createdAt)}
{formatDateTimeWithSettings(test.createdAt, settings)}
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between items-center">
Expand Down
Loading