Merge pull request #99 from misenhower/claude-cleanups

Claude cleanups
This commit is contained in:
Matt Isenhower 2026-01-31 20:11:05 -08:00 committed by GitHub
commit 0793cde63e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 169 additions and 138 deletions

View File

@ -82,3 +82,15 @@ export function getXRankSeasonId(id) {
? `${parts[1]}-${parts[2]}`
: id;
}
/**
* Calculate cache expiry timestamp with 5-minute buffer.
* @param {number} expiresIn - Seconds until expiry
* @returns {number} Timestamp to expire the cache (5 minutes early)
*/
export function calculateCacheExpiry(expiresIn) {
let expires = Date.now() + expiresIn * 1000;
// Expire 5min early to make sure we have time to execute requests
return expires - 5 * 60 * 1000;
}

View File

@ -102,7 +102,10 @@ export class LocalizationProcessor {
return JSON.parse(result) || {};
} catch (e) {
//
// File doesn't exist yet or is invalid - return empty object
if (e.code !== 'ENOENT') {
console.warn(`Failed to read localization file ${this.filename}:`, e.message);
}
}
return {};

View File

@ -22,7 +22,7 @@ export default class CoopUpdater extends DataUpdater
},
];
getData(locale) {
async getData(locale) {
return this.splatnet(locale).getCoopHistoryData();
}
}

View File

@ -41,7 +41,7 @@ export default class GearUpdater extends DataUpdater
},
];
getData(locale) {
async getData(locale) {
return this.splatnet(locale).getGesotownData();
}
}

View File

@ -31,40 +31,50 @@ export default class BlueskyClient extends Client
}
async send(status, generator) {
await this.login();
if (!status.media?.length) {
console.error(`[${this.name}] No media provided for ${generator.key}`);
return;
}
// Upload images
let images = await Promise.all(
status.media.map(async m => {
// We have to convert the PNG to a JPG for Bluesky because of size limits
let jpeg = sharp(m.file).jpeg();
let metadata = await jpeg.metadata();
let buffer = await jpeg.toBuffer();
try {
await this.login();
let response = await this.#agent.uploadBlob(buffer, { encoding: 'image/jpeg' });
// Upload images
let images = await Promise.all(
status.media.map(async m => {
// We have to convert the PNG to a JPG for Bluesky because of size limits
let jpeg = sharp(m.file).jpeg();
let metadata = await jpeg.metadata();
let buffer = await jpeg.toBuffer();
return {
image: response.data.blob,
alt: m.altText || '',
aspectRatio: { width: metadata.width, height: metadata.height },
};
}),
);
let response = await this.#agent.uploadBlob(buffer, { encoding: 'image/jpeg' });
// Send status
const rt = new RichText({
text: status.status,
});
return {
image: response.data.blob,
alt: m.altText || '',
aspectRatio: { width: metadata.width, height: metadata.height },
};
}),
);
await rt.detectFacets(this.#agent);
// Send status
const rt = new RichText({
text: status.status,
});
await this.#agent.post({
text: rt.text,
facets: rt.facets,
embed: {
images,
$type: 'app.bsky.embed.images',
},
});
await rt.detectFacets(this.#agent);
await this.#agent.post({
text: rt.text,
facets: rt.facets,
embed: {
images,
$type: 'app.bsky.embed.images',
},
});
} catch (error) {
console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message);
throw error;
}
}
}

View File

@ -11,18 +11,20 @@ export default class FileWriter extends Client {
async send(status, generator) {
await mkdirp(this.dir);
let imgFilename = `${this.dir}/${generator.key}.png`;
await fs.writeFile(imgFilename, status.media[0].file);
if (status.media?.length > 0) {
let imgFilename = `${this.dir}/${generator.key}.png`;
await fs.writeFile(imgFilename, status.media[0].file);
let text = [
'Status:',
status.status,
'',
'Alt text:',
status.media[0].altText,
].join('\n');
let text = [
'Status:',
status.status,
'',
'Alt text:',
status.media[0].altText,
].join('\n');
let textFilename = `${this.dir}/${generator.key}.txt`;
await fs.writeFile(textFilename, text);
let textFilename = `${this.dir}/${generator.key}.txt`;
await fs.writeFile(textFilename, text);
}
}
}

View File

