This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

bug: fix thread misfire, move to seconds, and back to hackatime ids

+107 -38
+1
src/features/api/routes/projects.ts
··· 7 7 projectName: string; 8 8 projectDescription: string; 9 9 projectBannerUrl: string; 10 + /** Total time spent on takes, in seconds */ 10 11 totalTakesTime: number; 11 12 userId: string; 12 13 };
+6 -4
src/features/api/routes/recentTakes.ts
··· 1 - import { eq, desc, and, or } from "drizzle-orm"; 1 + import { eq, desc, or } from "drizzle-orm"; 2 2 import { db } from "../../../libs/db"; 3 3 import { takes as takesTable, users as usersTable } from "../../../libs/schema"; 4 4 import { handleApiError } from "../../../libs/apiError"; ··· 9 9 notes: string; 10 10 createdAt: Date; 11 11 mediaUrls: string[]; 12 - elapsedTimeMs: number; 12 + /* elapsed time in seconds */ 13 + elapsedTime: number; 13 14 project: string; 15 + /* total time in seconds */ 14 16 totalTakesTime: number; 15 17 }; 16 18 ··· 87 89 notes: take.notes, 88 90 createdAt: new Date(take.createdAt), 89 91 mediaUrls: take.media ? JSON.parse(take.media) : [], 90 - elapsedTimeMs: take.elapsedTimeMs, 92 + elapsedTime: take.elapsedTime, 91 93 project: userMap[take.userId]?.projectName || "unknown project", 92 94 totalTakesTime: 93 - userMap[take.userId]?.totalTakesTime || take.elapsedTimeMs, 95 + userMap[take.userId]?.totalTakesTime || take.elapsedTime, 94 96 })) || []; 95 97 96 98 return new Response(
+3 -1
src/features/frontend/pages/ProjectTakes.tsx
··· 145 145 Duration: 146 146 </span> 147 147 <span className="meta-value"> 148 - {prettyPrintTime(take.elapsedTimeMs)} 148 + {prettyPrintTime( 149 + take.elapsedTime * 1000, 150 + )} 149 151 </span> 150 152 </div> 151 153 </div>
+3 -1
src/features/frontend/pages/Projects.tsx
··· 47 47 <div className="project-meta"> 48 48 <span> 49 49 Total Time:{" "} 50 - {prettyPrintTime(project.totalTakesTime)} 50 + {prettyPrintTime( 51 + project.totalTakesTime * 1000, 52 + )} 51 53 </span> 52 54 </div> 53 55 </Link>
+2 -2
src/features/takes/handlers/history.ts
··· 13 13 .orderBy(desc(takesTable.createdAt)); 14 14 15 15 const takeTimeMs = takes.reduce( 16 - (acc, take) => acc + take.elapsedTimeMs * Number(take.multiplier), 16 + (acc, take) => acc + take.elapsedTime * 1000 * Number(take.multiplier), 17 17 0, 18 18 ); 19 19 const takeTime = prettyPrintTime(takeTimeMs); ··· 32 32 33 33 for (const take of takes) { 34 34 const notes = take.notes ? `\n• Notes: ${take.notes}` : ""; 35 - const duration = prettyPrintTime(take.elapsedTimeMs); 35 + const duration = prettyPrintTime(take.elapsedTime * 1000); 36 36 37 37 historyBlocks.push({ 38 38 type: "section",
+3 -5
src/features/takes/handlers/settings.ts
··· 46 46 project_description: existingUser.projectDescription, 47 47 repo_link: existingUser.repoLink || undefined, 48 48 demo_link: existingUser.demoLink || undefined, 49 - hackatime_version: getHackatimeVersion( 50 - existingUser.hackatimeBaseAPI, 51 - ), 49 + hackatime_version: existingUser.hackatimeVersion, 52 50 }; 53 51 } 54 52 } catch (error) { ··· 233 231 demoLink: values.project_link?.demo_link?.value as 234 232 | string 235 233 | undefined, 236 - hackatimeBaseAPI: getHackatimeApiUrl(hackatimeVersion), 234 + hackatimeVersion, 237 235 }) 238 236 .onConflictDoUpdate({ 239 237 target: usersTable.id, ··· 249 247 demoLink: values.demo_link?.demo_link_input?.value as 250 248 | string 251 249 | undefined, 252 - hackatimeBaseAPI: getHackatimeApiUrl(hackatimeVersion), 250 + hackatimeVersion, 253 251 }, 254 252 }); 255 253 } catch (error) {
+2 -1
src/features/takes/handlers/upload.ts
··· 12 12 if ( 13 13 payload.subtype === "bot_message" || 14 14 payload.subtype === "thread_broadcast" || 15 + payload.thread_ts || 15 16 payload.channel !== process.env.SLACK_LISTEN_CHANNEL 16 17 ) 17 18 return; ··· 136 137 ts: payload.ts, 137 138 notes: markdownText, 138 139 media: JSON.stringify(mediaUrls), 139 - elapsedTimeMs: timeSpentMs, 140 + elapsedTime: timeSpentMs / 1000, 140 141 }); 141 142 142 143 await slackClient.reactions.add({
+63
src/libs/hackatime.ts
··· 47 47 } 48 48 return "v2"; 49 49 } 50 + 51 + /** 52 + * Fetches a user's Hackatime summary 53 + * @param userId The user ID to fetch the summary for 54 + * @param version The Hackatime version to use (defaults to v2) 55 + * @param projectKeys Optional array of project keys to filter results by 56 + * @returns A promise that resolves to the summary data 57 + */ 58 + export async function fetchHackatimeSummary( 59 + userId: string, 60 + version: HackatimeVersion = "v2", 61 + projectKeys?: string[], 62 + ) { 63 + const apiUrl = getHackatimeApiUrl(version); 64 + const response = await fetch( 65 + `${apiUrl}/summary?user=${userId}&interval=month`, 66 + { 67 + headers: { 68 + accept: "application/json", 69 + Authorization: "Bearer 2ce9e698-8a16-46f0-b49a-ac121bcfd608", 70 + }, 71 + }, 72 + ); 73 + 74 + if (!response.ok) { 75 + throw new Error( 76 + `Failed to fetch Hackatime summary: ${response.status} ${response.statusText}`, 77 + ); 78 + } 79 + 80 + const data = await response.json(); 81 + 82 + // Add derived properties similar to the shell command 83 + const totalCategoriesSum = 84 + data.categories?.reduce( 85 + (sum: number, category: { total: number }) => sum + category.total, 86 + 0, 87 + ) || 0; 88 + const hours = Math.floor(totalCategoriesSum / 3600); 89 + const minutes = Math.floor((totalCategoriesSum % 3600) / 60); 90 + const seconds = totalCategoriesSum % 60; 91 + 92 + // Get all project keys from the data 93 + const allProjectsKeys = 94 + data.projects 95 + ?.sort( 96 + (a: { total: number }, b: { total: number }) => 97 + b.total - a.total, 98 + ) 99 + .map((project: { key: string }) => project.key) || []; 100 + 101 + // Filter by provided project keys if any 102 + const projectsKeys = projectKeys 103 + ? allProjectsKeys.filter((key: string) => projectKeys.includes(key)) 104 + : allProjectsKeys; 105 + 106 + return { 107 + ...data, 108 + total_categories_sum: totalCategoriesSum, 109 + total_categories_human_readable: `${hours}h ${minutes}m ${seconds}s`, 110 + projectsKeys: projectsKeys, 111 + }; 112 + }
+24 -24
src/libs/schema.ts
··· 6 6 id: text("id").primaryKey(), 7 7 userId: text("user_id").notNull(), 8 8 ts: text("ts").notNull(), 9 - elapsedTimeMs: integer("elapsed_time_ms").notNull().default(0), 9 + /* elapsed time in seconds */ 10 + elapsedTime: integer("elapsed_time").notNull().default(0), 10 11 createdAt: text("created_at") 11 12 .$defaultFn(() => new Date().toISOString()) 12 13 .notNull(), ··· 19 20 id: text("id") 20 21 .primaryKey() 21 22 .$defaultFn(() => Bun.randomUUIDv7()), 23 + /* total time in seconds */ 22 24 totalTakesTime: integer("total_takes_time").default(0).notNull(), 23 25 hackatimeKeys: text("hackatime_keys").notNull().default("[]"), 24 26 projectName: text("project_name").notNull().default(""), 25 27 projectDescription: text("project_description").notNull().default(""), 26 28 projectBannerUrl: text("project_banner_url").notNull().default(""), 27 - hackatimeBaseAPI: text("hackatime_base_api") 28 - .notNull() 29 - .default("https://hackatime.hackclub.com/api"), 29 + hackatimeVersion: text("hackatime_version").notNull().default("v2"), 30 30 repoLink: text("repo_link"), 31 31 demoLink: text("demo_link"), 32 32 }); ··· 36 36 CREATE INDEX IF NOT EXISTS idx_takes_user_id ON takes(user_id); 37 37 38 38 CREATE OR REPLACE FUNCTION update_user_total_time() 39 - RETURNS TRIGGER AS $$ 40 - BEGIN 41 - IF TG_OP = 'INSERT' THEN 42 - UPDATE users 43 - SET total_takes_time = COALESCE(total_takes_time, 0) + NEW.elapsed_time_ms 44 - WHERE id = NEW.user_id; 45 - RETURN NEW; 46 - ELSIF TG_OP = 'DELETE' THEN 47 - UPDATE users 48 - SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time_ms 49 - WHERE id = OLD.user_id; 50 - RETURN OLD; 51 - ELSIF TG_OP = 'UPDATE' THEN 52 - UPDATE users 53 - SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time_ms + NEW.elapsed_time_ms 54 - WHERE id = NEW.user_id; 55 - RETURN NEW; 56 - END IF; 39 + RETURNS TRIGGER AS $$ 40 + BEGIN 41 + IF TG_OP = 'INSERT' THEN 42 + UPDATE users 43 + SET total_takes_time = COALESCE(total_takes_time, 0) + NEW.elapsed_time 44 + WHERE id = NEW.user_id; 45 + RETURN NEW; 46 + ELSIF TG_OP = 'DELETE' THEN 47 + UPDATE users 48 + SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time 49 + WHERE id = OLD.user_id; 50 + RETURN OLD; 51 + ELSIF TG_OP = 'UPDATE' THEN 52 + UPDATE users 53 + SET total_takes_time = COALESCE(total_takes_time, 0) - OLD.elapsed_time + NEW.elapsed_time 54 + WHERE id = NEW.user_id; 55 + RETURN NEW; 56 + END IF; 57 57 58 - RETURN NULL; -- Default return for unexpected operations 58 + RETURN NULL; -- Default return for unexpected operations 59 59 60 - EXCEPTION WHEN OTHERS THEN 60 + EXCEPTION WHEN OTHERS THEN 61 61 RAISE NOTICE 'Error updating user total time: %', SQLERRM; 62 62 RETURN NULL; 63 63 END;