Sync launcher: stop phantom update prompt, forum News panel, hardware-aware render distance
Build check / build (push) Has been cancelled

Squashed sync from upstream. Highlights:
- Updater no longer reports already-applied deletes as a pending update on
  every launch (guard the del branch on the target still existing)
- Derive the packaged CSP image origin from the configured server URL
- Forum Announcements panel + News tab; hardware-aware farClip recommendation;
  parchment UI; localization and tweak updates
- Addon source refresh; schema and mod-state fixes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
OctoWoW
2026-07-04 16:02:46 -07:00
parent 16e442ea0f
commit fbad749f0c
35 changed files with 763 additions and 56 deletions
+44
View File
@@ -0,0 +1,44 @@
import fetch from 'node-fetch';
import Logger from 'electron-log/main';
import { ForumAnnouncementSchema, type ForumAnnouncement } from '~common/schemas';
import { createTRPCRouter, publicProcedure } from '../trpc';
const FETCH_TIMEOUT_MS = 8_000;
const fetchLatestAnnouncement = async (): Promise<ForumAnnouncement | null> => {
const url = `${
import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'
}/forum/octonews.php?forum=35&mode=full`;
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw Error(`HTTP ${res.status}`);
const json = (await res.json()) as unknown;
if (!json || typeof json !== 'object' || !('id' in json)) return null;
const parsed = ForumAnnouncementSchema.safeParse(json);
if (!parsed.success) {
Logger.error(
'Forum announcement failed schema validation',
parsed.error.flatten()
);
throw Error('Malformed forum announcement');
}
return parsed.data;
} finally {
clearTimeout(t);
}
};
export const forumRouter = createTRPCRouter({
latestAnnouncement: publicProcedure.query(async () => {
try {
return await fetchLatestAnnouncement();
} catch (e) {
Logger.error('Failed to fetch forum announcement', e);
throw e;
}
})
});
+10
View File
@@ -5,11 +5,21 @@ import { z } from 'zod';
import { mainWindow } from '~main/index';
import Preferences from '~main/modules/preferences';
import { addDefenderExclusions } from '~main/modules/defender';
import { detectHardware, recommendFarClip } from '~main/modules/hardware';
import { createTRPCRouter, publicProcedure } from '../trpc';
export const generalRouter = createTRPCRouter({
appVersion: publicProcedure.query(() => app.getVersion()),
hardware: publicProcedure.query(() => {
const hardware = Preferences.data.hardware ?? null;
return { hardware, recommendedFarClip: recommendFarClip(hardware) };
}),
redetectHardware: publicProcedure.mutation(async () => {
const hardware = await detectHardware();
Preferences.data = { hardware };
return { hardware, recommendedFarClip: recommendFarClip(hardware) };
}),
quit: publicProcedure.mutation(() => app.quit()),
minimize: publicProcedure.mutation(() => mainWindow?.minimize()),
openLink: publicProcedure
+13
View File
@@ -1,6 +1,10 @@
import path from 'path';
import { z } from 'zod';
import Mods from '~main/modules/mods';
import Preferences from '~main/modules/preferences';
import { isGameRunning } from '~main/modules/updater';
import { ModIdSchema } from '~common/mods';
import { createTRPCRouter, publicProcedure } from '../trpc';
@@ -15,5 +19,14 @@ export const modsRouter = createTRPCRouter({
.input(z.object({ id: ModIdSchema, ignore: z.boolean() }))
.mutation(({ input }) => Mods.setIgnoreUpdates(input.id, input.ignore)),
applyAll: publicProcedure.mutation(() => Mods.applyAll()),
repair: publicProcedure.mutation(async () => {
const clientDir = Preferences.data?.clientDir;
if (clientDir) {
const exePath = path.join(clientDir, 'WoW.exe');
if (await isGameRunning(exePath))
throw new Error('Please close WoW first before verifying files.');
}
return Mods.applyAll({ repairOnly: true });
}),
observe: publicProcedure.subscription(() => Mods.observe())
});
+3 -1
View File
@@ -8,7 +8,9 @@ import { createTRPCRouter, publicProcedure } from '../trpc';
const FETCH_TIMEOUT_MS = 8_000;
const fetchNews = async (): Promise<NewsItem[]> => {
const url = `${import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'}/news.json`;
const url = `${
import.meta.env.MAIN_VITE_SERVER_URL || 'https://octowow.st'
}/forum/octonews.php?mode=list&forum=2&limit=3`;
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {