data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

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

feat: add a git activity section to status [skip ci]

dusk 41b181ce 954f2960

+129 -51
bun.lockb

This is a binary file and will not be displayed.

+1
package.json
··· 41 41 "type": "module", 42 42 "dependencies": { 43 43 "@neodrag/svelte": "^2.3.1", 44 + "@rowanmanning/feed-parser": "^2.0.2", 44 45 "@skyware/bot": "^0.3.8", 45 46 "@std/toml": "npm:@jsr/std__toml", 46 47 "@types/node-schedule": "^2.1.7",
+19 -13
src/hooks.server.ts
··· 1 1 import { updateLastPosts } from '$lib/bluesky'; 2 2 import { lastFmUpdateNowPlaying } from '$lib/lastfm'; 3 - import { steamUpdateNowPlaying } from '$lib/steam' 4 - import { cancelJob, scheduleJob, scheduledJobs } from 'node-schedule' 3 + import { steamUpdateNowPlaying } from '$lib/steam'; 4 + import { updateCommits } from '$lib/activity'; 5 + import { cancelJob, scheduleJob, scheduledJobs } from 'node-schedule'; 5 6 6 - const UPDATE_LAST_JOB_NAME = "update steam game, lastfm track, bsky posts" 7 + const UPDATE_LAST_JOB_NAME = 'update steam game, lastfm track, bsky posts, git activity'; 7 8 8 9 if (UPDATE_LAST_JOB_NAME in scheduledJobs) { 9 - console.log(`${UPDATE_LAST_JOB_NAME} is already running, cancelling so we can start a new one`) 10 - cancelJob(UPDATE_LAST_JOB_NAME) 10 + console.log(`${UPDATE_LAST_JOB_NAME} is already running, cancelling so we can start a new one`); 11 + cancelJob(UPDATE_LAST_JOB_NAME); 11 12 } 12 13 13 14 console.log(`starting ${UPDATE_LAST_JOB_NAME} job...`); 14 - scheduleJob(UPDATE_LAST_JOB_NAME, "*/1 * * * *", async () => { 15 - console.log(`running ${UPDATE_LAST_JOB_NAME} job...`) 16 - try { 17 - await Promise.all([steamUpdateNowPlaying(), lastFmUpdateNowPlaying(), updateLastPosts()]) 18 - } catch (err) { 19 - console.log(`error while running ${UPDATE_LAST_JOB_NAME} job: ${err}`) 20 - } 21 - }).invoke() // invoke once immediately 15 + scheduleJob(UPDATE_LAST_JOB_NAME, '*/1 * * * *', async () => { 16 + console.log(`running ${UPDATE_LAST_JOB_NAME} job...`); 17 + try { 18 + await Promise.all([ 19 + steamUpdateNowPlaying(), 20 + lastFmUpdateNowPlaying(), 21 + updateLastPosts(), 22 + updateCommits() 23 + ]); 24 + } catch (err) { 25 + console.log(`error while running ${UPDATE_LAST_JOB_NAME} job: ${err}`); 26 + } 27 + }).invoke(); // invoke once immediately
+55
src/lib/activity.ts
··· 1 + import { get, writable } from 'svelte/store'; 2 + import { parseFeed } from '@rowanmanning/feed-parser'; 3 + 4 + const lastCommits = writable<Activity[]>([]); 5 + 6 + export const updateCommits = async () => { 7 + try { 8 + const forgejoFeed = await parseFeedToActivity('https://git.gaze.systems/90008.rss'); 9 + const githubFeed = await parseFeedToActivity('https://github.com/yusdacra.atom'); 10 + const mergedFeed = sortActivities(forgejoFeed.concat(githubFeed)).slice(0, 7); 11 + lastCommits.set(mergedFeed); 12 + } catch (why) { 13 + console.log('could not fetch git activity: ', why); 14 + } 15 + }; 16 + 17 + export const getLastActivity = () => { 18 + return get(lastCommits); 19 + }; 20 + 21 + type Activity = { 22 + source: string; 23 + description: string; 24 + link: string | null; 25 + date: Date | null; 26 + }; 27 + 28 + const parseFeedToActivity = async (url: string) => { 29 + const response = await fetch(url); 30 + const feed = parseFeed(await response.text()); 31 + 32 + const source = new URL(url).host; 33 + const results: Activity[] = []; 34 + for (const item of feed.items.slice(0, 10)) { 35 + const description: string | null = item.description || item.title; 36 + if (description === null) continue; 37 + results.push({ 38 + source, 39 + description: description.split('</a>').pop(), 40 + link: item.url, 41 + date: item.published 42 + }); 43 + } 44 + 45 + return results; 46 + }; 47 + 48 + const sortActivities = (activities: Array<Activity>) => { 49 + return activities.sort((a, b) => { 50 + if (a.date === null && b.date === null) return 0; 51 + if (a.date === null) return 1; 52 + if (b.date === null) return -1; 53 + return b.date.getTime() - a.date.getTime(); 54 + }); 55 + };
+23 -23
src/lib/dateFmt.ts
··· 1 1 export const renderRelativeDate = (timestamp: number) => { 2 - const elapsed = timestamp - (new Date()).getTime() 3 - const units: Record<string, number> = { 4 - year : 24 * 60 * 60 * 1000 * 365, 5 - month : 24 * 60 * 60 * 1000 * 365/12, 6 - day : 24 * 60 * 60 * 1000, 7 - hour : 60 * 60 * 1000, 8 - minute: 60 * 1000, 9 - second: 1000 10 - } 11 - const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) 12 - for (var unit in units) 13 - if (Math.abs(elapsed) > units[unit] || unit == 'second') 14 - return rtf.format(Math.round(elapsed / units[unit]), unit as Intl.RelativeTimeFormatUnit) 15 - return "" 16 - } 2 + const elapsed = timestamp - new Date().getTime(); 3 + const units: Record<string, number> = { 4 + year: 24 * 60 * 60 * 1000 * 365, 5 + month: (24 * 60 * 60 * 1000 * 365) / 12, 6 + day: 24 * 60 * 60 * 1000, 7 + hour: 60 * 60 * 1000, 8 + minute: 60 * 1000, 9 + second: 1000 10 + }; 11 + const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); 12 + for (const unit in units) 13 + if (Math.abs(elapsed) > units[unit] || unit == 'second') 14 + return rtf.format(Math.round(elapsed / units[unit]), unit as Intl.RelativeTimeFormatUnit); 15 + return ''; 16 + }; 17 17 export const renderDate = (timestamp: number) => { 18 - return (new Date(timestamp)).toLocaleString("en-GB", { 19 - year: "2-digit", 20 - month: "2-digit", 21 - day: "2-digit", 22 - hour: "2-digit", 23 - minute: "2-digit", 24 - }) 25 - } 18 + return new Date(timestamp).toLocaleString('en-GB', { 19 + year: '2-digit', 20 + month: '2-digit', 21 + day: '2-digit', 22 + hour: '2-digit', 23 + minute: '2-digit' 24 + }); 25 + };
+4 -2
src/routes/+page.server.ts
··· 3 3 import { getLastGame } from '$lib/steam'; 4 4 import { noteFromBskyPost } from '../components/note.svelte'; 5 5 import { pushNotification } from '$lib/pushnotif'; 6 + import { getLastActivity } from '$lib/activity.js'; 6 7 7 8 export const load = async () => { 8 9 const lastTrack = getNowPlaying(); 9 10 const lastGame = getLastGame(); 10 11 const lastPosts = getLastPosts(); 11 12 const lastNote = lastPosts.length > 0 ? noteFromBskyPost(lastPosts[0]) : null; 12 - let banners: number[] = []; 13 + const lastActivity = getLastActivity(); 14 + const banners: number[] = []; 13 15 while (banners.length < 3) { 14 16 const no = getBannerNo(banners); 15 17 banners.push(no); 16 18 } 17 - return { banners, lastTrack, lastGame, lastNote }; 19 + return { banners, lastTrack, lastGame, lastNote, lastActivity }; 18 20 }; 19 21 20 22 export const actions = {
+27 -6
src/routes/+page.svelte
··· 2 2 import { PUBLIC_BASE_URL } from '$env/static/public'; 3 3 import Note from '../components/note.svelte'; 4 4 import Window from '../components/window.svelte'; 5 - import LatestStuff from './lateststuff.md'; 6 5 import { renderDate, renderRelativeDate } from '$lib/dateFmt'; 7 6 import Tooltip from '../components/tooltip.svelte'; 8 7 ··· 31 30 32 31 <div class="flex flex-col md:flex-row gap-2 md:gap-4 md:h-full h-card"> 33 32 <div class="flex flex-col gap-2 md:gap-6 ml-auto place-items-end"> 34 - <Window title="stuff it's doing.." iconUri="/icons/msg_information.webp"> 35 - <div class="prose prose-ralsei prose-img:m-0 leading-6"> 36 - <LatestStuff /> 37 - </div> 38 - </Window> 39 33 <Window style="md:mr-8" title="status" iconUri="/icons/msn.webp" removePadding> 40 34 {#if data.lastNote} 41 35 <div class="m-1.5 flex flex-col font-monospace text-sm"> ··· 49 43 </p> 50 44 <div class="mt-0 p-1.5 border-4 border-double bg-ralsei-black min-w-full max-w-[60ch]"> 51 45 <Note note={data.lastNote} onlyContent /> 46 + </div> 47 + </div> 48 + {/if} 49 + {#if data.lastActivity.length > 0} 50 + <div class="m-1.5 flex flex-col font-monospace text-sm"> 51 + <p 52 + class="prose prose-ralsei p-1 border-4 text-sm bg-ralsei-black" 53 + style="border-style: double double none double;" 54 + title={renderDate(data.lastActivity[0].date)} 55 + > 56 + <a href="/">last git activity…</a> 57 + was {renderRelativeDate(data.lastActivity[0].date)}.. 58 + </p> 59 + <div 60 + class="prose prose-ralsei mt-0 p-1.5 border-4 border-double bg-ralsei-black min-w-full max-w-[60ch]" 61 + > 62 + {#each data.lastActivity as activity, index} 63 + <div 64 + class="text-ralsei-green-light text-sm text-ellipsis text-nowrap overflow-hidden max-w-[60ch]" 65 + style="opacity: {1.0 - (index * 1.0) / data.lastActivity.length + index * 0.05};" 66 + > 67 + <span title={renderDate(activity.date)} class="text-[#f87c32]" 68 + >[{activity.source}]</span 69 + > 70 + <a href={activity.link} title={activity.description}>{activity.description}</a> 71 + </div> 72 + {/each} 52 73 </div> 53 74 </div> 54 75 {/if}
-7
src/routes/lateststuff.md
··· 1 - +++ 2 - layout = false 3 - +++ 4 - 5 - currently working on a game under the name `packet.runner`, read some very WIP stuff about it [here](https://doc.gaze.systems/LsE08EU7QOSKm7xps_treA). 6 - 7 - <span class="text-xs italic">last updated on: 19-02-2025</span>