mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 22:05:09 +00:00
refresh time
This commit is contained in:
parent
4ef01ad270
commit
43b4e934db
4 changed files with 138 additions and 2 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue