From ab8584ca062b6f2dfc9d00983ea4befedc7ac374 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Sun, 4 Oct 2026 09:04:15 +0800 Subject: [PATCH] Recover JAVGG list loading after HTTP timeout --- CHANGELOG.md | 8 ++ README.md | 14 +++- javgg-mini-library.js | 130 +++++++++++++++++++++++++++++---- tests/adapters.test.cjs | 122 ++++++++++++++++++++++++++++++- tools/library-restoration.json | 12 ++- 5 files changed, 263 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee4a90..0809a48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# JAVGG 1.0.3 列表加载修复 — 2026-10-04 + +- 针对 iPhone DreamBy 首页和分类 `stage=http-timeout`:列表普通 HTTP 使用 6 秒截止时间,包含异步正文;失败、验证页或不完整列表时,最多一次 12 秒网页 HTML 回退。浏览器只读取网页,不等待视频媒体,正常列表继续直接用 HTTP。 +- 兼容宿主嵌套对象/JSON、异步 `text()` 和 Promise 正文。普通 Cloudflare jsd 脚本不误作验证页;真实验证、404/410、缺失内容和超时保留明确阶段。 +- 首页/分区/分类/搜索失败返回 `error` 和空条目,撤回站点 Logo 的假内容失败卡。真实热门分区的影片补充排名;翻页地址及稳定详情动作保持。 +- 原路径、库 ID 和 `baseUrl` 保留,播放/详情解析的 19 个函数逐字保持恢复版。此次只更新 JAVGG;其他九个已恢复下载入口继续与任务前快照相同。恢复记录将 JAVGG 原快照移入 `supersededFiles`,注明用户要求的新版本。 +- 67 项媒体库回归、60 个脚本语法、39 个库契约、兼容副本、既有 SexBJCam 网络测试及发布包装检查通过。桌面真实八类两页及最近发布匿名请求验证;热门普通请求仍可能超时。iPhone HTML 回传与显示待用户更新测试。 + # 更正恢复时间:任务开始前的线上版本 — 2026-10-04 - 用户更正为昨天(2026-10-03)开始本轮任务之前的版本。采用上海时间 2026-10-03 零点之前主分支最后快照 `2fd0121bfbf0cc812d1e9e39501384710e684ade`;十个目标文件与本轮最初审查基线 `8e43017532ec81e3338046ba6e32dff7655bd3dd` 相同。 diff --git a/README.md b/README.md index 2fc666e..2e55279 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ 已经导入 DreamBy 的入口继续使用原来的下载地址和媒体库 ID。每个下载入口都包含完整 JavaScript,不需要远程加载器。 +## JAVGG 1.0.3:首页与分类加载修复 — 2026-10-04 + +用户在 iPhone DreamBy 中报告首页及各分类显示站点 Logo 的失败卡,错误为 `stage=http-timeout`。JAVGG 原版列表请求禁用浏览器回退,HTTP 失败后直接返回错误卡。此次仅修列表读取和失败显示:普通 HTTP 最多 6 秒,包含正文读取;失败或收到验证/不完整页面时,最多一次 12 秒隐藏网页 HTML 回退,不等待视频媒体。正常 HTTP 列表不打开浏览器,可使用宿主提供的正常浏览器会话;不提取或保存 Cookie。 + +首页仍只立即加载最近发布,其他分区保持懒加载。兼容原始 HTML、嵌套对象/JSON 正文及异步 `text()`。失败返回明确错误和空条目,避免把站点 Logo 的失败提示作为影片或排名内容显示;热门影片提供真实排名。原文件名、库 ID、`baseUrl` 参数、详情/播放载荷保留。 + +桌面验证:匿名 iPhone User-Agent 的最近发布/第二页及精选页返回 HTTP 200;当前浏览器会话验证八个分类的前两页,第二页均有不同影片。热门分类普通请求出现超时,网页导航曾返回 40 条。67 项离线回归通过,包含 HTTP/正文挂起、一次 HTML 回退、验证页、异步响应、翻页、空搜索及明确失败。iPhone 的网页 HTML 回传和实际列表显示仍需用户更新测试。 + +播放与详情解析的 19 个相关函数和原恢复版逐字相同;其他九个历史下载入口仍与任务前快照一致。此次列表修复更新为 1.0.3,之前的播放修复没有重新引入。 + ## 2026-10-04:更正恢复到任务开始前的版本 用户更正恢复时间为昨天(2026-10-03)开始本轮任务之前。按上海时间 `2026-10-03 00:00:00 +08:00` 之前主分支最后的快照 `2fd0121bfbf0cc812d1e9e39501384710e684ade` 恢复。这十个目标路径与原审查基线 `8e43017532ec81e3338046ba6e32dff7655bd3dd` 完全同字节;两提交之间仅新增另一个库 `hsex-mini-library.js`。 @@ -15,14 +25,14 @@ | [MissAV download-working](missav-mini-library-download-working.js) / [working 6](missav-mini-library-download-working%206.js) | 1.5.9 | | [SexBJCam](sexbjcam-mini-library.js) | 1.1.7 | | [ASMRLIB](asmrlib-mini-library.js) | 1.2.0 | -| [JAVGG](javgg-mini-library.js) | 1.0.0 | +| [JAVGG](javgg-mini-library.js) | 1.0.3(随后按用户要求修复列表加载) | | [麻豆亚洲版](madou8-mini-library%205.js) | 1.1.0 | | [套路 SM](taolusm-mini-library.js) | 1.0.0 | | [KBJ fan](kbjfan-mini-library.js) | 1.0.0 | 每个文件均恢复该时间点的原始内容、版本号和参数,原 raw 地址与库 ID 保留。MissAV 普通版和 CloudFlare 版虽然版本号相同,内容和参数不同,分别保留;只有内容相同的两个 download-working 文件继续同步。 -时间截止点、基线提交及十份文件 SHA-256 记录在 [tools/library-restoration.json](tools/library-restoration.json)。恢复检查校验每个路径的历史内容、真实 manifest、原版公共入口和对应版本行为。此前恢复最早上传版的操作已更正,下文播放修复为撤回的历史记录。 +时间截止点、基线提交及历史文件 SHA-256 记录在 [tools/library-restoration.json](tools/library-restoration.json)。JAVGG 的旧快照移入 `supersededFiles` 并标明此后按用户要求更新到 1.0.3;其余九个入口继续校验历史内容。恢复检查校验每个路径的历史内容、真实 manifest、原版公共入口和对应版本行为。此前恢复最早上传版的操作已更正,下文播放修复为撤回的历史记录。 ## 主维护文件与兼容入口 diff --git a/javgg-mini-library.js b/javgg-mini-library.js index 4cbc0f3..5020588 100644 --- a/javgg-mini-library.js +++ b/javgg-mini-library.js @@ -9,7 +9,7 @@ const WidgetMetadata = { id: 'javgg-mini-library', name: 'JAVGG', title: 'JAVGG', - version: '1.0.0', + version: '1.0.3', requiredVersion: '0.0.1', author: 'Alan huang', site: JAVGG_DEFAULT_BASE, @@ -64,10 +64,11 @@ function getManifest() { async function getHome(rawCtx) { const ctx = normalizeContext(rawCtx); let immediate = []; + let loadError = ''; try { - immediate = parseCards(ctx, await fetchText(ctx, sectionURL(ctx, JAVGG_SECTIONS[0], 1))).slice(0, 18); + immediate = (await fetchListingPage(ctx, sectionURL(ctx, JAVGG_SECTIONS[0], 1))).items.slice(0, 18); } catch (error) { - immediate = [diagnosticItem('首页加载失败', error)]; + loadError = error.message || '首页加载失败'; } const browse = { id: 'javgg-browse', @@ -76,11 +77,12 @@ async function getHome(rawCtx) { lazy: false, items: JAVGG_SECTIONS.map(categoryCard) }; - const first = sectionResult(JAVGG_SECTIONS[0], immediate); + const first = sectionResult(JAVGG_SECTIONS[0], immediate, loadError); return { pageType: 'home', id: 'javgg-home', title: 'JAVGG', + error: loadError || undefined, heroAspectRatio: '2:3', hero: immediate.filter(isMediaItem).slice(0, 5), sections: [browse, first].concat(JAVGG_SECTIONS.slice(1).map(sectionShell)) @@ -91,10 +93,10 @@ async function getHomeSection(rawCtx) { const ctx = normalizeContext(rawCtx); const section = findSection(firstNonEmpty(ctx.sectionId, ctx.id, ctx.pageId)) || JAVGG_SECTIONS[0]; try { - const items = parseCards(ctx, await fetchText(ctx, sectionURL(ctx, section, 1))).slice(0, 18); - return sectionResult(section, items.length ? items : [diagnosticItem(section.title + '暂无内容')]); + const items = (await fetchListingPage(ctx, sectionURL(ctx, section, 1))).items.slice(0, 18); + return sectionResult(section, items); } catch (error) { - return sectionResult(section, [diagnosticItem(section.title + '加载失败', error)]); + return sectionResult(section, [], error.message || section.title + '加载失败'); } } @@ -107,8 +109,9 @@ async function getCategory(rawCtx) { const title = cleanText(firstNonEmpty(ctx.title, section && section.title, dynamicPath && dynamicPath.title, 'JAVGG')); const url = section ? sectionURL(ctx, section, page) : pagedURL(baseURL(ctx) + (dynamicPath ? dynamicPath.path : '/new-post/'), page); try { - const html = await fetchText(ctx, url); - const items = parseCards(ctx, html); + const listing = await fetchListingPage(ctx, url); + const html = listing.html; + const items = listing.items; return { pageType: 'category', id: pageId, @@ -123,8 +126,8 @@ async function getCategory(rawCtx) { } catch (error) { return { pageType: 'category', id: pageId, title: title, style: 'media.posterGrid', - itemAspectRatio: '2:3', page: page, hasMore: false, - items: [diagnosticItem(title + '加载失败', error)] + itemAspectRatio: '2:3', page: page, nextPage: page, hasMore: false, + error: error.message || title + '加载失败', items: [] }; } } @@ -136,8 +139,9 @@ async function search(rawCtx) { if (!query) return { pageType: 'search', title: '搜索', keyword: '', page: page, hasMore: false, items: [] }; const url = searchURL(ctx, query, page); try { - const html = await fetchText(ctx, url); - const items = parseCards(ctx, html); + const listing = await fetchListingPage(ctx, url); + const html = listing.html; + const items = listing.items; return { pageType: 'search', title: '搜索:' + query, keyword: query, style: 'media.posterGrid', itemAspectRatio: '2:3', @@ -146,7 +150,8 @@ async function search(rawCtx) { } catch (error) { return { pageType: 'search', title: '搜索:' + query, keyword: query, - page: page, hasMore: false, items: [diagnosticItem('搜索失败', error)] + page: page, nextPage: page, hasMore: false, + error: error.message || '搜索失败', items: [] }; } } @@ -279,10 +284,14 @@ function sectionShell(section) { }; } -function sectionResult(section, items) { +function sectionResult(section, items, error) { return { id: section.id, title: section.title, style: section.style, - lazy: false, moreAction: categoryAction(section), items: items + lazy: false, moreAction: categoryAction(section), + error: error || undefined, subtitle: error || undefined, + items: section.style === 'discover.ranked' ? items.map(function (item, index) { + return Object.assign({}, item, { rank: index + 1 }); + }) : items }; } @@ -450,6 +459,95 @@ function resolveRelativeURL(base, value) { return String(base || '').replace(/[?#].*$/, '').replace(/\/[^/]*$/, '/') + value; } +// Only list pages use this bounded HTML fallback. Player/detail requests retain +// their existing implementation and never enter this list-page browser path. +async function fetchListingPage(ctx, url) { + let httpError; + try { + return await listingDeadline(async function () { + const response = await httpGet(url, { + headers: requestHeaders(url), timeout: 6, timeoutSeconds: 6, + useBrowserCookie: true, attachBrowserCookie: true, + useBrowserFallback: false, browserFallback: false, allowBrowserFallback: false + }); + return readListingResponse(ctx, response, 'http'); + }, 6, 'http-timeout'); + } catch (error) { + httpError = error || new Error('HTTP 请求失败'); + if (httpError.status === 404 || httpError.status === 410) throw httpError; + } + const httpStage = httpError.stage || (/timeout|超时/i.test(String(httpError.message)) ? 'http-timeout' : 'http-error'); + if (typeof Widget === 'undefined' || !Widget.browser || typeof Widget.browser.fetch !== 'function') { + throw listingError(httpStage, '列表读取失败,当前环境没有可用的网页回退'); + } + try { + return await listingDeadline(async function () { + const response = await Widget.browser.fetch(url, { + visible: false, timeout: 12, timeoutSeconds: 12, + waitAfterLoad: 1, headers: requestHeaders(url) + }); + return readListingResponse(ctx, response, 'browser'); + }, 12, 'browser-timeout'); + } catch (error) { + const stage = (error && error.stage) || 'browser-error'; + throw listingError(stage, '列表读取失败(HTTP阶段:' + httpStage + ')' + + (stage === 'browser-verification' ? ',请在原站完成验证后重试' : ',请稍后重试')); + } +} + +async function readListingResponse(ctx, response, stage) { + const status = Number(response && firstNonEmpty(response.statusCode, response.status)) || 0; + const html = await listingResponseText(response, 0); + if (/Just a moment|Checking(?:\s+your)?\s+browser|cf-browser-verification|cf-chl-/i.test(html.slice(0, 30000))) { + throw listingError(stage + '-verification', '来源需要浏览器验证'); + } + if (status >= 400) throw listingError(stage + '-status', '来源返回 HTTP ' + status, status); + const items = parseCards(ctx, html); + const emptyList = /<[^>]+class=["'][^"']*\b(?:no-results|noresults|not-found)\b/i.test(html) || + /(?:No results found|Nothing found|No posts found)/i.test(cleanText(html)); + if (!items.length && !emptyList) throw listingError(stage + '-content', '来源没有返回影片列表'); + return { html: html, items: items }; +} + +async function listingResponseText(response, depth) { + if (depth > 4 || response === undefined || response === null) return ''; + response = await response; + if (response === undefined || response === null) return ''; + if (typeof response === 'string') { + if (/^\s*\{/.test(response)) { + try { return await listingResponseText(JSON.parse(response), depth + 1); } catch (error) { /* Plain HTML below. */ } + } + return response; + } + if (typeof response.text === 'function') return String(await response.text()); + const keys = ['html', 'data', 'body', 'text', 'content']; + for (let index = 0; index < keys.length; index += 1) { + if (response[keys[index]] === undefined || response[keys[index]] === null) continue; + const text = await listingResponseText(response[keys[index]], depth + 1); + if (text) return text; + } + return ''; +} + +function listingError(stage, message, status) { + const error = new Error(message + ';stage=' + stage); + error.stage = stage; + if (status) error.status = status; + return error; +} + +async function listingDeadline(operation, seconds, stage) { + let timer; + try { + return await Promise.race([ + Promise.resolve().then(operation), + new Promise(function (_, reject) { + timer = setTimeout(function () { reject(listingError(stage, '来源请求超时')); }, seconds * 1000); + }) + ]); + } finally { clearTimeout(timer); } +} + async function fetchText(ctx, url, referer) { const response = await httpGet(url, { headers: requestHeaders(referer || url), diff --git a/tests/adapters.test.cjs b/tests/adapters.test.cjs index 0626c34..bccaca9 100644 --- a/tests/adapters.test.cjs +++ b/tests/adapters.test.cjs @@ -237,7 +237,6 @@ const restoredIds = { 'missav-mini-library.js': 'missav-mini-library', 'sexbjcam-mini-library.js': 'sexbjcam-mini-library', 'asmrlib-mini-library.js': 'asmrlib', - 'javgg-mini-library.js': 'javgg-mini-library', 'madou8-mini-library 5.js': 'madou8-mini-library', 'taolusm-mini-library.js': 'taolusm-mini-library', 'kbjfan-mini-library.js': 'kbjfan-mini-library', @@ -308,3 +307,124 @@ test('Taolu pre-task snapshot retains its download version and original playback assert.equal(playback.url, groups[0].versions[0].url); assert.equal(playback.container, 'mp4'); }); + +function javggListHTML(slugs, next) { + return '
' + slugs.map(slug => + '
' + + '
' + ).join('') + '
' + (next ? '' : '') + ''; +} +function javggListContext() { + return load('javgg-mini-library.js', { setTimeout: (fn, ms) => setTimeout(fn, ms / 1000) }); +} +test('JAVGG list repair retains its imported ID, parameter and superseded snapshot record', () => { + const c = javggListContext(); + assert.equal(c.getManifest().id, 'javgg-mini-library'); + assert.equal(c.getManifest().version, '1.0.3'); + assert.deepEqual(plain(c.getManifest().parameters.map(x => x.name)), ['baseUrl']); + assert.equal(restoration.supersededFiles['javgg-mini-library.js'].version, '1.0.0'); + assert.equal(restoration.supersededFiles['javgg-mini-library.js'].replacementVersion, '1.0.3'); +}); +test('JAVGG home uses one ordinary HTTP request and retains real poster and detail actions', async () => { + const c = javggListContext(); let http = 0, browser = 0; + c.Widget.http.get = async (url, options) => { + http++; assert.equal(url, 'https://javgg.net/new-post/'); + assert.equal(options.timeoutSeconds, 6); assert.equal(options.useBrowserCookie, true); + assert.equal(options.browserFallback, false); + return { status: 200, data: javggListHTML(['a', 'b', 'c']) }; + }; + c.Widget.browser = { async fetch() { browser++; throw new Error('not needed'); } }; + const home = await c.getHome('{}'); + assert.equal(http, 1); assert.equal(browser, 0); assert.equal(home.hero.length, 3); + assert.equal(home.sections[1].items[0].poster, 'https://img.test/a.jpg'); + assert.equal(c.detailURLFromId(home.hero[0].action.itemId), 'https://javgg.net/jav/a/'); + assert.equal(home.sections.filter(x => x.lazy).length, 9); assert.equal(home.error, undefined); +}); +test('JAVGG hung HTTP gets exactly one bounded HTML browser fallback without media capture', async () => { + const c = javggListContext(); let browser = 0; + c.Widget.http.get = () => new Promise(() => {}); + c.Widget.browser = { async fetch(url, options) { + browser++; assert.equal(url, 'https://javgg.net/new-post/'); + assert.equal(options.timeoutSeconds, 12); assert.equal(options.visible, false); + assert.equal(options.waitForMediaSource, undefined); assert.equal(options.captureMedia, undefined); + return { html: javggListHTML(['recovered']) }; + } }; + const home = await c.getHome({}); + assert.equal(browser, 1); assert.equal(home.hero.length, 1); assert.equal(home.error, undefined); +}); +test('JAVGG host http-timeout error and async browser body recover the requested category', async () => { + const c = javggListContext(); const requests = []; + c.Widget.http.get = async () => { throw new Error('来源请求超时;stage=http-timeout'); }; + c.Widget.browser = { async fetch(url) { requests.push(url); return { statusCode: 200, async text() { return javggListHTML(['weekly']); } }; } }; + const page = await c.getCategory({ pageId: 'popular-weekly', page: 2 }); + assert.deepEqual(requests, ['https://javgg.net/trending/page/2/?sort=weekly']); + assert.equal(page.items.length, 1); assert.equal(page.page, 2); assert.equal(page.error, undefined); +}); +test('JAVGG Checking Browser pivots to HTML; ordinary Cloudflare jsd does not', async () => { + const c = javggListContext(); let browser = 0; + c.Widget.http.get = async () => ({ status: 200, data: 'Checking Browser' }); + c.Widget.browser = { async fetch() { browser++; return { body: { html: javggListHTML(['verified']) } }; } }; + assert.equal((await c.getHomeSection({ sectionId: 'featured' })).items.length, 1); + c.Widget.http.get = async () => ({ status: 200, data: javggListHTML(['plain']) + '' }); + assert.equal((await c.getHomeSection({ sectionId: 'featured' })).items.length, 1); + assert.equal(browser, 1); +}); +test('JAVGG total listing wait includes a hanging HTTP response body', async () => { + const c = javggListContext(); let browser = 0; + c.Widget.http.get = async () => ({ status: 200, text() { return new Promise(() => {}); } }); + c.Widget.browser = { async fetch() { browser++; return javggListHTML(['body-recovery']); } }; + assert.equal((await c.getHomeSection({ sectionId: 'new-post' })).items.length, 1); + assert.equal(browser, 1); +}); +test('JAVGG nested JSON and promise body responses retain real pagination and ranked items', async () => { + const c = javggListContext(); const requested = []; + c.Widget.http.get = async url => { + requested.push(url); + const second = url.includes('/page/2/'); + return { statusCode: 200, body: Promise.resolve(JSON.stringify({ data: { html: javggListHTML([second ? 'two' : 'one'], second ? '' : 'https://javgg.net/trending/page/2/?sort=today') } })) }; + }; + const first = await c.getCategory({ pageId: 'popular-today', page: 1 }); + const second = await c.getCategory(JSON.stringify({ pageId: 'popular-today', page: 2 })); + assert.equal(first.hasMore, true); assert.equal(second.hasMore, false); + assert.notEqual(first.items[0].id, second.items[0].id); + assert.deepEqual(requested, ['https://javgg.net/trending/?sort=today', 'https://javgg.net/trending/page/2/?sort=today']); + const ranked = await c.getHomeSection({ sectionId: 'popular-today' }); + assert.equal(ranked.items[0].rank, 1); +}); +test('JAVGG listing failure returns explicit error and no fake failure artwork', async () => { + const c = javggListContext(); let browser = 0; + c.Widget.http.get = async () => { throw new Error('timeout'); }; + c.Widget.browser = { fetch() { browser++; return new Promise(() => {}); } }; + const home = await c.getHome({}); + assert.match(home.error, /stage=browser-timeout/); assert.equal(home.hero.length, 0); + assert.equal(home.sections[1].items.length, 0); assert.match(home.sections[1].error, /http-timeout/); + const section = await c.getHomeSection({ sectionId: 'featured' }); + assert.equal(section.items.length, 0); assert.ok(section.error); + assert.equal(browser, 2); +}); +test('JAVGG genuine not-found page fails without a browser retry or advancing pagination', async () => { + const c = javggListContext(); let browser = 0; + c.Widget.http.get = async () => ({ status: 404, data: 'Not found' }); + c.Widget.browser = { async fetch() { browser++; return javggListHTML(['wrong']); } }; + const page = await c.getCategory({ pageId: 'new-post', page: 2 }); + assert.equal(page.items.length, 0); assert.equal(page.nextPage, 2); + assert.match(page.error, /HTTP 404/); assert.equal(browser, 0); +}); +test('JAVGG browser verification stays a visible error and cannot become an empty success', async () => { + const c = javggListContext(); + c.Widget.http.get = async () => ({ status: 403, data: 'Checking Browser' }); + c.Widget.browser = { async fetch() { return { status: 403, html: 'Just a moment' }; } }; + const page = await c.getCategory({ pageId: 'featured' }); + assert.equal(page.items.length, 0); assert.match(page.error, /browser-verification/); + assert.match(page.error, /完成验证/); +}); +test('JAVGG empty real search results are valid while missing browser support reports the HTTP stage', async () => { + const c = javggListContext(); const urls = []; + c.Widget.http.get = async url => { urls.push(url); return { status: 200, data: '
Nothing found
' }; }; + const result = await c.search({ query: 'A&B #tag', page: 2 }); + assert.equal(result.items.length, 0); assert.equal(result.error, undefined); + assert.deepEqual(urls, ['https://javgg.net/page/2/?s=A%26B%20%23tag']); + c.Widget.http.get = async () => { throw new Error('stage=http-timeout'); }; + const failed = await c.getCategory({ pageId: 'new-post' }); + assert.equal(failed.items.length, 0); assert.match(failed.error, /stage=http-timeout/); +}); diff --git a/tools/library-restoration.json b/tools/library-restoration.json index 17bae8c..bd2676a 100644 --- a/tools/library-restoration.json +++ b/tools/library-restoration.json @@ -14,10 +14,6 @@ "version": "1.2.0", "sha256": "2cb404dbbab67626d762248dbe050ab7e534ea68278ff745c5d304b6a487faa0" }, - "javgg-mini-library.js": { - "version": "1.0.0", - "sha256": "39092a7afb7107182c33fc923d93cb99c20ea3fb97d01810ef1f4e974de8885e" - }, "madou8-mini-library 5.js": { "version": "1.1.0", "sha256": "7ea8fc7aec7352e470fc0019f509723dce4e8c5f1a1f05d4ab692bca8f3bb488" @@ -42,5 +38,13 @@ "version": "1.5.9", "sha256": "20f5cc483a756e52a7eab4d306a978c8b1d1bf04c75a3d3584c68a1915d15ce6" } + }, + "supersededFiles": { + "javgg-mini-library.js": { + "version": "1.0.0", + "sha256": "39092a7afb7107182c33fc923d93cb99c20ea3fb97d01810ef1f4e974de8885e", + "replacementVersion": "1.0.3", + "reason": "User requested home and category loading repair on 2026-10-04" + } } }