refresh time

This commit is contained in:
Nathan 2026-02-06 17:45:43 -05:00
parent 4ef01ad270
commit 43b4e934db
4 changed files with 138 additions and 2 deletions

View file

@ -242,6 +242,53 @@ async function checkHourMilestones(): Promise<void> {
let syncInterval: ReturnType<typeof setInterval> | null = null
export async function syncSingleProject(projectId: number): Promise<{ hours: number; updated: boolean; error?: string }> {
try {
const [project] = await db
.select({
id: projectsTable.id,
hackatimeProject: projectsTable.hackatimeProject,
hours: projectsTable.hours,
userEmail: usersTable.email
})
.from(projectsTable)
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
.where(eq(projectsTable.id, projectId))
.limit(1)
if (!project) return { hours: 0, updated: false, error: 'Project not found' }
if (!project.hackatimeProject) return { hours: project.hours ?? 0, updated: false, error: 'No Hackatime project linked' }
const parsed = parseHackatimeProject(project.hackatimeProject)
if (!parsed) return { hours: project.hours ?? 0, updated: false, error: 'Invalid Hackatime project format' }
const hackatimeUserId = await getHackatimeUserId(project.userEmail)
if (hackatimeUserId === null) return { hours: project.hours ?? 0, updated: false, error: 'Could not find Hackatime user' }
const adminProjects = await fetchUserProjects(hackatimeUserId)
if (adminProjects === null) return { hours: project.hours ?? 0, updated: false, error: 'Failed to fetch Hackatime projects' }
const hackatimeProject = adminProjects.find(p => p.name === parsed.projectName)
const hours = hackatimeProject
? Math.round(hackatimeProject.total_duration / 3600 * 10) / 10
: 0
if (hours !== project.hours) {
await db
.update(projectsTable)
.set({ hours, updatedAt: new Date() })
.where(eq(projectsTable.id, projectId))
console.log(`[HACKATIME-SYNC] Manual sync project ${projectId}: ${project.hours}h -> ${hours}h`)
return { hours, updated: true }
}
return { hours, updated: false }
} catch (error) {
console.error(`[HACKATIME-SYNC] Error syncing project ${projectId}:`, error)
return { hours: 0, updated: false, error: 'Sync failed' }
}
}
export function startHackatimeSync(): void {
if (syncInterval) return

View file

@ -9,6 +9,7 @@ import { newsTable } from '../schemas/news'
import { projectActivityTable } from '../schemas/activity'
import { getUserFromSession } from '../lib/auth'
import { calculateScrapsFromHours, getUserScrapsBalance } from '../lib/scraps'
import { syncSingleProject } from '../lib/hackatime-sync'
const admin = new Elysia({ prefix: '/admin' })
@ -981,4 +982,23 @@ admin.patch('/orders/:id', async ({ params, body, headers, status }) => {
}
})
// Sync hours for a single project from Hackatime
admin.post('/projects/:id/sync-hours', async ({ headers, params, status }) => {
const user = await requireAdmin(headers as Record<string, string>)
if (!user) {
return status(401, { error: 'Unauthorized' })
}
try {
const result = await syncSingleProject(parseInt(params.id))
if (result.error) {
return { hours: result.hours, updated: result.updated, error: result.error }
}
return { hours: result.hours, updated: result.updated }
} catch (err) {
console.error(err)
return status(500, { error: 'Failed to sync hours' })
}
})
export default admin

View file

@ -11,7 +11,8 @@
CheckCircle,
XCircle,
Info,
Globe
Globe,
RefreshCw
} from '@lucide/svelte';
import ProjectPlaceholder from '$lib/components/ProjectPlaceholder.svelte';
import { getUser } from '$lib/auth-client';
@ -92,6 +93,7 @@
let confirmAction = $state<'approved' | 'denied' | 'permanently_rejected' | null>(null);
let errorModal = $state<string | null>(null);
let syncingHours = $state(false);
let projectId = $derived(page.params.id);
@ -201,6 +203,28 @@
}
}
async function syncHours() {
if (!project || syncingHours) return;
syncingHours = true;
try {
const response = await fetch(`${API_URL}/admin/projects/${project.id}/sync-hours`, {
method: 'POST',
credentials: 'include'
});
const data = await response.json();
if (data.error) {
errorModal = data.error;
} else if (data.updated && project) {
project.hours = data.hours;
}
} catch (e) {
console.error('Failed to sync hours:', e);
errorModal = 'Failed to sync hours from Hackatime';
} finally {
syncingHours = false;
}
}
function getActionLabel(action: string) {
switch (action) {
case 'approved':
@ -323,6 +347,15 @@
<span class="rounded-full border-2 border-black bg-gray-100 px-3 py-1 font-bold"
>{formatHours(project.hours)}h logged</span
>
<button
onclick={syncHours}
disabled={syncingHours}
title="Sync hours from Hackatime"
class="inline-flex cursor-pointer items-center gap-1 rounded-full border-2 border-black bg-blue-100 px-3 py-1 text-sm font-bold text-blue-700 transition-all duration-200 hover:border-dashed disabled:cursor-not-allowed disabled:opacity-50"
>
<RefreshCw size={14} class={syncingHours ? 'animate-spin' : ''} />
{syncingHours ? 'syncing...' : 'sync hours'}
</button>
{#if project.hackatimeProject}
<span class="rounded-full border-2 border-black bg-gray-100 px-3 py-1 font-bold"
>hackatime: {project.hackatimeProject}</span

View file

@ -15,7 +15,8 @@
Plus,
Globe,
Spool,
Eye
Eye,
RefreshCw
} from '@lucide/svelte';
import { getUser } from '$lib/auth-client';
import { API_URL } from '$lib/config';
@ -65,9 +66,11 @@
let project = $state<Project | null>(null);
let owner = $state<Owner | null>(null);
let isOwner = $state(false);
let isAdmin = $state(false);
let activity = $state<ActivityEntry[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
let syncingHours = $state(false);
onMount(async () => {
const user = await getUser();
@ -75,6 +78,7 @@
goto('/');
return;
}
isAdmin = user.role === 'admin';
try {
const projectRes = await fetch(`${API_URL}/projects/${data.id}`, { credentials: 'include' });
@ -147,6 +151,27 @@
minute: '2-digit'
});
}
async function syncHours() {
if (!project || syncingHours) return;
syncingHours = true;
try {
const response = await fetch(`${API_URL}/admin/projects/${project.id}/sync-hours`, {
method: 'POST',
credentials: 'include'
});
const data = await response.json();
if (data.error) {
error = data.error;
} else if (data.updated && project) {
project.hours = data.hours;
}
} catch (e) {
console.error('Failed to sync hours:', e);
} finally {
syncingHours = false;
}
}
</script>
<svelte:head>
@ -265,6 +290,17 @@
<Clock size={18} />
{formatHours(project.hours)}h
</span>
{#if isAdmin}
<button
onclick={syncHours}
disabled={syncingHours}
title="Sync hours from Hackatime"
class="inline-flex cursor-pointer items-center gap-1 rounded-full border-2 border-black bg-blue-100 px-3 py-2 text-sm font-bold text-blue-700 transition-all duration-200 hover:border-dashed disabled:cursor-not-allowed disabled:opacity-50"
>
<RefreshCw size={14} class={syncingHours ? 'animate-spin' : ''} />
{syncingHours ? 'syncing...' : 'sync hours'}
</button>
{/if}
{#if project.githubUrl}
<a
href={project.githubUrl}