Browse Source

remove time limit feature

master
Stephanie Gredell 4 weeks ago
parent
commit
e035cb472d
  1. 8
      backend/src/controllers/magicCode.controller.ts
  2. 67
      backend/src/controllers/settings.controller.ts
  3. 73
      backend/src/controllers/settingsProfiles.controller.ts
  4. 10
      backend/src/routes/settings.routes.ts
  5. 8
      backend/src/services/connection-tracker.service.ts
  6. 12
      frontend/src/components/MagicCodeInput/MagicCodeInput.tsx
  7. 215
      frontend/src/components/TimeLimitManager/TimeLimitManager.tsx
  8. 155
      frontend/src/hooks/useTimeLimit.ts
  9. 11
      frontend/src/services/apiClient.ts
  10. 8
      frontend/src/services/connectionTracker.ts
  11. 2
      frontend/src/services/magicCodeService.ts
  12. 210
      frontend/src/services/timeLimitService.ts
  13. 1
      frontend/src/types/api.ts

8
backend/src/controllers/magicCode.controller.ts

@ -49,11 +49,8 @@ export async function getSettingsByCode(req: Request, res: Response) { @@ -49,11 +49,8 @@ export async function getSettingsByCode(req: Request, res: Response) {
const key = row.setting_key as string;
const value = row.setting_value as string;
// Parse numeric values
if (key === 'daily_time_limit_minutes') {
settings[key] = parseInt(value, 10);
} else if (key === 'enabled_apps') {
// Parse JSON array
// Parse JSON array for enabled_apps
if (key === 'enabled_apps') {
try {
settings[key] = JSON.parse(value);
} catch (e) {
@ -75,7 +72,6 @@ export async function getSettingsByCode(req: Request, res: Response) { @@ -75,7 +72,6 @@ export async function getSettingsByCode(req: Request, res: Response) {
data: {
magicCode: profile.magic_code,
settings,
dailyTimeLimit: settings.daily_time_limit_minutes || null,
enabledApps
}
});

67
backend/src/controllers/settings.controller.ts

@ -4,63 +4,6 @@ import { getSetting, setSetting } from '../config/database.js'; @@ -4,63 +4,6 @@ import { getSetting, setSetting } from '../config/database.js';
import { connectionTracker } from '../services/connection-tracker.service.js';
import crypto from 'crypto';
export async function getTimeLimit(req: AuthRequest, res: Response) {
try {
const limit = await getSetting('daily_time_limit_minutes');
const defaultLimit = 1; // Default 1 minute for testing
res.json({
success: true,
data: {
dailyLimit: limit ? parseInt(limit, 10) : defaultLimit
}
});
} catch (error: any) {
console.error('Get time limit error:', error);
res.status(500).json({
success: false,
error: {
code: 'GET_TIME_LIMIT_ERROR',
message: 'Error fetching time limit'
}
});
}
}
export async function setTimeLimit(req: AuthRequest, res: Response) {
try {
const { dailyLimit } = req.body;
if (!dailyLimit || typeof dailyLimit !== 'number' || dailyLimit < 1) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_LIMIT',
message: 'Daily limit must be a number greater than 0'
}
});
}
await setSetting('daily_time_limit_minutes', dailyLimit.toString());
res.json({
success: true,
data: {
dailyLimit
}
});
} catch (error: any) {
console.error('Set time limit error:', error);
res.status(500).json({
success: false,
error: {
code: 'SET_TIME_LIMIT_ERROR',
message: 'Error setting time limit'
}
});
}
}
/**
* Heartbeat endpoint - clients ping this to indicate they're active
* Public endpoint - no auth required
@ -85,23 +28,19 @@ export async function heartbeat(req: AuthRequest, res: Response) { @@ -85,23 +28,19 @@ export async function heartbeat(req: AuthRequest, res: Response) {
});
}
// Get route, video info, and time limit usage from request body
// Get route and video info from request body
const route = req.body.route || '/';
const videoTitle = req.body.videoTitle;
const videoChannel = req.body.videoChannel;
const timeUsed = req.body.timeUsed;
const dailyLimit = req.body.dailyLimit;
// Register heartbeat (with user info if authenticated, current route, video info, and time limit usage)
// Register heartbeat (with user info if authenticated, current route, and video info)
connectionTracker.heartbeat(
sessionId,
req.userId,
req.username,
route,
videoTitle,
videoChannel,
timeUsed,
dailyLimit
videoChannel
);
res.json({

73
backend/src/controllers/settingsProfiles.controller.ts

@ -73,9 +73,6 @@ export async function getAllProfiles(req: AuthRequest, res: Response) { @@ -73,9 +73,6 @@ export async function getAllProfiles(req: AuthRequest, res: Response) {
return {
...profile,
dailyTimeLimit: profile.settings.daily_time_limit_minutes
? parseInt(profile.settings.daily_time_limit_minutes, 10)
: null,
enabledApps
};
});
@ -168,9 +165,6 @@ export async function getProfile(req: AuthRequest, res: Response) { @@ -168,9 +165,6 @@ export async function getProfile(req: AuthRequest, res: Response) {
updatedAt: profile.updated_at,
isActive: profile.is_active === 1,
settings,
dailyTimeLimit: settings.daily_time_limit_minutes
? parseInt(settings.daily_time_limit_minutes, 10)
: null,
enabledApps
}
});
@ -198,7 +192,7 @@ export async function createProfile(req: AuthRequest, res: Response) { @@ -198,7 +192,7 @@ export async function createProfile(req: AuthRequest, res: Response) {
});
}
const { name, description, dailyTimeLimit, enabledApps } = req.body;
const { name, description, enabledApps } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({
@ -210,16 +204,6 @@ export async function createProfile(req: AuthRequest, res: Response) { @@ -210,16 +204,6 @@ export async function createProfile(req: AuthRequest, res: Response) {
});
}
if (!dailyTimeLimit || typeof dailyTimeLimit !== 'number' || dailyTimeLimit < 1) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_TIME_LIMIT',
message: 'Daily time limit must be a number greater than 0'
}
});
}
// Generate unique magic code
const magicCode = await generateMagicCode();
@ -234,15 +218,6 @@ export async function createProfile(req: AuthRequest, res: Response) { @@ -234,15 +218,6 @@ export async function createProfile(req: AuthRequest, res: Response) {
const profileId = Number(profileResult.lastInsertRowid);
// Add settings
await db.execute({
sql: `
INSERT INTO settings_profile_values (profile_id, setting_key, setting_value)
VALUES (?, ?, ?)
`,
args: [profileId, 'daily_time_limit_minutes', dailyTimeLimit.toString()]
});
// Add enabled apps if provided
if (enabledApps && Array.isArray(enabledApps)) {
await db.execute({
@ -269,8 +244,7 @@ export async function createProfile(req: AuthRequest, res: Response) { @@ -269,8 +244,7 @@ export async function createProfile(req: AuthRequest, res: Response) {
description: createdProfile.rows[0].description,
createdAt: createdProfile.rows[0].created_at,
updatedAt: createdProfile.rows[0].updated_at,
isActive: createdProfile.rows[0].is_active === 1,
dailyTimeLimit
isActive: createdProfile.rows[0].is_active === 1
}
});
} catch (error: any) {
@ -487,7 +461,7 @@ export async function updateProfileSettings(req: AuthRequest, res: Response) { @@ -487,7 +461,7 @@ export async function updateProfileSettings(req: AuthRequest, res: Response) {
});
}
const { dailyTimeLimit, enabledApps } = req.body;
const { enabledApps } = req.body;
// Verify ownership
const existing = await db.execute({
@ -505,30 +479,6 @@ export async function updateProfileSettings(req: AuthRequest, res: Response) { @@ -505,30 +479,6 @@ export async function updateProfileSettings(req: AuthRequest, res: Response) {
});
}
if (dailyTimeLimit !== undefined) {
if (typeof dailyTimeLimit !== 'number' || dailyTimeLimit < 1) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_TIME_LIMIT',
message: 'Daily time limit must be a number greater than 0'
}
});
}
// Update or insert setting
await db.execute({
sql: `
INSERT INTO settings_profile_values (profile_id, setting_key, setting_value, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(profile_id, setting_key) DO UPDATE SET
setting_value = excluded.setting_value,
updated_at = excluded.updated_at
`,
args: [profileId, 'daily_time_limit_minutes', dailyTimeLimit.toString(), new Date().toISOString()]
});
}
if (enabledApps !== undefined) {
if (!Array.isArray(enabledApps)) {
return res.status(400).json({
@ -553,24 +503,9 @@ export async function updateProfileSettings(req: AuthRequest, res: Response) { @@ -553,24 +503,9 @@ export async function updateProfileSettings(req: AuthRequest, res: Response) {
});
}
// Get updated settings
const settingsResult = await db.execute({
sql: 'SELECT setting_key, setting_value FROM settings_profile_values WHERE profile_id = ?',
args: [profileId]
});
const settings: Record<string, string> = {};
for (const row of settingsResult.rows) {
settings[row.setting_key as string] = row.setting_value as string;
}
res.json({
success: true,
data: {
dailyTimeLimit: settings.daily_time_limit_minutes
? parseInt(settings.daily_time_limit_minutes, 10)
: null
}
data: {}
});
} catch (error: any) {
console.error('Update profile settings error:', error);

10
backend/src/routes/settings.routes.ts

@ -1,17 +1,9 @@ @@ -1,17 +1,9 @@
import { Router } from 'express';
import { getTimeLimit, setTimeLimit, heartbeat, getConnectionStats } from '../controllers/settings.controller.js';
import { authMiddleware } from '../middleware/auth.js';
import { adminMiddleware } from '../middleware/admin.js';
import { heartbeat, getConnectionStats } from '../controllers/settings.controller.js';
import { optionalAuthMiddleware } from '../middleware/optionalAuth.js';
const router = Router();
// Public route - anyone can read the time limit
router.get('/time-limit', getTimeLimit);
// Admin-only route - only admins can set time limits
router.put('/time-limit', authMiddleware, adminMiddleware, setTimeLimit);
// Public route - heartbeat for connection tracking (optional auth to track authenticated users)
router.post('/heartbeat', optionalAuthMiddleware, heartbeat);

8
backend/src/services/connection-tracker.service.ts

@ -10,8 +10,6 @@ interface Connection { @@ -10,8 +10,6 @@ interface Connection {
route?: string;
videoTitle?: string;
videoChannel?: string;
timeUsed?: number; // minutes
dailyLimit?: number; // minutes
lastHeartbeat: number;
connectedAt: number;
}
@ -31,7 +29,7 @@ class ConnectionTracker { @@ -31,7 +29,7 @@ class ConnectionTracker {
/**
* Register or update a connection heartbeat
*/
heartbeat(sessionId: string, userId?: number, username?: string, route?: string, videoTitle?: string, videoChannel?: string, timeUsed?: number, dailyLimit?: number): void {
heartbeat(sessionId: string, userId?: number, username?: string, route?: string, videoTitle?: string, videoChannel?: string): void {
const now = Date.now();
const existing = this.connections.get(sessionId);
@ -43,8 +41,6 @@ class ConnectionTracker { @@ -43,8 +41,6 @@ class ConnectionTracker {
if (route !== undefined) existing.route = route;
if (videoTitle !== undefined) existing.videoTitle = videoTitle;
if (videoChannel !== undefined) existing.videoChannel = videoChannel;
if (timeUsed !== undefined) existing.timeUsed = timeUsed;
if (dailyLimit !== undefined) existing.dailyLimit = dailyLimit;
} else {
// New connection
this.connections.set(sessionId, {
@ -54,8 +50,6 @@ class ConnectionTracker { @@ -54,8 +50,6 @@ class ConnectionTracker {
route,
videoTitle,
videoChannel,
timeUsed,
dailyLimit,
lastHeartbeat: now,
connectedAt: now
});

12
frontend/src/components/MagicCodeInput/MagicCodeInput.tsx

@ -64,13 +64,6 @@ export function MagicCodeInput({ onApplied, onClose }: MagicCodeInputProps) { @@ -64,13 +64,6 @@ export function MagicCodeInput({ onApplied, onClose }: MagicCodeInputProps) {
}
};
const formatTime = (minutes: number | null) => {
if (!minutes) return 'Not set';
if (minutes < 60) return `${minutes} minutes`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours} hour${hours !== 1 ? 's' : ''} ${mins} minute${mins !== 1 ? 's' : ''}` : `${hours} hour${hours !== 1 ? 's' : ''}`;
};
return (
<div className="bg-card rounded-xl p-6 border border-border max-w-md w-full">
@ -85,11 +78,6 @@ export function MagicCodeInput({ onApplied, onClose }: MagicCodeInputProps) { @@ -85,11 +78,6 @@ export function MagicCodeInput({ onApplied, onClose }: MagicCodeInputProps) {
<p className="text-xs text-green-700">
Code: <strong>{appliedCode}</strong>
</p>
{settings.dailyTimeLimit && (
<p className="text-xs text-green-700 mt-1">
Daily time limit: <strong>{formatTime(settings.dailyTimeLimit)}</strong>
</p>
)}
<button
onClick={handleClear}
className="mt-2 text-xs text-green-700 hover:underline"

215
frontend/src/components/TimeLimitManager/TimeLimitManager.tsx

@ -1,215 +0,0 @@ @@ -1,215 +0,0 @@
import { useState, useEffect } from 'react';
import {
getDailyLimit,
getTimeUsedToday,
setDailyLimit,
resetDailyCounter
} from '../../services/timeLimitService';
export function TimeLimitManager() {
const [dailyLimit, setDailyLimitState] = useState<number | null>(null);
const [timeUsed, setTimeUsed] = useState(getTimeUsedToday());
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fetch limit from server on mount
useEffect(() => {
const fetchLimit = async () => {
try {
setIsLoading(true);
const limit = await getDailyLimit();
setDailyLimitState(limit);
setInputValue(limit.toString());
} catch (err: any) {
setError(err.error?.message || 'Failed to load time limit');
} finally {
setIsLoading(false);
}
};
fetchLimit();
}, []);
// Update time used periodically
useEffect(() => {
const interval = setInterval(() => {
setTimeUsed(getTimeUsedToday());
}, 5000);
return () => clearInterval(interval);
}, []);
const handleSaveLimit = async () => {
const minutes = parseInt(inputValue, 10);
if (isNaN(minutes) || minutes < 1) {
alert('Please enter a valid number of minutes (minimum 1)');
return;
}
setIsSaving(true);
setError(null);
try {
await setDailyLimit(minutes);
setDailyLimitState(minutes);
} catch (err: any) {
setError(err.error?.message || 'Failed to save time limit');
} finally {
setIsSaving(false);
}
};
const handleResetCounter = () => {
resetDailyCounter();
setTimeUsed(0);
setShowResetConfirm(false);
};
const formatTime = (minutes: number): string => {
if (minutes < 1) {
return `${Math.round(minutes * 60)} seconds`;
}
if (minutes < 60) {
return `${Math.round(minutes)} minute${Math.round(minutes) !== 1 ? 's' : ''}`;
}
const hours = Math.floor(minutes / 60);
const mins = Math.round(minutes % 60);
if (mins === 0) {
return `${hours} hour${hours !== 1 ? 's' : ''}`;
}
return `${hours} hour${hours !== 1 ? 's' : ''} ${mins} minute${mins !== 1 ? 's' : ''}`;
};
const remainingTime = dailyLimit !== null ? Math.max(0, dailyLimit - timeUsed) : 0;
if (isLoading) {
return (
<div className="bg-card rounded-xl p-6 border border-border h-fit">
<div className="mb-6">
<h2 className="m-0 mb-2 text-xl font-semibold text-foreground">Daily Time Limit Settings</h2>
<p className="m-0 text-sm text-muted-foreground">Loading...</p>
</div>
</div>
);
}
return (
<div className="bg-card rounded-xl p-6 border border-border h-fit">
<div className="mb-6">
<h2 className="m-0 mb-2 text-xl font-semibold text-foreground">Daily Time Limit Settings</h2>
<p className="m-0 text-sm text-muted-foreground">
Configure how much time users can spend watching videos each day
</p>
</div>
{error && (
<div className="px-4 py-3 rounded-md mb-4 text-sm bg-red-50 text-red-800 border border-red-200">
{error}
</div>
)}
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<label htmlFor="daily-limit-input" className="text-sm font-medium text-foreground">
Daily Limit (minutes)
</label>
<div className="flex gap-3 items-center md:flex-row flex-col md:items-center items-stretch">
<input
id="daily-limit-input"
type="number"
min="1"
step="1"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSaveLimit();
}
}}
className="flex-1 max-w-[200px] md:max-w-[200px] max-w-full px-3 py-2.5 border border-border rounded-md text-sm bg-muted text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<button
onClick={handleSaveLimit}
disabled={isSaving || (dailyLimit !== null && inputValue === dailyLimit.toString())}
className="px-5 py-2.5 bg-gradient-to-r from-primary to-secondary text-white border-none rounded-md text-sm font-medium cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? 'Saving...' : 'Save'}
</button>
</div>
{dailyLimit !== null && (
<p className="m-0 text-[13px] text-muted-foreground">
Current limit: <strong className="font-semibold">{formatTime(dailyLimit)}</strong> per day
</p>
)}
</div>
{dailyLimit !== null && (
<div className="p-5 bg-muted rounded-lg border border-border/50">
<h3 className="m-0 mb-4 text-base font-semibold text-foreground">Today's Usage</h3>
<div className="flex flex-col gap-3">
<div className="w-full h-6 bg-border/30 rounded-xl overflow-hidden relative">
<div
className="h-full bg-gradient-to-r from-primary to-secondary transition-all duration-300 ease-in-out rounded-xl"
style={{
width: `${Math.min(100, (timeUsed / dailyLimit) * 100)}%`
}}
/>
</div>
<div className="flex justify-between text-sm text-foreground md:flex-row flex-col md:gap-0 gap-2">
<span className="time-used">
Used: <strong className="text-primary font-semibold">{formatTime(timeUsed)}</strong>
</span>
<span className="time-remaining">
Remaining: <strong className="text-primary font-semibold">{formatTime(remainingTime)}</strong>
</span>
</div>
</div>
{timeUsed > 0 && (
<div className="mt-4 pt-4 border-t border-border/50">
<button
onClick={() => setShowResetConfirm(true)}
className="px-4 py-2 bg-transparent text-primary border border-primary rounded-md text-sm font-medium cursor-pointer transition-colors hover:bg-primary hover:text-primary-foreground"
>
Reset Today's Counter
</button>
</div>
)}
</div>
)}
</div>
{showResetConfirm && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[1000] backdrop-blur-sm"
onClick={() => setShowResetConfirm(false)}
>
<div
className="bg-card rounded-xl p-6 max-w-[400px] w-[90%] shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<h3 className="m-0 mb-3 text-lg font-semibold text-foreground">Reset Today's Counter?</h3>
<p className="m-0 mb-5 text-sm text-muted-foreground leading-relaxed">
This will reset the time used today back to 0. Users will be able to watch videos again.
</p>
<div className="flex gap-3 justify-end md:flex-row flex-col">
<button
onClick={handleResetCounter}
className="px-5 py-2.5 bg-gradient-to-r from-primary to-secondary text-white border-none rounded-md text-sm font-medium cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-lg"
>
Reset Counter
</button>
<button
onClick={() => setShowResetConfirm(false)}
className="px-5 py-2.5 bg-transparent text-foreground border border-border rounded-md text-sm font-medium cursor-pointer transition-colors hover:bg-muted"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
);
}

155
frontend/src/hooks/useTimeLimit.ts

@ -1,155 +0,0 @@ @@ -1,155 +0,0 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import {
getDailyLimit,
getDailyLimitSync,
getTimeUsedToday,
getRemainingTimeToday,
isLimitReached,
addTimeSpent
} from '../services/timeLimitService';
interface UseTimeLimitReturn {
dailyLimit: number;
timeUsed: number;
remainingTime: number;
limitReached: boolean;
startTracking: () => void;
stopTracking: () => void;
isTracking: boolean;
}
/**
* Hook to manage video time limits
* Tracks time spent watching videos and enforces daily limits
*/
export function useTimeLimit(): UseTimeLimitReturn {
const [dailyLimit, setDailyLimit] = useState(getDailyLimitSync());
const [timeUsed, setTimeUsed] = useState(getTimeUsedToday());
const [remainingTime, setRemainingTime] = useState(getRemainingTimeToday());
const [limitReached, setLimitReached] = useState(isLimitReached());
const [isTracking, setIsTracking] = useState(false);
const trackingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const startTimeRef = useRef<number | null>(null);
const lastUpdateRef = useRef<number>(Date.now());
// Update state from localStorage and cached server limit
const updateState = useCallback(() => {
setDailyLimit(getDailyLimitSync());
setTimeUsed(getTimeUsedToday());
setRemainingTime(getRemainingTimeToday());
setLimitReached(isLimitReached());
}, []);
// Fetch limit from server on mount
useEffect(() => {
getDailyLimit().then(limit => {
setDailyLimit(limit);
// Immediately recalculate limitReached with the correct server limit
updateState();
});
}, [updateState]);
// Start tracking time
const startTracking = useCallback(() => {
if (limitReached) {
return; // Don't start if limit already reached
}
if (trackingIntervalRef.current) {
return; // Already tracking
}
startTimeRef.current = Date.now();
lastUpdateRef.current = Date.now();
setIsTracking(true);
// Update every 5 seconds
trackingIntervalRef.current = setInterval(() => {
if (startTimeRef.current) {
const now = Date.now();
const secondsElapsed = (now - lastUpdateRef.current) / 1000;
lastUpdateRef.current = now;
// Add time spent
addTimeSpent(secondsElapsed);
// Update state
updateState();
// Check if limit reached during tracking
if (isLimitReached()) {
// Stop tracking if limit reached
if (trackingIntervalRef.current) {
clearInterval(trackingIntervalRef.current);
trackingIntervalRef.current = null;
}
if (startTimeRef.current && lastUpdateRef.current) {
const secondsElapsed = (Date.now() - lastUpdateRef.current) / 1000;
if (secondsElapsed > 0) {
addTimeSpent(secondsElapsed);
}
startTimeRef.current = null;
}
setIsTracking(false);
updateState();
}
}
}, 5000);
}, [limitReached, updateState]);
// Stop tracking time
const stopTracking = useCallback(() => {
if (trackingIntervalRef.current) {
clearInterval(trackingIntervalRef.current);
trackingIntervalRef.current = null;
}
// Add any remaining time
if (startTimeRef.current && lastUpdateRef.current) {
const secondsElapsed = (Date.now() - lastUpdateRef.current) / 1000;
if (secondsElapsed > 0) {
addTimeSpent(secondsElapsed);
}
startTimeRef.current = null;
}
setIsTracking(false);
updateState();
}, [updateState]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (trackingIntervalRef.current) {
clearInterval(trackingIntervalRef.current);
}
// Save any remaining time before unmount
if (startTimeRef.current && lastUpdateRef.current) {
const secondsElapsed = (Date.now() - lastUpdateRef.current) / 1000;
if (secondsElapsed > 0) {
addTimeSpent(secondsElapsed);
}
}
};
}, []);
// Update state periodically to catch external changes
useEffect(() => {
const interval = setInterval(() => {
updateState();
}, 10000); // Check every 10 seconds
return () => clearInterval(interval);
}, [updateState]);
return {
dailyLimit,
timeUsed,
remainingTime,
limitReached,
startTracking,
stopTracking,
isTracking
};
}

11
frontend/src/services/apiClient.ts

@ -124,12 +124,7 @@ export const videosApi = { @@ -124,12 +124,7 @@ export const videosApi = {
// Settings API
export const settingsApi = {
getTimeLimit: () => api.get('/settings/time-limit'),
setTimeLimit: (dailyLimit: number) =>
api.put('/settings/time-limit', { dailyLimit }),
heartbeat: (sessionId: string, route: string, video?: { title: string; channelName: string }, timeLimit?: { timeUsed: number; dailyLimit: number }) => api.post('/settings/heartbeat', { sessionId, route, videoTitle: video?.title, videoChannel: video?.channelName, timeUsed: timeLimit?.timeUsed, dailyLimit: timeLimit?.dailyLimit }),
heartbeat: (sessionId: string, route: string, video?: { title: string; channelName: string }) => api.post('/settings/heartbeat', { sessionId, route, videoTitle: video?.title, videoChannel: video?.channelName }),
getConnectionStats: () => api.get('/settings/connection-stats')
};
@ -177,7 +172,7 @@ export const settingsProfilesApi = { @@ -177,7 +172,7 @@ export const settingsProfilesApi = {
getById: (id: number) => api.get(`/settings-profiles/${id}`),
create: (data: { name: string; description?: string; dailyTimeLimit?: number; enabledApps?: string[] }) =>
create: (data: { name: string; description?: string; enabledApps?: string[] }) =>
api.post('/settings-profiles', data),
update: (id: number, data: { name?: string; description?: string; isActive?: boolean }) =>
@ -185,7 +180,7 @@ export const settingsProfilesApi = { @@ -185,7 +180,7 @@ export const settingsProfilesApi = {
delete: (id: number) => api.delete(`/settings-profiles/${id}`),
updateSettings: (id: number, settings: { dailyTimeLimit?: number; enabledApps?: string[] }) =>
updateSettings: (id: number, settings: { enabledApps?: string[] }) =>
api.put(`/settings-profiles/${id}/settings`, settings),
regenerateCode: (id: number) => api.post(`/settings-profiles/${id}/regenerate-code`)

8
frontend/src/services/connectionTracker.ts

@ -106,23 +106,19 @@ export function setCurrentVideo(video: { title: string; channelName: string } | @@ -106,23 +106,19 @@ export function setCurrentVideo(video: { title: string; channelName: string } |
}
/**
* Send a heartbeat to the server with the session ID, current route, video info, and time limit usage
* Send a heartbeat to the server with the session ID, current route, and video info
*/
async function sendHeartbeat(): Promise<void> {
try {
const { settingsApi } = await import('./apiClient');
const { getTimeUsedToday, getDailyLimitSync } = await import('./timeLimitService');
const sessionId = getSessionId();
const route = getCurrentRoute();
const timeUsed = getTimeUsedToday();
const dailyLimit = getDailyLimitSync();
await settingsApi.heartbeat(
sessionId,
route,
currentVideo ? { title: currentVideo.title, channelName: currentVideo.channelName } : undefined,
{ timeUsed, dailyLimit }
currentVideo ? { title: currentVideo.title, channelName: currentVideo.channelName } : undefined
);
} catch (error) {
// Silently fail - don't spam console with errors

2
frontend/src/services/magicCodeService.ts

@ -2,7 +2,6 @@ const MAGIC_CODE_KEY = 'magic_code'; @@ -2,7 +2,6 @@ const MAGIC_CODE_KEY = 'magic_code';
const MAGIC_CODE_SETTINGS_KEY = 'magic_code_settings';
export interface MagicCodeSettings {
dailyTimeLimit: number | null;
enabledApps: string[] | null;
appliedAt: string;
}
@ -58,7 +57,6 @@ export async function applyMagicCode(code: string): Promise<MagicCodeSettings> { @@ -58,7 +57,6 @@ export async function applyMagicCode(code: string): Promise<MagicCodeSettings> {
const response: any = await magicCodeApi.getSettingsByCode(normalizedCode);
const settings: MagicCodeSettings = {
dailyTimeLimit: response.data.dailyTimeLimit,
enabledApps: response.data.enabledApps || null,
appliedAt: new Date().toISOString()
};

210
frontend/src/services/timeLimitService.ts

@ -1,210 +0,0 @@ @@ -1,210 +0,0 @@
interface TimeLimitData {
dailyLimit: number; // minutes (stored on server)
dailyTimeUsed: number; // minutes (stored per-device in localStorage)
lastResetDate: string; // ISO date string (YYYY-MM-DD)
}
const STORAGE_KEY = 'video_time_limit';
const DEFAULT_DAILY_LIMIT = 1; // 1 minute for testing
// Cache for daily limit from server
let cachedDailyLimit: number | null = null;
let limitCacheTime: number = 0;
const LIMIT_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
/**
* Get local date string in YYYY-MM-DD format (not UTC)
* This ensures the daily reset happens at local midnight, not UTC midnight
*/
function getLocalDateString(): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Get time limit data from localStorage (for usage tracking only)
*/
function getTimeLimitData(): Omit<TimeLimitData, 'dailyLimit'> {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const data = JSON.parse(stored);
return {
dailyTimeUsed: data.dailyTimeUsed || 0,
lastResetDate: data.lastResetDate || getLocalDateString()
};
}
} catch (e) {
console.warn('Failed to parse time limit data from localStorage', e);
}
// Return default data
return {
dailyTimeUsed: 0,
lastResetDate: getLocalDateString()
};
}
/**
* Save time limit data to localStorage (for usage tracking only)
*/
function saveTimeLimitData(data: Omit<TimeLimitData, 'dailyLimit'>): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
console.warn('Failed to save time limit data to localStorage', e);
}
}
/**
* Check if we need to reset daily counter (new day)
*/
function shouldResetDaily(): boolean {
const data = getTimeLimitData();
const today = getLocalDateString();
return data.lastResetDate !== today;
}
/**
* Reset daily counter if it's a new day
*/
function resetIfNeeded(): void {
if (shouldResetDaily()) {
const data = getTimeLimitData();
data.dailyTimeUsed = 0;
data.lastResetDate = getLocalDateString();
saveTimeLimitData(data);
}
}
/**
* Get current time limit configuration
* Priority: Magic code settings > Server settings > Cached > Default
*/
export async function getDailyLimit(): Promise<number> {
// Check magic code settings first (highest priority)
try {
const { getMagicCodeSettings } = await import('./magicCodeService');
const magicCodeSettings = getMagicCodeSettings();
if (magicCodeSettings && magicCodeSettings.dailyTimeLimit !== null && magicCodeSettings.dailyTimeLimit !== undefined) {
return magicCodeSettings.dailyTimeLimit;
}
} catch (error) {
console.warn('Failed to check magic code settings:', error);
}
// Return cached value if still valid
const now = Date.now();
if (cachedDailyLimit !== null && (now - limitCacheTime) < LIMIT_CACHE_DURATION) {
return cachedDailyLimit;
}
try {
const { settingsApi } = await import('./apiClient');
const response = await settingsApi.getTimeLimit();
const limit = response.data.dailyLimit;
cachedDailyLimit = limit;
limitCacheTime = now;
return limit;
} catch (error) {
console.warn('Failed to fetch daily limit from server, using cached/default:', error);
// Return cached value or default
return cachedDailyLimit ?? DEFAULT_DAILY_LIMIT;
}
}
/**
* Set daily time limit (in minutes) on server
*/
export async function setDailyLimit(minutes: number): Promise<void> {
try {
const { settingsApi } = await import('./apiClient');
await settingsApi.setTimeLimit(minutes);
cachedDailyLimit = minutes;
limitCacheTime = Date.now();
} catch (error) {
console.error('Failed to set daily limit on server:', error);
throw error;
}
}
/**
* Synchronous version for use in hooks (uses cached value or magic code settings)
* Note: Magic code settings are checked synchronously from localStorage
*/
export function getDailyLimitSync(): number {
// Check magic code settings first (highest priority)
// We can access localStorage synchronously
try {
const MAGIC_CODE_SETTINGS_KEY = 'magic_code_settings';
const stored = localStorage.getItem(MAGIC_CODE_SETTINGS_KEY);
if (stored) {
const magicCodeSettings = JSON.parse(stored);
if (magicCodeSettings && magicCodeSettings.dailyTimeLimit !== null && magicCodeSettings.dailyTimeLimit !== undefined) {
return magicCodeSettings.dailyTimeLimit;
}
}
} catch (error) {
// Ignore errors in sync context
}
return cachedDailyLimit ?? DEFAULT_DAILY_LIMIT;
}
/**
* Get time used today (in minutes) - per device
*/
export function getTimeUsedToday(): number {
resetIfNeeded();
const data = getTimeLimitData();
return data.dailyTimeUsed;
}
/**
* Get remaining time today (in minutes)
* Note: Uses cached limit value
*/
export function getRemainingTimeToday(): number {
resetIfNeeded();
const limit = getDailyLimitSync();
const used = getTimeUsedToday();
return Math.max(0, limit - used);
}
/**
* Check if daily limit has been reached
* Note: Uses cached limit value
*/
export function isLimitReached(): boolean {
resetIfNeeded();
return getRemainingTimeToday() <= 0;
}
/**
* Add time spent (in seconds) to the daily counter
* Note: Uses cached limit value to cap the usage
*/
export function addTimeSpent(seconds: number): void {
resetIfNeeded();
const data = getTimeLimitData();
const minutesToAdd = seconds / 60;
const limit = getDailyLimitSync();
data.dailyTimeUsed = Math.min(
limit,
data.dailyTimeUsed + minutesToAdd
);
saveTimeLimitData(data);
}
/**
* Reset daily counter (for testing/admin purposes)
*/
export function resetDailyCounter(): void {
const data = getTimeLimitData();
data.dailyTimeUsed = 0;
data.lastResetDate = getLocalDateString();
saveTimeLimitData(data);
}

1
frontend/src/types/api.ts

@ -50,7 +50,6 @@ export interface SettingsProfile { @@ -50,7 +50,6 @@ export interface SettingsProfile {
createdAt: string;
updatedAt: string;
isActive: boolean;
dailyTimeLimit: number | null;
enabledApps?: string[];
}

Loading…
Cancel
Save