@ -9,6 +9,10 @@ export default class ImageWriter extends Client {
dir = 'dist/status-screenshots'; // `/screenshots` points to the page used by puppeteer
async send(status, generator) {
if (!status.media?.length) {
return;
}
await mkdirp(this.dir);
let imgFilename = `${this.dir}/${generator.key}.png`;

View File

@ -23,35 +23,45 @@ export default class MastodonClient extends Client
}
async send(status, generator) {
// Mastodon API
const masto = await createRestAPIClient({
url: this.#url,
accessToken: this.#accessToken,
disableVersionCheck: true,
disableExperimentalWarning: true,
});
if (!status.media?.length) {
console.error(`[${this.name}] No media provided for ${generator.key}`);
return;
}
// Upload images
let mediaIds = await Promise.all(
status.media.map(async m => {
let request = { file: new Blob([m.file]) };
if (m.altText) {
request.description = m.altText;
}
try {
// Mastodon API
const masto = await createRestAPIClient({
url: this.#url,
accessToken: this.#accessToken,
disableVersionCheck: true,
disableExperimentalWarning: true,
});
let attachment = await masto.v2.media.create(request);
// Upload images
let mediaIds = await Promise.all(
status.media.map(async m => {
let request = { file: new Blob([m.file]) };
if (m.altText) {
request.description = m.altText;
}
return attachment.id;
}),
);
let attachment = await masto.v2.media.create(request);
// Send status
await masto.v1.statuses.create({
status: status.status,
spoilerText: status.contentWrapper,
sensitive: !!status.contentWrapper, // Without the sensitive property the image is still visible
mediaIds,
visibility: this.#visibility,
});
return attachment.id;
}),
);
// Send status
await masto.v1.statuses.create({
status: status.status,
spoilerText: status.contentWrapper,
sensitive: !!status.contentWrapper, // Without the sensitive property the image is still visible
mediaIds,
visibility: this.#visibility,
});
} catch (error) {
console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message);
throw error;
}
}
}

View File

@ -29,11 +29,21 @@ export default class ThreadsClient extends Client {
}
async send(status, generator) {
let jpeg = await sharp(status.media[0].file).jpeg().toBuffer();
if (!status.media?.length) {
console.error(`[${this.name}] No media provided for ${generator.key}`);
return;
}
await this.#api.publish({
text: status.status,
image: { type: 'image/jpeg', data: jpeg },
});
try {
let jpeg = await sharp(status.media[0].file).jpeg().toBuffer();
await this.#api.publish({
text: status.status,
image: { type: 'image/jpeg', data: jpeg },
});
} catch (error) {
console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message);
throw error;
}
}
}

View File

@ -30,20 +30,30 @@ export default class TwitterClient extends Client
}
async send(status, generator) {
// Upload images
let mediaIds = await Promise.all(
status.media.map(async m => {
let id = await this.api().v1.uploadMedia(Buffer.from(m.file), { mimeType: m.type });
if (!status.media?.length) {
console.error(`[${this.name}] No media provided for ${generator.key}`);
return;
}
if (m.altText) {
await this.api().v1.createMediaMetadata(id, { alt_text: { text: m.altText } });
}
try {
// Upload images
let mediaIds = await Promise.all(
status.media.map(async m => {
let id = await this.api().v1.uploadMedia(Buffer.from(m.file), { mimeType: m.type });
return id;
}),
);
if (m.altText) {
await this.api().v1.createMediaMetadata(id, { alt_text: { text: m.altText } });
}
// Send status
await this.api().v2.tweet(status.status, { media: { media_ids: mediaIds } });
return id;
}),
);
// Send status
await this.api().v2.tweet(status.status, { media: { media_ids: mediaIds } });
} catch (error) {
console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message);
throw error;
}
}
}

View File

