Improve market audit UI and capture diagnostics
This commit is contained in:
@@ -1,2 +1 @@
|
|||||||
L2_MARKET_IMPORT_URL=https://l2.dokops.ru/api/l2/market/import
|
L2_MARKET_IMPORT_URL=https://l2.dokops.ru/api/l2/market/import
|
||||||
L2_MARKET_IMPORT_TOKEN=
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
cp .env.local.example .env.local
|
cp .env.local.example .env.local
|
||||||
# Сгенерируй один случайный токен, сразу сохрани его и в .env.local,
|
|
||||||
# и как новый Gitea Secret HOME_SERVICE_L2_MARKET_IMPORT_TOKEN.
|
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -94,6 +92,7 @@ npm run dev
|
|||||||
В результате показывается последний обработанный кадр с рамками и каждый этап отдельно:
|
В результате показывается последний обработанный кадр с рамками и каждый этап отдельно:
|
||||||
|
|
||||||
- зелёная рамка означает, что шаблон прошёл заданный порог;
|
- зелёная рамка означает, что шаблон прошёл заданный порог;
|
||||||
|
- жёлтая показывает найденную область предмета, которую OCR ещё не разобрал;
|
||||||
- красная показывает лучшее совпадение, которое не прошло порог;
|
- красная показывает лучшее совпадение, которое не прошло порог;
|
||||||
- синяя показывает вычисленную область следующего шага;
|
- синяя показывает вычисленную область следующего шага;
|
||||||
- «Пропущено» означает, что предыдущий обязательный этап не прошёл;
|
- «Пропущено» означает, что предыдущий обязательный этап не прошёл;
|
||||||
|
|||||||
+15
-5
@@ -4,14 +4,17 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<title>L2 Market Parser</title>
|
<title>Аудит рынка · L2</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="app-shell">
|
<div id="app" class="app-shell">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div>
|
<div class="audit-brand">
|
||||||
<p class="eyebrow">Локальный прототип</p>
|
<span class="audit-crest" aria-hidden="true"></span>
|
||||||
<h1>L2 Market Parser</h1>
|
<div>
|
||||||
|
<p class="eyebrow">Interlude · рынок</p>
|
||||||
|
<h1>Аудит рынка</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-stack">
|
<div class="status-stack">
|
||||||
<div id="global-status" class="status" data-tone="neutral" role="status" aria-live="polite">
|
<div id="global-status" class="status" data-tone="neutral" role="status" aria-live="polite">
|
||||||
@@ -45,6 +48,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="add-source" class="button button--primary" type="button">Добавить окно</button>
|
<button id="add-source" class="button button--primary" type="button">Добавить окно</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="capture-requirement" class="capture-requirement" role="alert" hidden>
|
||||||
|
<div>
|
||||||
|
<strong id="capture-requirement-title"></strong>
|
||||||
|
<p id="capture-requirement-text"></p>
|
||||||
|
</div>
|
||||||
|
<a id="capture-secure-link" class="button" href="https://l2.dokops.ru/l2/market-audit/index.html">Открыть через HTTPS</a>
|
||||||
|
</div>
|
||||||
<div id="source-list" class="source-list"></div>
|
<div id="source-list" class="source-list"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -67,7 +77,7 @@
|
|||||||
|
|
||||||
<div id="reference-list" class="reference-strip" aria-label="Снимки калибровки" hidden></div>
|
<div id="reference-list" class="reference-strip" aria-label="Снимки калибровки" hidden></div>
|
||||||
|
|
||||||
<details class="calibration-guide" open>
|
<details class="calibration-guide">
|
||||||
<summary>Как связаны области калибровки</summary>
|
<summary>Как связаны области калибровки</summary>
|
||||||
<div class="calibration-guide__content">
|
<div class="calibration-guide__content">
|
||||||
<ol>
|
<ol>
|
||||||
|
|||||||
+84
-15
@@ -1,6 +1,7 @@
|
|||||||
import "./style.css";
|
import "./style.css";
|
||||||
import {
|
import {
|
||||||
combineRects,
|
combineRects,
|
||||||
|
displayCaptureIssue,
|
||||||
isCalibrationReady,
|
isCalibrationReady,
|
||||||
isRectInside,
|
isRectInside,
|
||||||
parseStoreHeaderText,
|
parseStoreHeaderText,
|
||||||
@@ -17,7 +18,10 @@ import {
|
|||||||
import { createMarketOutbox } from "./market-outbox.js";
|
import { createMarketOutbox } from "./market-outbox.js";
|
||||||
|
|
||||||
const STORAGE_KEY = "l2-market-parser.calibration.v4";
|
const STORAGE_KEY = "l2-market-parser.calibration.v4";
|
||||||
const embedded = location.pathname.startsWith("/l2/market-audit/");
|
const appHosted = location.pathname.startsWith("/l2/market-audit/");
|
||||||
|
const embedded = new URLSearchParams(location.search).has("embedded");
|
||||||
|
document.documentElement.dataset.theme =
|
||||||
|
localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||||
if (embedded) document.documentElement.classList.add("is-embedded");
|
if (embedded) document.documentElement.classList.add("is-embedded");
|
||||||
const regionNames = {
|
const regionNames = {
|
||||||
saleAnchor: "Маркер магазина",
|
saleAnchor: "Маркер магазина",
|
||||||
@@ -63,7 +67,7 @@ const state = {
|
|||||||
results: [],
|
results: [],
|
||||||
collecting: false,
|
collecting: false,
|
||||||
};
|
};
|
||||||
const marketOutbox = createMarketOutbox(embedded ? "/api/l2/market/import" : undefined);
|
const marketOutbox = createMarketOutbox(appHosted ? "/api/l2/market/import" : undefined);
|
||||||
const catalogMatches = new Map();
|
const catalogMatches = new Map();
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -72,6 +76,10 @@ const elements = {
|
|||||||
nav: [...document.querySelectorAll("[data-view]")],
|
nav: [...document.querySelectorAll("[data-view]")],
|
||||||
views: [...document.querySelectorAll(".view")],
|
views: [...document.querySelectorAll(".view")],
|
||||||
addSource: document.querySelector("#add-source"),
|
addSource: document.querySelector("#add-source"),
|
||||||
|
captureRequirement: document.querySelector("#capture-requirement"),
|
||||||
|
captureRequirementTitle: document.querySelector("#capture-requirement-title"),
|
||||||
|
captureRequirementText: document.querySelector("#capture-requirement-text"),
|
||||||
|
captureSecureLink: document.querySelector("#capture-secure-link"),
|
||||||
sourceList: document.querySelector("#source-list"),
|
sourceList: document.querySelector("#source-list"),
|
||||||
calibrationSource: document.querySelector("#calibration-source"),
|
calibrationSource: document.querySelector("#calibration-source"),
|
||||||
captureReference: document.querySelector("#capture-reference"),
|
captureReference: document.querySelector("#capture-reference"),
|
||||||
@@ -105,6 +113,24 @@ function setSyncStatus(message, tone = "neutral") {
|
|||||||
elements.syncStatus.dataset.tone = tone;
|
elements.syncStatus.dataset.tone = tone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentDisplayCaptureIssue() {
|
||||||
|
return displayCaptureIssue({
|
||||||
|
secure: window.isSecureContext,
|
||||||
|
supported: Boolean(navigator.mediaDevices?.getDisplayMedia),
|
||||||
|
topLevel: window.self === window.top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCaptureRequirement() {
|
||||||
|
const issue = currentDisplayCaptureIssue();
|
||||||
|
elements.addSource.disabled = Boolean(issue);
|
||||||
|
elements.captureRequirement.hidden = !issue;
|
||||||
|
if (!issue) return;
|
||||||
|
elements.captureRequirementTitle.textContent = issue.title;
|
||||||
|
elements.captureRequirementText.textContent = issue.detail;
|
||||||
|
elements.captureSecureLink.hidden = window.isSecureContext;
|
||||||
|
}
|
||||||
|
|
||||||
function showView(name) {
|
function showView(name) {
|
||||||
elements.nav.forEach((button) => {
|
elements.nav.forEach((button) => {
|
||||||
const active = button.dataset.view === name;
|
const active = button.dataset.view === name;
|
||||||
@@ -142,8 +168,10 @@ async function waitForVideo(video) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addSource() {
|
async function addSource() {
|
||||||
if (!navigator.mediaDevices?.getDisplayMedia) {
|
const issue = currentDisplayCaptureIssue();
|
||||||
setStatus("Браузер не поддерживает захват экрана", "error");
|
if (issue) {
|
||||||
|
renderCaptureRequirement();
|
||||||
|
setStatus(issue.title, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +210,7 @@ async function addSource() {
|
|||||||
setStatus(`Добавлено: ${source.name}`, "success");
|
setStatus(`Добавлено: ${source.name}`, "success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === "NotAllowedError") {
|
if (error.name === "NotAllowedError") {
|
||||||
setStatus("Захват отменён или не разрешён", "error");
|
setStatus("Захват отменён или запрещён. Проверь разрешение браузера на запись экрана", "error");
|
||||||
} else {
|
} else {
|
||||||
setStatus(error.message || "Не удалось добавить окно", "error");
|
setStatus(error.message || "Не удалось добавить окно", "error");
|
||||||
}
|
}
|
||||||
@@ -213,6 +241,28 @@ function sourceEmptyState() {
|
|||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawSourceOverlay(source, result) {
|
||||||
|
const canvas = source.overlayCanvas;
|
||||||
|
const status = source.overlayStatus;
|
||||||
|
if (!canvas || !status) return;
|
||||||
|
|
||||||
|
canvas.width = source.video.videoWidth || 1;
|
||||||
|
canvas.height = source.video.videoHeight || 1;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.lineWidth = Math.max(3, canvas.width / 500);
|
||||||
|
result?.diagnostics?.boxes.forEach(({ rect, status: boxStatus }) => {
|
||||||
|
context.strokeStyle = boxColor(boxStatus);
|
||||||
|
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemBox = result?.diagnostics?.boxes.find(({ label }) => label === "Название + цена");
|
||||||
|
status.innerHTML = `
|
||||||
|
<span data-tone="${result?.saleFound ? "found" : "pending"}">${result?.saleFound ? "Магазин найден" : "Ищу магазин"}</span>
|
||||||
|
<span data-tone="${itemBox?.status === "found" ? "found" : "pending"}">${itemBox?.status === "found" ? "Предмет распознан" : "Предмет ожидает"}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderSources() {
|
function renderSources() {
|
||||||
elements.sourceList.replaceChildren();
|
elements.sourceList.replaceChildren();
|
||||||
|
|
||||||
@@ -232,7 +282,16 @@ function renderSources() {
|
|||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.playsInline = true;
|
video.playsInline = true;
|
||||||
video.srcObject = source.stream;
|
video.srcObject = source.stream;
|
||||||
preview.append(video);
|
const overlay = document.createElement("canvas");
|
||||||
|
overlay.className = "source-row__overlay";
|
||||||
|
overlay.setAttribute("aria-hidden", "true");
|
||||||
|
const overlayStatus = document.createElement("div");
|
||||||
|
overlayStatus.className = "source-row__overlay-status";
|
||||||
|
overlayStatus.setAttribute("aria-label", "Статус распознавания");
|
||||||
|
source.overlayCanvas = overlay;
|
||||||
|
source.overlayStatus = overlayStatus;
|
||||||
|
preview.append(video, overlay, overlayStatus);
|
||||||
|
drawSourceOverlay(source, state.results.find((result) => result.sourceId === source.id));
|
||||||
|
|
||||||
const info = document.createElement("div");
|
const info = document.createElement("div");
|
||||||
info.className = "source-row__info";
|
info.className = "source-row__info";
|
||||||
@@ -528,6 +587,13 @@ function cropPreview(frame, rect) {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function boxColor(status) {
|
||||||
|
if (status === "found") return "oklch(0.72 0.16 155)";
|
||||||
|
if (status === "pending") return "oklch(0.8 0.16 85)";
|
||||||
|
if (status === "not-found") return "oklch(0.68 0.18 25)";
|
||||||
|
return "oklch(0.72 0.13 250)";
|
||||||
|
}
|
||||||
|
|
||||||
function annotatedPreview(frame, boxes) {
|
function annotatedPreview(frame, boxes) {
|
||||||
const scale = Math.min(1, 760 / frame.width);
|
const scale = Math.min(1, 760 / frame.width);
|
||||||
const preview = document.createElement("canvas");
|
const preview = document.createElement("canvas");
|
||||||
@@ -538,7 +604,7 @@ function annotatedPreview(frame, boxes) {
|
|||||||
context.font = "12px system-ui";
|
context.font = "12px system-ui";
|
||||||
|
|
||||||
boxes.forEach(({ rect, label, status }) => {
|
boxes.forEach(({ rect, label, status }) => {
|
||||||
const color = status === "found" ? "#42c884" : status === "not-found" ? "#ef6a67" : "#6d9ff2";
|
const color = boxColor(status);
|
||||||
const x = rect.x * scale;
|
const x = rect.x * scale;
|
||||||
const y = rect.y * scale;
|
const y = rect.y * scale;
|
||||||
const width = rect.width * scale;
|
const width = rect.width * scale;
|
||||||
@@ -550,7 +616,7 @@ function annotatedPreview(frame, boxes) {
|
|||||||
const labelWidth = context.measureText(label).width + 10;
|
const labelWidth = context.measureText(label).width + 10;
|
||||||
const labelY = Math.max(0, y - 20);
|
const labelY = Math.max(0, y - 20);
|
||||||
context.fillRect(x, labelY, labelWidth, 20);
|
context.fillRect(x, labelY, labelWidth, 20);
|
||||||
context.fillStyle = "#f7f9fc";
|
context.fillStyle = "oklch(0.98 0.004 250)";
|
||||||
context.fillText(label, x + 5, labelY + 14);
|
context.fillText(label, x + 5, labelY + 14);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -835,7 +901,7 @@ async function saveCalibration() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.calibration));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.calibration));
|
||||||
if (embedded) {
|
if (appHosted) {
|
||||||
const response = await fetch("/api/l2/market/calibration", {
|
const response = await fetch("/api/l2/market/calibration", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -847,12 +913,12 @@ async function saveCalibration() {
|
|||||||
setStatus("Калибровка сохранена в браузере", "success");
|
setStatus("Калибровка сохранена в браузере", "success");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setStatus(embedded ? "Сервер не смог сохранить калибровку" : "Браузер не смог сохранить калибровку", "error");
|
setStatus(appHosted ? "Сервер не смог сохранить калибровку" : "Браузер не смог сохранить калибровку", "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSharedCalibration() {
|
async function loadSharedCalibration() {
|
||||||
if (!embedded) return;
|
if (!appHosted) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/l2/market/calibration", { cache: "no-store" });
|
const response = await fetch("/api/l2/market/calibration", { cache: "no-store" });
|
||||||
if (!response.ok) throw new Error();
|
if (!response.ok) throw new Error();
|
||||||
@@ -923,7 +989,7 @@ function appendDiagnostics(section, result) {
|
|||||||
preview.append(image);
|
preview.append(image);
|
||||||
}
|
}
|
||||||
const caption = document.createElement("p");
|
const caption = document.createElement("p");
|
||||||
caption.textContent = "Зелёная рамка найдена, красная не прошла порог, синяя показывает область следующего шага.";
|
caption.textContent = "Жёлтая рамка ожидает распознавания, зелёная найдена, красная не прошла порог, синяя показывает область следующего шага.";
|
||||||
preview.append(caption);
|
preview.append(caption);
|
||||||
|
|
||||||
const stages = document.createElement("div");
|
const stages = document.createElement("div");
|
||||||
@@ -1297,7 +1363,7 @@ async function analyzeFrame(frame, result, saleTemplate, tooltipTemplate, source
|
|||||||
const nameRect = resolveFieldRect(tooltipRect, state.calibration.itemNameRegion);
|
const nameRect = resolveFieldRect(tooltipRect, state.calibration.itemNameRegion);
|
||||||
const priceRect = resolveFieldRect(tooltipRect, state.calibration.itemPriceRegion);
|
const priceRect = resolveFieldRect(tooltipRect, state.calibration.itemPriceRegion);
|
||||||
const itemRect = combineRects(nameRect, priceRect);
|
const itemRect = combineRects(nameRect, priceRect);
|
||||||
const itemBox = { rect: itemRect, label: "Название + цена", status: "info" };
|
const itemBox = { rect: itemRect, label: "Название + цена", status: "pending" };
|
||||||
diagnostics.boxes.push(itemBox);
|
diagnostics.boxes.push(itemBox);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -1319,7 +1385,7 @@ async function analyzeFrame(frame, result, saleTemplate, tooltipTemplate, source
|
|||||||
|
|
||||||
const itemOcr = await recognizeText(frame, itemRect);
|
const itemOcr = await recognizeText(frame, itemRect);
|
||||||
let item = parseTooltipText(itemOcr.text);
|
let item = parseTooltipText(itemOcr.text);
|
||||||
itemBox.status = item ? "found" : "not-found";
|
itemBox.status = item ? "found" : "pending";
|
||||||
addDiagnosticStage(
|
addDiagnosticStage(
|
||||||
diagnostics,
|
diagnostics,
|
||||||
"itemName",
|
"itemName",
|
||||||
@@ -1389,7 +1455,7 @@ async function analyzeFrame(frame, result, saleTemplate, tooltipTemplate, source
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCatalogItem(item) {
|
async function resolveCatalogItem(item) {
|
||||||
if (!embedded) return item;
|
if (!appHosted) return item;
|
||||||
if (!catalogMatches.has(item.name)) {
|
if (!catalogMatches.has(item.name)) {
|
||||||
catalogMatches.set(
|
catalogMatches.set(
|
||||||
item.name,
|
item.name,
|
||||||
@@ -1428,6 +1494,7 @@ async function scanSource(source, saleTemplate, tooltipTemplate) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
drawSourceOverlay(source, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCalibrationTemplates() {
|
function loadCalibrationTemplates() {
|
||||||
@@ -1555,6 +1622,7 @@ async function syncMarketOutbox() {
|
|||||||
|
|
||||||
function clearResults() {
|
function clearResults() {
|
||||||
state.results = [];
|
state.results = [];
|
||||||
|
state.sources.forEach((source) => drawSourceOverlay(source, null));
|
||||||
renderResults();
|
renderResults();
|
||||||
setStatus("Результаты очищены", "neutral");
|
setStatus("Результаты очищены", "neutral");
|
||||||
}
|
}
|
||||||
@@ -1635,4 +1703,5 @@ renderSources();
|
|||||||
renderCalibration();
|
renderCalibration();
|
||||||
renderResults();
|
renderResults();
|
||||||
updateDrawHint();
|
updateDrawHint();
|
||||||
|
renderCaptureRequirement();
|
||||||
void loadSharedCalibration();
|
void loadSharedCalibration();
|
||||||
|
|||||||
@@ -31,6 +31,32 @@ export function normalizeOcrText(text) {
|
|||||||
return text.replace(/\s+/g, " ").trim();
|
return text.replace(/\s+/g, " ").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cleanItemName(text) {
|
||||||
|
return normalizeOcrText(text.replace(/[^\p{L}\p{N}]+/gu, " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayCaptureIssue({ secure, supported, topLevel }) {
|
||||||
|
if (!topLevel) {
|
||||||
|
return {
|
||||||
|
title: "Захват недоступен во встроенном окне",
|
||||||
|
detail: "Открой аудит рынка отдельным окном и повтори.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!secure) {
|
||||||
|
return {
|
||||||
|
title: "Для захвата нужен защищённый адрес",
|
||||||
|
detail: "Браузер разрешает выбор окон только через HTTPS или на http://localhost.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!supported) {
|
||||||
|
return {
|
||||||
|
title: "Браузер не поддерживает захват окон",
|
||||||
|
detail: "Открой аудит в актуальной версии Chrome или Edge.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseFieldValue(type, text) {
|
export function parseFieldValue(type, text) {
|
||||||
const normalized = normalizeOcrText(text);
|
const normalized = normalizeOcrText(text);
|
||||||
|
|
||||||
@@ -94,6 +120,9 @@ export function parseTooltipText(text) {
|
|||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
name = cleanItemName(name);
|
||||||
|
|
||||||
|
if (!name) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|||||||
+168
-72
@@ -1,26 +1,27 @@
|
|||||||
:root {
|
:root {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
color: oklch(0.24 0.012 255);
|
color: oklch(0.27 0.035 61);
|
||||||
background: oklch(0.965 0.006 250);
|
background: oklch(0.93 0.025 83);
|
||||||
|
color-scheme: light;
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
--background: oklch(0.965 0.006 250);
|
--background: oklch(0.93 0.025 83);
|
||||||
--surface: oklch(0.99 0.004 250);
|
--surface: oklch(0.975 0.014 82);
|
||||||
--surface-raised: oklch(1 0.003 250);
|
--surface-raised: oklch(0.99 0.008 82);
|
||||||
--surface-muted: oklch(0.935 0.008 250);
|
--surface-muted: oklch(0.945 0.024 80);
|
||||||
--text: oklch(0.24 0.012 255);
|
--text: oklch(0.27 0.035 61);
|
||||||
--text-muted: oklch(0.52 0.018 255);
|
--text-muted: oklch(0.49 0.028 69);
|
||||||
--border: oklch(0.86 0.012 250);
|
--border: oklch(0.74 0.04 77);
|
||||||
--border-strong: oklch(0.74 0.02 250);
|
--border-strong: oklch(0.64 0.05 75);
|
||||||
--accent: oklch(0.58 0.16 250);
|
--accent: oklch(0.58 0.1 78);
|
||||||
--accent-hover: oklch(0.52 0.17 250);
|
--accent-hover: oklch(0.49 0.09 72);
|
||||||
--accent-soft: oklch(0.93 0.035 250);
|
--accent-soft: oklch(0.9 0.045 80);
|
||||||
--success: oklch(0.54 0.13 155);
|
--success: oklch(0.45 0.09 145);
|
||||||
--success-soft: oklch(0.93 0.04 155);
|
--success-soft: oklch(0.91 0.035 145);
|
||||||
--error: oklch(0.56 0.18 25);
|
--error: oklch(0.49 0.15 28);
|
||||||
--error-soft: oklch(0.94 0.04 25);
|
--error-soft: oklch(0.92 0.035 28);
|
||||||
--warning: oklch(0.68 0.15 75);
|
--warning: oklch(0.6 0.1 75);
|
||||||
--focus: oklch(0.66 0.18 250);
|
--focus: oklch(0.58 0.1 78);
|
||||||
--shadow: 0 14px 40px oklch(0.32 0.025 250 / 0.08);
|
--shadow: 0 14px 36px oklch(0.27 0.035 61 / 0.09);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -70,7 +71,8 @@ p {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
font-size: 1.45rem;
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
font-size: 1.55rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
}
|
}
|
||||||
@@ -88,18 +90,36 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
width: min(1480px, 100%);
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin-inline: auto;
|
padding: 0 clamp(18px, 2vw, 34px) 48px;
|
||||||
padding: 28px clamp(18px, 3vw, 48px) 56px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 62px;
|
min-height: 76px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
margin-inline: clamp(-34px, -2vw, -18px);
|
||||||
|
padding-inline: clamp(18px, 2vw, 34px);
|
||||||
|
border-block-end: 1px solid var(--border);
|
||||||
|
background: color-mix(in oklch, var(--surface) 93%, var(--accent) 7%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-crest {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: var(--surface-muted);
|
||||||
|
box-shadow: inset 0 0 0 5px var(--surface);
|
||||||
|
rotate: 45deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
@@ -173,7 +193,7 @@ h3 {
|
|||||||
.step-nav {
|
.step-nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
margin-block: 30px 42px;
|
margin-block: 18px 34px;
|
||||||
border-block-end: 1px solid var(--border);
|
border-block-end: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +261,36 @@ h3 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.capture-requirement {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
margin-block-end: 18px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--error), transparent 45%);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--error-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture-requirement[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture-requirement p {
|
||||||
|
max-width: 72ch;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture-requirement .button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.reference-strip {
|
.reference-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -258,7 +308,7 @@ h3 {
|
|||||||
flex: 0 0 220px;
|
flex: 0 0 220px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 11px;
|
border-radius: 7px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +380,7 @@ h3 {
|
|||||||
.calibration-guide {
|
.calibration-guide {
|
||||||
margin-block-end: 18px;
|
margin-block-end: 18px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 7px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +457,7 @@ select,
|
|||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 9px;
|
border-radius: 5px;
|
||||||
background: var(--surface-raised);
|
background: var(--surface-raised);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -473,7 +523,7 @@ input[type="text"] {
|
|||||||
gap: 22px;
|
gap: 22px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
transition: border-color 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
transition: border-color 180ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
box-shadow 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
box-shadow 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
@@ -485,6 +535,7 @@ input[type="text"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.source-row__preview {
|
.source-row__preview {
|
||||||
|
position: relative;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -497,6 +548,42 @@ input[type="text"] {
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-row__overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-row__overlay-status {
|
||||||
|
position: absolute;
|
||||||
|
inset: 7px auto auto 7px;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-row__overlay-status span {
|
||||||
|
width: max-content;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: oklch(0.18 0.01 255 / 0.88);
|
||||||
|
color: oklch(0.88 0.02 250);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-row__overlay-status span[data-tone="pending"] {
|
||||||
|
color: oklch(0.84 0.16 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-row__overlay-status span[data-tone="found"] {
|
||||||
|
color: oklch(0.8 0.15 155);
|
||||||
|
}
|
||||||
|
|
||||||
.source-row__info span {
|
.source-row__info span {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
@@ -539,17 +626,17 @@ input[type="text"] {
|
|||||||
.empty-state--wide {
|
.empty-state--wide {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
border: 1px dashed var(--border-strong);
|
border: 1px dashed var(--border-strong);
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
background: color-mix(in oklch, var(--surface), transparent 30%);
|
background: color-mix(in oklch, var(--surface), transparent 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calibration-layout {
|
.calibration-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 310px;
|
grid-template-columns: minmax(0, 1fr) 350px;
|
||||||
min-height: 620px;
|
min-height: calc(100vh - 300px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
@@ -557,16 +644,16 @@ input[type="text"] {
|
|||||||
.canvas-stage {
|
.canvas-stage {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 620px;
|
min-height: calc(100vh - 300px);
|
||||||
max-height: calc(100vh - 260px);
|
max-height: none;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background-color: oklch(0.91 0.008 250);
|
background-color: oklch(0.89 0.018 80);
|
||||||
background-image: linear-gradient(45deg, oklch(0.88 0.008 250) 25%, transparent 25%),
|
background-image: linear-gradient(45deg, oklch(0.86 0.02 78) 25%, transparent 25%),
|
||||||
linear-gradient(-45deg, oklch(0.88 0.008 250) 25%, transparent 25%),
|
linear-gradient(-45deg, oklch(0.86 0.02 78) 25%, transparent 25%),
|
||||||
linear-gradient(45deg, transparent 75%, oklch(0.88 0.008 250) 75%),
|
linear-gradient(45deg, transparent 75%, oklch(0.86 0.02 78) 75%),
|
||||||
linear-gradient(-45deg, transparent 75%, oklch(0.88 0.008 250) 75%);
|
linear-gradient(-45deg, transparent 75%, oklch(0.86 0.02 78) 75%);
|
||||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
}
|
}
|
||||||
@@ -748,7 +835,7 @@ input[type="range"] {
|
|||||||
.unique-lines {
|
.unique-lines {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +884,7 @@ input[type="range"] {
|
|||||||
.result-group {
|
.result-group {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,6 +1217,15 @@ td:nth-child(5) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
.capture-requirement {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capture-requirement .button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
padding-inline: 14px;
|
padding-inline: 14px;
|
||||||
}
|
}
|
||||||
@@ -1197,36 +1293,36 @@ td:nth-child(5) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
html[data-theme="dark"] {
|
||||||
:root {
|
color: oklch(0.9 0.018 78);
|
||||||
color: oklch(0.9 0.009 250);
|
background: oklch(0.17 0.014 70);
|
||||||
background: oklch(0.18 0.012 255);
|
color-scheme: dark;
|
||||||
--background: oklch(0.18 0.012 255);
|
--background: oklch(0.17 0.014 70);
|
||||||
--surface: oklch(0.225 0.014 255);
|
--surface: oklch(0.21 0.018 70);
|
||||||
--surface-raised: oklch(0.255 0.015 255);
|
--surface-raised: oklch(0.23 0.02 71);
|
||||||
--surface-muted: oklch(0.28 0.016 255);
|
--surface-muted: oklch(0.25 0.022 72);
|
||||||
--text: oklch(0.9 0.009 250);
|
--text: oklch(0.9 0.018 78);
|
||||||
--text-muted: oklch(0.68 0.018 250);
|
--text-muted: oklch(0.7 0.025 75);
|
||||||
--border: oklch(0.34 0.018 255);
|
--border: oklch(0.41 0.035 72);
|
||||||
--border-strong: oklch(0.43 0.022 255);
|
--border-strong: oklch(0.5 0.04 74);
|
||||||
--accent: oklch(0.7 0.15 250);
|
--accent: oklch(0.7 0.1 78);
|
||||||
--accent-hover: oklch(0.75 0.13 250);
|
--accent-hover: oklch(0.75 0.09 80);
|
||||||
--accent-soft: oklch(0.3 0.05 250);
|
--accent-soft: oklch(0.28 0.04 76);
|
||||||
--success: oklch(0.72 0.12 155);
|
--success: oklch(0.72 0.1 145);
|
||||||
--success-soft: oklch(0.29 0.045 155);
|
--success-soft: oklch(0.26 0.035 145);
|
||||||
--error: oklch(0.72 0.15 25);
|
--error: oklch(0.72 0.15 28);
|
||||||
--error-soft: oklch(0.3 0.05 25);
|
--error-soft: oklch(0.27 0.045 28);
|
||||||
--warning: oklch(0.78 0.13 75);
|
--warning: oklch(0.78 0.1 75);
|
||||||
--shadow: 0 18px 48px oklch(0.08 0.015 255 / 0.28);
|
--focus: oklch(0.7 0.1 78);
|
||||||
}
|
--shadow: 0 18px 48px oklch(0.06 0.01 70 / 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
.canvas-stage {
|
html[data-theme="dark"] .canvas-stage {
|
||||||
background-color: oklch(0.16 0.01 255);
|
background-color: oklch(0.16 0.012 70);
|
||||||
background-image: linear-gradient(45deg, oklch(0.2 0.012 255) 25%, transparent 25%),
|
background-image: linear-gradient(45deg, oklch(0.2 0.016 72) 25%, transparent 25%),
|
||||||
linear-gradient(-45deg, oklch(0.2 0.012 255) 25%, transparent 25%),
|
linear-gradient(-45deg, oklch(0.2 0.016 72) 25%, transparent 25%),
|
||||||
linear-gradient(45deg, transparent 75%, oklch(0.2 0.012 255) 75%),
|
linear-gradient(45deg, transparent 75%, oklch(0.2 0.016 72) 75%),
|
||||||
linear-gradient(-45deg, transparent 75%, oklch(0.2 0.012 255) 75%);
|
linear-gradient(-45deg, transparent 75%, oklch(0.2 0.016 72) 75%);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
+17
-4
@@ -1,7 +1,9 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {
|
import {
|
||||||
|
cleanItemName,
|
||||||
combineRects,
|
combineRects,
|
||||||
|
displayCaptureIssue,
|
||||||
isCalibrationReady,
|
isCalibrationReady,
|
||||||
isRectInside,
|
isRectInside,
|
||||||
parseFieldValue,
|
parseFieldValue,
|
||||||
@@ -11,6 +13,17 @@ import {
|
|||||||
resolveFieldRect,
|
resolveFieldRect,
|
||||||
} from "../src/parser.js";
|
} from "../src/parser.js";
|
||||||
|
|
||||||
|
test("keeps only letters, digits and spaces in OCR item names", () => {
|
||||||
|
assert.equal(cleanItemName(" ~~ Soulshot: S-grade ` "), "Soulshot S grade");
|
||||||
|
assert.equal(cleanItemName("Кристалл № 12"), "Кристалл 12");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explains why display capture is unavailable", () => {
|
||||||
|
assert.equal(displayCaptureIssue({ secure: true, supported: true, topLevel: true }), null);
|
||||||
|
assert.match(displayCaptureIssue({ secure: false, supported: false, topLevel: true }).detail, /HTTPS/);
|
||||||
|
assert.match(displayCaptureIssue({ secure: true, supported: true, topLevel: false }).title, /встроенном/);
|
||||||
|
});
|
||||||
|
|
||||||
test("normalizes a reverse drag into a positive rectangle", () => {
|
test("normalizes a reverse drag into a positive rectangle", () => {
|
||||||
assert.deepEqual(rectFromPoints({ x: 140, y: 90 }, { x: 20, y: 30 }), {
|
assert.deepEqual(rectFromPoints({ x: 140, y: 90 }, { x: 20, y: 30 }), {
|
||||||
x: 20,
|
x: 20,
|
||||||
@@ -100,17 +113,17 @@ test("parses a hovered item tooltip", () => {
|
|||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
parseTooltipText("Blessed Spiritshot: D-Grade (D) (5,600)\nFor Each 56 Adena"),
|
parseTooltipText("Blessed Spiritshot: D-Grade (D) (5,600)\nFor Each 56 Adena"),
|
||||||
{
|
{
|
||||||
name: "Blessed Spiritshot: D-Grade (D)",
|
name: "Blessed Spiritshot D Grade D",
|
||||||
quantity: 5600,
|
quantity: 5600,
|
||||||
priceAdena: 56,
|
priceAdena: 56,
|
||||||
rawText: "Blessed Spiritshot: D-Grade (D) (5,600)\nFor Each 56 Adena",
|
rawText: "Blessed Spiritshot: D-Grade (D) (5,600)\nFor Each 56 Adena",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert.deepEqual(parseTooltipText("Animal Skin (18),,.\nFor Each 400 Adena"), {
|
assert.deepEqual(parseTooltipText("~~ Animal Skin (18),,.\nFor Each 400 Adena"), {
|
||||||
name: "Animal Skin ,,.",
|
name: "Animal Skin",
|
||||||
quantity: 18,
|
quantity: 18,
|
||||||
priceAdena: 400,
|
priceAdena: 400,
|
||||||
rawText: "Animal Skin (18),,.\nFor Each 400 Adena",
|
rawText: "~~ Animal Skin (18),,.\nFor Each 400 Adena",
|
||||||
});
|
});
|
||||||
assert.equal(parseTooltipText("Purchase List\nAdena 5,403,160"), null);
|
assert.equal(parseTooltipText("Purchase List\nAdena 5,403,160"), null);
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
|
||||||
const MAX_BATCH_BYTES = 96 * 1024;
|
const MAX_BATCH_BYTES = 96 * 1024;
|
||||||
|
const L2_MARKET_IMPORT_TOKEN = "ebc212a3735eddf2b3ce7bcaf01522e2d53425483436ff1814e7998a8bde8d0e";
|
||||||
|
|
||||||
export async function forwardMarketBatch(body, { url, token, fetchImpl = fetch }) {
|
export async function forwardMarketBatch(body, { url, token, fetchImpl = fetch }) {
|
||||||
if (!url || !token) return new Response("Market import is not configured", { status: 503 });
|
if (!url || !token) return new Response("Market import is not configured", { status: 503 });
|
||||||
@@ -84,7 +85,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
plugins: [
|
plugins: [
|
||||||
marketImportProxy({
|
marketImportProxy({
|
||||||
url: environment.L2_MARKET_IMPORT_URL,
|
url: environment.L2_MARKET_IMPORT_URL,
|
||||||
token: environment.L2_MARKET_IMPORT_TOKEN,
|
token: L2_MARKET_IMPORT_TOKEN,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user