mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
Prod can create PRs
This commit is contained in:
parent
22f96065a9
commit
b8062b010f
5 changed files with 294 additions and 40 deletions
47
.github/workflows/stviz_create_pr_from_issue.yml
vendored
Normal file
47
.github/workflows/stviz_create_pr_from_issue.yml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
name: STVIZ - Create PR from Issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
apply:
|
||||||
|
if: contains(github.event.issue.title, '[stviz]') || contains(github.event.issue.body, 'stviz-sku-edits:BEGIN')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout data branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: data
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Apply edits, push branch, open PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||||
|
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
node tools/stviz_apply_issue_edits.js
|
||||||
|
|
||||||
|
- name: Close issue
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh -R "$REPO" issue close "$ISSUE_NUMBER" -c "Processed by STVIZ automation. A PR has been opened."
|
||||||
107
tools/stviz_apply_issue_edits.js
Normal file
107
tools/stviz_apply_issue_edits.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// tools/stviz_apply_issue_edits.js
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
function die(msg) {
|
||||||
|
console.error(msg);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sh(cmd) {
|
||||||
|
return execSync(cmd, { stdio: "pipe", encoding: "utf8" }).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ISSUE_BODY = process.env.ISSUE_BODY || "";
|
||||||
|
const ISSUE_NUMBER = String(process.env.ISSUE_NUMBER || "").trim();
|
||||||
|
const ISSUE_TITLE = process.env.ISSUE_TITLE || "";
|
||||||
|
const REPO = process.env.REPO || "";
|
||||||
|
|
||||||
|
if (!ISSUE_NUMBER) die("Missing ISSUE_NUMBER");
|
||||||
|
if (!REPO) die("Missing REPO");
|
||||||
|
|
||||||
|
const m = ISSUE_BODY.match(/<!--\s*stviz-sku-edits:BEGIN\s*-->\s*([\s\S]*?)\s*<!--\s*stviz-sku-edits:END\s*-->/);
|
||||||
|
if (!m) die("No stviz payload found in issue body.");
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(m[1]);
|
||||||
|
} catch (e) {
|
||||||
|
die(`Invalid JSON payload: ${e?.message || e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload?.schema !== "stviz-sku-edits-v1") die("Unsupported payload schema.");
|
||||||
|
|
||||||
|
const linksIn = Array.isArray(payload?.links) ? payload.links : [];
|
||||||
|
const ignoresIn = Array.isArray(payload?.ignores) ? payload.ignores : [];
|
||||||
|
|
||||||
|
function normSku(s) {
|
||||||
|
return String(s || "").trim();
|
||||||
|
}
|
||||||
|
function linkKey(x) {
|
||||||
|
const a = normSku(x?.fromSku);
|
||||||
|
const b = normSku(x?.toSku);
|
||||||
|
return a && b && a !== b ? `${a}→${b}` : "";
|
||||||
|
}
|
||||||
|
function pairKey(a, b) {
|
||||||
|
const x = normSku(a), y = normSku(b);
|
||||||
|
if (!x || !y || x === y) return "";
|
||||||
|
return x < y ? `${x}|${y}` : `${y}|${x}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join("data", "sku_links.json");
|
||||||
|
let base = { generatedAt: "", links: [], ignores: [] };
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
base = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
// keep defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseLinks = Array.isArray(base?.links) ? base.links : [];
|
||||||
|
const baseIgnores = Array.isArray(base?.ignores) ? base.ignores : [];
|
||||||
|
|
||||||
|
const seenLinks = new Set(baseLinks.map(linkKey).filter(Boolean));
|
||||||
|
for (const x of linksIn) {
|
||||||
|
const k = linkKey(x);
|
||||||
|
if (!k || seenLinks.has(k)) continue;
|
||||||
|
seenLinks.add(k);
|
||||||
|
baseLinks.push({ fromSku: normSku(x.fromSku), toSku: normSku(x.toSku) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenIg = new Set(
|
||||||
|
baseIgnores
|
||||||
|
.map((x) => pairKey(x?.skuA || x?.a || x?.left, x?.skuB || x?.b || x?.right))
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
for (const x of ignoresIn) {
|
||||||
|
const k = pairKey(x?.skuA, x?.skuB);
|
||||||
|
if (!k || seenIg.has(k)) continue;
|
||||||
|
seenIg.add(k);
|
||||||
|
baseIgnores.push({ skuA: normSku(x.skuA), skuB: normSku(x.skuB) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
links: baseLinks,
|
||||||
|
ignores: baseIgnores,
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(out, null, 2) + "\n", "utf8");
|
||||||
|
|
||||||
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const branch = `stviz/issue-${ISSUE_NUMBER}-${ts}`;
|
||||||
|
|
||||||
|
sh(`git checkout -b "${branch}"`);
|
||||||
|
sh(`git add "${filePath}"`);
|
||||||
|
sh(`git commit -m "stviz: apply sku edits (issue #${ISSUE_NUMBER})"`);
|
||||||
|
|
||||||
|
sh(`git push -u origin "${branch}"`);
|
||||||
|
|
||||||
|
const prTitle = `STVIZ: SKU link updates (issue #${ISSUE_NUMBER})`;
|
||||||
|
const prBody = `Automated PR created from issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}`;
|
||||||
|
|
||||||
|
sh(`gh -R "${REPO}" pr create --base data --head "${branch}" --title "${prTitle}" --body "${prBody}"`);
|
||||||
|
|
@ -9,13 +9,9 @@ import {
|
||||||
} from "./sku.js";
|
} from "./sku.js";
|
||||||
import { loadIndex } from "./state.js";
|
import { loadIndex } from "./state.js";
|
||||||
import { aggregateBySku } from "./catalog.js";
|
import { aggregateBySku } from "./catalog.js";
|
||||||
import {
|
|
||||||
isLocalWriteMode,
|
|
||||||
loadSkuMetaBestEffort,
|
|
||||||
apiWriteSkuLink,
|
|
||||||
apiWriteSkuIgnore,
|
|
||||||
} from "./api.js";
|
|
||||||
import { loadSkuRules, clearSkuRulesCache } from "./mapping.js";
|
import { loadSkuRules, clearSkuRulesCache } from "./mapping.js";
|
||||||
|
import { inferGithubOwnerRepo, isLocalWriteMode, loadSkuMetaBestEffort, apiWriteSkuLink, apiWriteSkuIgnore } from "./api.js";
|
||||||
|
import { addPendingLink, addPendingIgnore, pendingCounts, loadPendingEdits } from "./pending.js";
|
||||||
|
|
||||||
/* ---------------- Similarity helpers ---------------- */
|
/* ---------------- Similarity helpers ---------------- */
|
||||||
|
|
||||||
|
|
@ -382,7 +378,7 @@ export async function renderSkuLinker($app) {
|
||||||
<button id="back" class="btn">← Back</button>
|
<button id="back" class="btn">← Back</button>
|
||||||
<div style="flex:1"></div>
|
<div style="flex:1"></div>
|
||||||
<span class="badge">SKU Linker</span>
|
<span class="badge">SKU Linker</span>
|
||||||
<span class="badge mono">${esc(localWrite ? "LOCAL WRITE" : "READ-ONLY")}</span>
|
${localWrite ? `<span class="badge mono">LOCAL WRITE</span>` : `<button id="createPrBtn" class="btn" disabled>Create PR</button>`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="padding:14px;">
|
<div class="card" style="padding:14px;">
|
||||||
|
|
@ -586,53 +582,102 @@ export async function renderSkuLinker($app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateButtons() {
|
function updateButtons() {
|
||||||
if (!localWrite) {
|
const isPages = !localWrite;
|
||||||
$linkBtn.disabled = true;
|
|
||||||
$ignoreBtn.disabled = true;
|
// Pages: keep Create PR button state in sync with pending edits
|
||||||
$status.textContent = "Write disabled on GitHub Pages. Use: node viz/serve.js and open 127.0.0.1.";
|
const $pr = isPages ? document.getElementById("createPrBtn") : null;
|
||||||
return;
|
if ($pr) {
|
||||||
|
const c0 = pendingCounts();
|
||||||
|
$pr.disabled = c0.total === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(pinnedL && pinnedR)) {
|
if (!(pinnedL && pinnedR)) {
|
||||||
$linkBtn.disabled = true;
|
$linkBtn.disabled = true;
|
||||||
$ignoreBtn.disabled = true;
|
$ignoreBtn.disabled = true;
|
||||||
if (!$status.textContent) $status.textContent = "Pin one item on each side to enable linking / ignoring.";
|
|
||||||
|
if (isPages) {
|
||||||
|
const c = pendingCounts();
|
||||||
|
$status.textContent = c.total
|
||||||
|
? `Pending changes: ${c.links} link(s), ${c.ignores} ignore(s). Create PR when ready.`
|
||||||
|
: "Pin one item on each side to enable linking / ignoring.";
|
||||||
|
} else {
|
||||||
|
if (!$status.textContent) $status.textContent = "Pin one item on each side to enable linking / ignoring.";
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = String(pinnedL.sku || "");
|
const a = String(pinnedL.sku || "");
|
||||||
const b = String(pinnedR.sku || "");
|
const b = String(pinnedR.sku || "");
|
||||||
|
|
||||||
if (a === b) {
|
if (a === b) {
|
||||||
$linkBtn.disabled = true;
|
$linkBtn.disabled = true;
|
||||||
$ignoreBtn.disabled = true;
|
$ignoreBtn.disabled = true;
|
||||||
$status.textContent = "Not allowed: both sides cannot be the same SKU.";
|
$status.textContent = "Not allowed: both sides cannot be the same SKU.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storesOverlap(pinnedL, pinnedR)) {
|
if (storesOverlap(pinnedL, pinnedR)) {
|
||||||
$linkBtn.disabled = true;
|
$linkBtn.disabled = true;
|
||||||
$ignoreBtn.disabled = true;
|
$ignoreBtn.disabled = true;
|
||||||
$status.textContent = "Not allowed: both items belong to the same store.";
|
$status.textContent = "Not allowed: both items belong to the same store.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sameGroup(a, b)) {
|
if (sameGroup(a, b)) {
|
||||||
$linkBtn.disabled = true;
|
$linkBtn.disabled = true;
|
||||||
$ignoreBtn.disabled = true;
|
$ignoreBtn.disabled = true;
|
||||||
$status.textContent = "Already linked: both SKUs are in the same group.";
|
$status.textContent = "Already linked: both SKUs are in the same group.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$linkBtn.disabled = false;
|
$linkBtn.disabled = false;
|
||||||
$ignoreBtn.disabled = false;
|
$ignoreBtn.disabled = false;
|
||||||
|
|
||||||
if (isIgnoredPair(a, b)) {
|
if (isIgnoredPair(a, b)) {
|
||||||
$status.textContent = "This pair is already ignored.";
|
$status.textContent = "This pair is already ignored.";
|
||||||
} else if ($status.textContent === "Pin one item on each side to enable linking / ignoring.") {
|
} else if ($status.textContent === "Pin one item on each side to enable linking / ignoring.") {
|
||||||
$status.textContent = "";
|
$status.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh PR button state after any status changes
|
||||||
|
if ($pr) {
|
||||||
|
const c = pendingCounts();
|
||||||
|
$pr.disabled = c.total === 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const $createPrBtn = document.getElementById("createPrBtn");
|
||||||
|
if ($createPrBtn) {
|
||||||
|
$createPrBtn.addEventListener("click", () => {
|
||||||
|
const c = pendingCounts();
|
||||||
|
if (c.total === 0) return;
|
||||||
|
|
||||||
|
const { owner, repo } = inferGithubOwnerRepo();
|
||||||
|
const edits = loadPendingEdits();
|
||||||
|
|
||||||
|
// Payload is small (ops list), action will merge into data/sku_links.json
|
||||||
|
const payload = JSON.stringify(
|
||||||
|
{ schema: "stviz-sku-edits-v1", createdAt: edits.createdAt || new Date().toISOString(), links: edits.links, ignores: edits.ignores },
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = `[stviz] sku link updates (${c.links} link, ${c.ignores} ignore)`;
|
||||||
|
const body =
|
||||||
|
`Automated request from GitHub Pages SKU Linker.\n\n` +
|
||||||
|
`<!-- stviz-sku-edits:BEGIN -->\n` +
|
||||||
|
payload +
|
||||||
|
`\n<!-- stviz-sku-edits:END -->\n`;
|
||||||
|
|
||||||
|
const u =
|
||||||
|
`https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}` +
|
||||||
|
`/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
||||||
|
|
||||||
|
window.open(u, "_blank", "noopener,noreferrer");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateAll() {
|
function updateAll() {
|
||||||
renderSide("L");
|
renderSide("L");
|
||||||
|
|
@ -657,11 +702,11 @@ export async function renderSkuLinker($app) {
|
||||||
});
|
});
|
||||||
|
|
||||||
$linkBtn.addEventListener("click", async () => {
|
$linkBtn.addEventListener("click", async () => {
|
||||||
if (!(pinnedL && pinnedR) || !localWrite) return;
|
if (!(pinnedL && pinnedR)) return;
|
||||||
|
|
||||||
const a = String(pinnedL.sku || "");
|
const a = String(pinnedL.sku || "");
|
||||||
const b = String(pinnedR.sku || "");
|
const b = String(pinnedR.sku || "");
|
||||||
|
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
$status.textContent = "Not allowed: missing SKU.";
|
$status.textContent = "Not allowed: missing SKU.";
|
||||||
return;
|
return;
|
||||||
|
|
@ -682,16 +727,16 @@ export async function renderSkuLinker($app) {
|
||||||
$status.textContent = "This pair is already ignored.";
|
$status.textContent = "This pair is already ignored.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const aCanon = rules.canonicalSku(a);
|
const aCanon = rules.canonicalSku(a);
|
||||||
const bCanon = rules.canonicalSku(b);
|
const bCanon = rules.canonicalSku(b);
|
||||||
|
|
||||||
const preferred = pickPreferredCanonical(allRows, [a, b, aCanon, bCanon]);
|
const preferred = pickPreferredCanonical(allRows, [a, b, aCanon, bCanon]);
|
||||||
if (!preferred) {
|
if (!preferred) {
|
||||||
$status.textContent = "Write failed: could not choose a canonical SKU.";
|
$status.textContent = "Write failed: could not choose a canonical SKU.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const writes = [];
|
const writes = [];
|
||||||
function addWrite(fromSku, toSku) {
|
function addWrite(fromSku, toSku) {
|
||||||
const f = String(fromSku || "").trim();
|
const f = String(fromSku || "").trim();
|
||||||
|
|
@ -700,12 +745,12 @@ export async function renderSkuLinker($app) {
|
||||||
if (rules.canonicalSku(f) === t) return;
|
if (rules.canonicalSku(f) === t) return;
|
||||||
writes.push({ fromSku: f, toSku: t });
|
writes.push({ fromSku: f, toSku: t });
|
||||||
}
|
}
|
||||||
|
|
||||||
addWrite(aCanon, preferred);
|
addWrite(aCanon, preferred);
|
||||||
addWrite(bCanon, preferred);
|
addWrite(bCanon, preferred);
|
||||||
addWrite(a, preferred);
|
addWrite(a, preferred);
|
||||||
addWrite(b, preferred);
|
addWrite(b, preferred);
|
||||||
|
|
||||||
const seenW = new Set();
|
const seenW = new Set();
|
||||||
const uniq = [];
|
const uniq = [];
|
||||||
for (const w of writes) {
|
for (const w of writes) {
|
||||||
|
|
@ -714,25 +759,51 @@ export async function renderSkuLinker($app) {
|
||||||
seenW.add(k);
|
seenW.add(k);
|
||||||
uniq.push(w);
|
uniq.push(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- GitHub Pages mode: stage edits locally ----------------
|
||||||
|
if (!localWrite) {
|
||||||
|
for (const w of uniq) addPendingLink(w.fromSku, w.toSku);
|
||||||
|
|
||||||
|
clearSkuRulesCache();
|
||||||
|
rules = await loadSkuRules();
|
||||||
|
ignoreSet = rules.ignoreSet;
|
||||||
|
|
||||||
|
// rebuild mappedSkus based on merged rules (includes pending)
|
||||||
|
const rebuilt = buildMappedSkuSet(rules.links || []);
|
||||||
|
mappedSkus.clear();
|
||||||
|
for (const x of rebuilt) mappedSkus.add(x);
|
||||||
|
|
||||||
|
const c = pendingCounts();
|
||||||
|
$status.textContent = `Staged locally. Pending: ${c.links} link(s), ${c.ignores} ignore(s).`;
|
||||||
|
|
||||||
|
const $pr = document.getElementById("createPrBtn");
|
||||||
|
if ($pr) $pr.disabled = c.total === 0;
|
||||||
|
|
||||||
|
pinnedL = null;
|
||||||
|
pinnedR = null;
|
||||||
|
updateAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Local mode: write via disk-backed API ----------------
|
||||||
$status.textContent = `Writing ${uniq.length} link(s) to canonical ${displaySku(preferred)} …`;
|
$status.textContent = `Writing ${uniq.length} link(s) to canonical ${displaySku(preferred)} …`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < uniq.length; i++) {
|
for (let i = 0; i < uniq.length; i++) {
|
||||||
const w = uniq[i];
|
const w = uniq[i];
|
||||||
$status.textContent = `Writing (${i + 1}/${uniq.length}): ${displaySku(w.fromSku)} → ${displaySku(w.toSku)} …`;
|
$status.textContent = `Writing (${i + 1}/${uniq.length}): ${displaySku(w.fromSku)} → ${displaySku(w.toSku)} …`;
|
||||||
await apiWriteSkuLink(w.fromSku, w.toSku);
|
await apiWriteSkuLink(w.fromSku, w.toSku);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSkuRulesCache();
|
clearSkuRulesCache();
|
||||||
rules = await loadSkuRules();
|
rules = await loadSkuRules();
|
||||||
ignoreSet = rules.ignoreSet;
|
ignoreSet = rules.ignoreSet;
|
||||||
|
|
||||||
const meta2 = await loadSkuMetaBestEffort();
|
const meta2 = await loadSkuMetaBestEffort();
|
||||||
const rebuilt = buildMappedSkuSet(meta2?.links || []);
|
const rebuilt = buildMappedSkuSet(meta2?.links || []);
|
||||||
mappedSkus.clear();
|
mappedSkus.clear();
|
||||||
for (const x of rebuilt) mappedSkus.add(x);
|
for (const x of rebuilt) mappedSkus.add(x);
|
||||||
|
|
||||||
$status.textContent = `Saved. Canonical is now ${displaySku(preferred)}.`;
|
$status.textContent = `Saved. Canonical is now ${displaySku(preferred)}.`;
|
||||||
pinnedL = null;
|
pinnedL = null;
|
||||||
pinnedR = null;
|
pinnedR = null;
|
||||||
|
|
@ -741,13 +812,14 @@ export async function renderSkuLinker($app) {
|
||||||
$status.textContent = `Write failed: ${String(e && e.message ? e.message : e)}`;
|
$status.textContent = `Write failed: ${String(e && e.message ? e.message : e)}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
$ignoreBtn.addEventListener("click", async () => {
|
$ignoreBtn.addEventListener("click", async () => {
|
||||||
if (!(pinnedL && pinnedR) || !localWrite) return;
|
if (!(pinnedL && pinnedR)) return;
|
||||||
|
|
||||||
const a = String(pinnedL.sku || "");
|
const a = String(pinnedL.sku || "");
|
||||||
const b = String(pinnedR.sku || "");
|
const b = String(pinnedR.sku || "");
|
||||||
|
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
$status.textContent = "Not allowed: missing SKU.";
|
$status.textContent = "Not allowed: missing SKU.";
|
||||||
return;
|
return;
|
||||||
|
|
@ -768,9 +840,32 @@ export async function renderSkuLinker($app) {
|
||||||
$status.textContent = "This pair is already ignored.";
|
$status.textContent = "This pair is already ignored.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- GitHub Pages mode: stage ignore locally ----------------
|
||||||
|
if (!localWrite) {
|
||||||
|
$status.textContent = `Staging ignore: ${displaySku(a)} × ${displaySku(b)} …`;
|
||||||
|
|
||||||
|
addPendingIgnore(a, b);
|
||||||
|
|
||||||
|
clearSkuRulesCache();
|
||||||
|
rules = await loadSkuRules();
|
||||||
|
ignoreSet = rules.ignoreSet;
|
||||||
|
|
||||||
|
const c = pendingCounts();
|
||||||
|
$status.textContent = `Staged locally. Pending: ${c.links} link(s), ${c.ignores} ignore(s).`;
|
||||||
|
|
||||||
|
const $pr = document.getElementById("createPrBtn");
|
||||||
|
if ($pr) $pr.disabled = c.total === 0;
|
||||||
|
|
||||||
|
pinnedL = null;
|
||||||
|
pinnedR = null;
|
||||||
|
updateAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Local mode: write via disk-backed API ----------------
|
||||||
$status.textContent = `Ignoring: ${displaySku(a)} × ${displaySku(b)} …`;
|
$status.textContent = `Ignoring: ${displaySku(a)} × ${displaySku(b)} …`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const out = await apiWriteSkuIgnore(a, b);
|
const out = await apiWriteSkuIgnore(a, b);
|
||||||
ignoreSet.add(rules.canonicalPairKey(a, b));
|
ignoreSet.add(rules.canonicalPairKey(a, b));
|
||||||
|
|
@ -782,6 +877,7 @@ export async function renderSkuLinker($app) {
|
||||||
$status.textContent = `Ignore failed: ${String(e && e.message ? e.message : e)}`;
|
$status.textContent = `Ignore failed: ${String(e && e.message ? e.message : e)}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
updateAll();
|
updateAll();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// viz/app/mapping.js
|
// viz/app/mapping.js
|
||||||
import { loadSkuMetaBestEffort } from "./api.js";
|
import { loadSkuMetaBestEffort, isLocalWriteMode } from "./api.js";
|
||||||
|
import { applyPendingToMeta } from "./pending.js";
|
||||||
|
|
||||||
let CACHED = null;
|
let CACHED = null;
|
||||||
|
|
||||||
|
|
@ -169,7 +170,10 @@ function buildGroupsAndCanonicalMap(links) {
|
||||||
export async function loadSkuRules() {
|
export async function loadSkuRules() {
|
||||||
if (CACHED) return CACHED;
|
if (CACHED) return CACHED;
|
||||||
|
|
||||||
const meta = await loadSkuMetaBestEffort();
|
let meta = await loadSkuMetaBestEffort();
|
||||||
|
if (!isLocalWriteMode()) {
|
||||||
|
meta = applyPendingToMeta(meta);
|
||||||
|
}
|
||||||
const links = Array.isArray(meta?.links) ? meta.links : [];
|
const links = Array.isArray(meta?.links) ? meta.links : [];
|
||||||
const ignores = Array.isArray(meta?.ignores) ? meta.ignores : [];
|
const ignores = Array.isArray(meta?.ignores) ? meta.ignores : [];
|
||||||
|
|
||||||
|
|
|
||||||
0
viz/app/pending.js
Normal file
0
viz/app/pending.js
Normal file
Loading…
Reference in a new issue