@ -4,6 +4,7 @@ import { addUserAgent } from 'nxapi';
import pLimit from 'p-limit';
import ValueCache from '../common/ValueCache.mjs';
import prefixedConsole from '../common/prefixedConsole.mjs';
import { calculateCacheExpiry } from '../common/util.mjs';
const coralLimit = pLimit(1);
const webServiceLimit = pLimit(1);
@ -62,13 +63,6 @@ export default class NsoClient
return `nso.${token}`;
}
_calculateCacheExpiry(expiresIn) {
let expires = Date.now() + expiresIn * 1000;
// Expire 5min early to make sure we have time to execute requests
return expires - 5 * 60 * 1000;
}
// Coral API
_getCoralCache() {
@ -93,7 +87,7 @@ export default class NsoClient
this.console.info('Creating Coral session...');
let { data } = await CoralApi.createWithSessionToken(this.nintendoToken);
let expires = this._calculateCacheExpiry(data.credential.expiresIn);
let expires = calculateCacheExpiry(data.credential.expiresIn);
this.console.debug(`Caching Coral session until: ${expires}`);
await this._getCoralCache().setData(data, expires);
@ -127,7 +121,7 @@ export default class NsoClient
this.console.info(`Creating web service token for ID ${id}...`);
let { result } = await coral.getWebServiceToken(id);
let expires = this._calculateCacheExpiry(result.expiresIn);
let expires = calculateCacheExpiry(result.expiresIn);
this.console.debug(`Caching web service token for ID ${id} until: ${expires}`);
await tokenCache.setData(result, expires);

View File

@ -2,6 +2,7 @@ import fs from 'fs/promises';
import pLimit from 'p-limit';
import ValueCache from '../common/ValueCache.mjs';
import prefixedConsole from '../common/prefixedConsole.mjs';
import { calculateCacheExpiry } from '../common/util.mjs';
export const SPLATNET3_WEB_SERVICE_ID = '4834290508791808';
@ -24,13 +25,6 @@ export default class SplatNet3Client
return !!this.bulletToken;
}
_calculateCacheExpiry(expiresIn) {
let expires = Date.now() + expiresIn * 1000;
// Expire 5min early to make sure we have time to execute requests
return expires - 5 * 60 * 1000;
}
// Query hashes
async _loadQueryHashes() {
if (!this.queryHashes) {
@ -100,7 +94,7 @@ export default class SplatNet3Client
let bulletToken = await response.json();
// We can assume the token expires after 7200 seconds
let expiry = this._calculateCacheExpiry(7200);
let expiry = calculateCacheExpiry(7200);
await bulletTokenCache.setData(bulletToken, expiry);
this.console.debug(`Caching bullet token until: ${expiry}`);

View File

@ -16,13 +16,9 @@ const data = useDataStore();
onMounted(() => data.startUpdating());
onUnmounted(() => data.stopUpdating());
try {
// Detect mobile browsers
if (navigator.userAgent.match(/iPhone|iPad|Android/i)) {
document.body.classList.add('is-mobile');
}
} catch (e) {
//
// Detect mobile browsers
if (navigator.userAgent.match(/iPhone|iPad|Android/i)) {
document.body.classList.add('is-mobile');
}
</script>

View File

@ -56,6 +56,7 @@ const datetimeFormats = {
};
let i18n = null;
let storageListenerAdded = false;
export function initializeI18n() {
if (!i18n) {
@ -69,8 +70,11 @@ export function initializeI18n() {
},
});
// Listen for local storage changes
window.addEventListener('storage', reload);
// Listen for local storage changes (guard prevents duplicate listeners in HMR)
if (!storageListenerAdded) {
window.addEventListener('storage', reload);
storageListenerAdded = true;
}
reload();
}
@ -122,7 +126,7 @@ async function loadLocale() {
let response = await fetch(`/data/locale/${locale}.json`);
if (!response.ok) {
console.error(response);
console.error(`Failed to load locale ${locale}: ${response.status} ${response.statusText}`);
return;
}

View File

@ -1,5 +1,5 @@
<template>
<ProductContainer class="pt-10 pb-4" bg="bg-camo-purple" :bg-style="`background-color: ${toRgba(winner.color)};`">
<ProductContainer v-if="winner" class="pt-10 pb-4" bg="bg-camo-purple" :bg-style="`background-color: ${toRgba(winner.color)};`">
<div class="space-y-2">
<div class="font-splatoon1 text-2xl lg:text-3xl text-shadow mx-2">
{{ $t('festival.results.title') }}
@ -89,7 +89,7 @@ const resultRows = computed(() => {
});
const winnerIndex = computed(() => props.festival.teams.findIndex(t => t.result.isWinner));
const winner = computed(() => props.festival.teams[winnerIndex.value]);
const winner = computed(() => winnerIndex.value >= 0 ? props.festival.teams[winnerIndex.value] : null);
</script>
<style scoped>

View File

@ -44,7 +44,7 @@
</div>
<div v-if="tricolor?.tricolorStages">
<div v-if="tricolor?.tricolorStages.length == 1" class="flex space-x-1">
<div v-if="tricolor?.tricolorStages.length === 1" class="flex space-x-1">
<StageImage
img-class="rounded-xl"
:stage="tricolor?.tricolorStages?.[0]"

View File

@ -54,23 +54,8 @@
</div>
</div>
<!-- Time left/Order button -->
<div class="absolute top-1 left-6 space-y-2">
<div v-if="false" class="hidden mobile:block -ml-4">
<a :href="shopUrl">
<SquidTape
class="font-splatoon2 text-sm text-black rounded-sm -rotate-3"
bg="bg-splatoon-yellow"
squid-bg="bg-black"
border="border border-black"
>
<div class="px-1">
{{ $t('gear.order') }}
</div>
</SquidTape>
</a>
</div>
<!-- Time left -->
<div class="absolute top-1 left-6">
<div class="inline-block text-xs bg-zinc-200 bg-opacity-30 rounded px-1 py-px font-semibold">
{{ $t('time.left', { time: formatDurationHoursFromNow(props.gear.saleEndTime) }) }}
</div>
@ -80,9 +65,7 @@
<script setup>
import { computed } from 'vue';
import SquidTape from '@/components/SquidTape.vue';
import { formatDurationHoursFromNow } from '@/common/time';
import { getGesotownGearUrl } from '@/common/links';
const props = defineProps({
gear: Object,
@ -90,8 +73,6 @@ const props = defineProps({
const price = computed(() => props.gear.price);
const gear = computed(() => props.gear.gear);
const shopUrl = computed(() => getGesotownGearUrl(props.gear.id));
</script>
<style scoped>

View File

@ -15,7 +15,7 @@ function defineEndpointStore(id, endpoint, transform = null) {
let response = await fetch(baseUrl + endpoint);
if (!response.ok) {
console.error(response);
console.error(`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`);
return;
}

View File

@ -36,4 +36,5 @@ export const useCoopGearStore = defineStore('coopGear', () => {
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useGearStore, import.meta.hot));
import.meta.hot.accept(acceptHMRUpdate(useCoopGearStore, import.meta.hot));
}